diff --git a/README.rst b/README.rst
index c063de79..13ab0f92 100644
--- a/README.rst
+++ b/README.rst
@@ -9,11 +9,9 @@ in Spotify's vast archive, manage playlists, and play music, you can use most
platforms, including Windows, Mac OS X, Linux, Android and iOS.
To install Mopidy, check out
-`the installation docs `_.
+`the installation docs `_.
-- `Documentation for the latest release `_
-- `Documentation for the development version
- `_
+- `Documentation `_
- `Source code `_
- `Issue tracker `_
- IRC: ``#mopidy`` at `irc.freenode.net `_
diff --git a/data/mopidy.desktop b/data/mopidy.desktop
index 70257d58..88dd5ae4 100644
--- a/data/mopidy.desktop
+++ b/data/mopidy.desktop
@@ -8,3 +8,4 @@ TryExec=mopidy
Exec=mopidy
Terminal=true
Categories=AudioVideo;Audio;Player;ConsoleOnly;
+StartupNotify=true
diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html
deleted file mode 100644
index d6cb00e9..00000000
--- a/docs/_templates/layout.html
+++ /dev/null
@@ -1,15 +0,0 @@
-{% extends "!layout.html" %}
-
-{% block footer %}
-{{ super() }}
-
-
-{% endblock %}
diff --git a/docs/_themes/nature/static/nature.css_t b/docs/_themes/nature/static/nature.css_t
deleted file mode 100644
index b6c0f22e..00000000
--- a/docs/_themes/nature/static/nature.css_t
+++ /dev/null
@@ -1,236 +0,0 @@
-/**
- * Sphinx stylesheet -- default theme
- * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- */
-
-@import url("basic.css");
-
-/* -- page layout ----------------------------------------------------------- */
-
-body {
- font-family: Arial, sans-serif;
- font-size: 100%;
- background-color: #111111;
- color: #555555;
- margin: 0;
- padding: 0;
-}
-
-div.documentwrapper {
- float: left;
- width: 100%;
-}
-
-div.bodywrapper {
- margin: 0 0 0 300px;
-}
-
-hr{
- border: 1px solid #B1B4B6;
-}
-
-div.document {
- background-color: #eeeeee;
-}
-
-div.body {
- background-color: #ffffff;
- color: #3E4349;
- padding: 1em 30px 30px 30px;
- font-size: 0.9em;
-}
-
-div.footer {
- color: #555;
- width: 100%;
- padding: 13px 0;
- text-align: center;
- font-size: 75%;
-}
-
-div.footer a {
- color: #444444;
-}
-
-div.related {
- background-color: #6BA81E;
- line-height: 36px;
- color: #ffffff;
- text-shadow: 0px 1px 0 #444444;
- font-size: 1.1em;
-}
-
-div.related a {
- color: #E2F3CC;
-}
-
-div.related .right {
- font-size: 0.9em;
-}
-
-div.sphinxsidebar {
- font-size: 0.9em;
- line-height: 1.5em;
- width: 300px
-}
-
-div.sphinxsidebarwrapper{
- padding: 20px 0;
-}
-
-div.sphinxsidebar h3,
-div.sphinxsidebar h4 {
- font-family: Arial, sans-serif;
- color: #222222;
- font-size: 1.2em;
- font-weight: bold;
- margin: 0;
- padding: 5px 10px;
- text-shadow: 1px 1px 0 white
-}
-
-div.sphinxsidebar h3 a {
- color: #444444;
-}
-
-div.sphinxsidebar p {
- color: #888888;
- padding: 5px 20px;
- margin: 0.5em 0px;
-}
-
-div.sphinxsidebar p.topless {
-}
-
-div.sphinxsidebar ul {
- margin: 10px 10px 10px 20px;
- padding: 0;
- color: #000000;
-}
-
-div.sphinxsidebar a {
- color: #444444;
-}
-
-div.sphinxsidebar a:hover {
- color: #E32E00;
-}
-
-div.sphinxsidebar input {
- border: 1px solid #cccccc;
- font-family: sans-serif;
- font-size: 1.1em;
- padding: 0.15em 0.3em;
-}
-
-div.sphinxsidebar input[type=text]{
- margin-left: 20px;
-}
-
-/* -- body styles ----------------------------------------------------------- */
-
-a {
- color: #005B81;
- text-decoration: none;
-}
-
-a:hover {
- color: #E32E00;
-}
-
-div.body h1,
-div.body h2,
-div.body h3,
-div.body h4,
-div.body h5,
-div.body h6 {
- font-family: Arial, sans-serif;
- font-weight: normal;
- color: #212224;
- margin: 30px 0px 10px 0px;
- padding: 5px 0 5px 0px;
- text-shadow: 0px 1px 0 white;
- border-bottom: 1px solid #C8D5E3;
-}
-
-div.body h1 { margin-top: 0; font-size: 200%; }
-div.body h2 { font-size: 150%; }
-div.body h3 { font-size: 120%; }
-div.body h4 { font-size: 110%; }
-div.body h5 { font-size: 100%; }
-div.body h6 { font-size: 100%; }
-
-a.headerlink {
- color: #c60f0f;
- font-size: 0.8em;
- padding: 0 4px 0 4px;
- text-decoration: none;
-}
-
-a.headerlink:hover {
- background-color: #c60f0f;
- color: white;
-}
-
-div.body p, div.body dd, div.body li {
- line-height: 1.8em;
-}
-
-div.admonition p.admonition-title + p {
- display: inline;
-}
-
-div.highlight{
- background-color: white;
-}
-
-div.note {
- background-color: #eeeeee;
- border: 1px solid #cccccc;
-}
-
-div.seealso {
- background-color: #ffffcc;
- border: 1px solid #ffff66;
-}
-
-div.topic {
- background-color: #fafafa;
- border-width: 0;
-}
-
-div.warning {
- background-color: #ffe4e4;
- border: 1px solid #ff6666;
-}
-
-p.admonition-title {
- display: inline;
-}
-
-p.admonition-title:after {
- content: ":";
-}
-
-pre {
- padding: 10px;
- background-color: #eeeeee;
- color: #222222;
- line-height: 1.5em;
- font-size: 1.1em;
- margin: 1.5em 0 1.5em 0;
- -webkit-box-shadow: 0px 0px 4px #d8d8d8;
- -moz-box-shadow: 0px 0px 4px #d8d8d8;
- box-shadow: 0px 0px 4px #d8d8d8;
-}
-
-tt {
- color: #222222;
- padding: 1px 2px;
- font-size: 1.2em;
- font-family: monospace;
-}
-
-#table-of-contents ul {
- padding-left: 2em;
-}
diff --git a/docs/_themes/nature/static/pygments.css b/docs/_themes/nature/static/pygments.css
deleted file mode 100644
index 652b7612..00000000
--- a/docs/_themes/nature/static/pygments.css
+++ /dev/null
@@ -1,54 +0,0 @@
-.c { color: #999988; font-style: italic } /* Comment */
-.k { font-weight: bold } /* Keyword */
-.o { font-weight: bold } /* Operator */
-.cm { color: #999988; font-style: italic } /* Comment.Multiline */
-.cp { color: #999999; font-weight: bold } /* Comment.preproc */
-.c1 { color: #999988; font-style: italic } /* Comment.Single */
-.gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
-.ge { font-style: italic } /* Generic.Emph */
-.gr { color: #aa0000 } /* Generic.Error */
-.gh { color: #999999 } /* Generic.Heading */
-.gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
-.go { color: #111 } /* Generic.Output */
-.gp { color: #555555 } /* Generic.Prompt */
-.gs { font-weight: bold } /* Generic.Strong */
-.gu { color: #aaaaaa } /* Generic.Subheading */
-.gt { color: #aa0000 } /* Generic.Traceback */
-.kc { font-weight: bold } /* Keyword.Constant */
-.kd { font-weight: bold } /* Keyword.Declaration */
-.kp { font-weight: bold } /* Keyword.Pseudo */
-.kr { font-weight: bold } /* Keyword.Reserved */
-.kt { color: #445588; font-weight: bold } /* Keyword.Type */
-.m { color: #009999 } /* Literal.Number */
-.s { color: #bb8844 } /* Literal.String */
-.na { color: #008080 } /* Name.Attribute */
-.nb { color: #999999 } /* Name.Builtin */
-.nc { color: #445588; font-weight: bold } /* Name.Class */
-.no { color: #ff99ff } /* Name.Constant */
-.ni { color: #800080 } /* Name.Entity */
-.ne { color: #990000; font-weight: bold } /* Name.Exception */
-.nf { color: #990000; font-weight: bold } /* Name.Function */
-.nn { color: #555555 } /* Name.Namespace */
-.nt { color: #000080 } /* Name.Tag */
-.nv { color: purple } /* Name.Variable */
-.ow { font-weight: bold } /* Operator.Word */
-.mf { color: #009999 } /* Literal.Number.Float */
-.mh { color: #009999 } /* Literal.Number.Hex */
-.mi { color: #009999 } /* Literal.Number.Integer */
-.mo { color: #009999 } /* Literal.Number.Oct */
-.sb { color: #bb8844 } /* Literal.String.Backtick */
-.sc { color: #bb8844 } /* Literal.String.Char */
-.sd { color: #bb8844 } /* Literal.String.Doc */
-.s2 { color: #bb8844 } /* Literal.String.Double */
-.se { color: #bb8844 } /* Literal.String.Escape */
-.sh { color: #bb8844 } /* Literal.String.Heredoc */
-.si { color: #bb8844 } /* Literal.String.Interpol */
-.sx { color: #bb8844 } /* Literal.String.Other */
-.sr { color: #808000 } /* Literal.String.Regex */
-.s1 { color: #bb8844 } /* Literal.String.Single */
-.ss { color: #bb8844 } /* Literal.String.Symbol */
-.bp { color: #999999 } /* Name.Builtin.Pseudo */
-.vc { color: #ff99ff } /* Name.Variable.Class */
-.vg { color: #ff99ff } /* Name.Variable.Global */
-.vi { color: #ff99ff } /* Name.Variable.Instance */
-.il { color: #009999 } /* Literal.Number.Integer.Long */
\ No newline at end of file
diff --git a/docs/_themes/nature/theme.conf b/docs/_themes/nature/theme.conf
deleted file mode 100644
index 1cc40044..00000000
--- a/docs/_themes/nature/theme.conf
+++ /dev/null
@@ -1,4 +0,0 @@
-[theme]
-inherit = basic
-stylesheet = nature.css
-pygments_style = tango
diff --git a/docs/api/frontends.rst b/docs/api/frontends.rst
index 792e4bc9..af0cc991 100644
--- a/docs/api/frontends.rst
+++ b/docs/api/frontends.rst
@@ -7,7 +7,7 @@ The following requirements applies to any frontend implementation:
- A frontend MAY do mostly whatever it wants to, including creating threads,
opening TCP ports and exposing Mopidy for a group of clients.
- A frontend MUST implement at least one `Pykka
- `_ actor, called the "main actor" from here
+ `_ actor, called the "main actor" from here
on.
- It MAY use additional actors to implement whatever it does, and using actors
in frontend implementations is encouraged.
@@ -28,3 +28,4 @@ Frontend implementations
* :mod:`mopidy.frontends.lastfm`
* :mod:`mopidy.frontends.mpd`
+* :mod:`mopidy.frontends.mpris`
diff --git a/docs/changes.rst b/docs/changes.rst
index b0d320eb..a4aae058 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -4,14 +4,144 @@ Changes
This change log is used to track all major changes to Mopidy.
+v0.7.3 (2012-08-11)
+===================
-v0.6.0 (in development)
-=======================
+A small maintenance release to fix a crash affecting a few users, and a couple
+of small adjustments to the Spotify backend.
+
+**Changes**
+
+- Fixed crash when logging :exc:`IOError` exceptions on systems using languages
+ with non-ASCII characters, like French.
+
+- Move the default location of the Spotify cache from `~/.cache/mopidy` to
+ `~/.cache/mopidy/spotify`. You can change this by setting
+ :attr:`mopidy.settings.SPOTIFY_CACHE_PATH`.
+
+- Reduce time required to update the Spotify cache on startup. One one
+ system/Spotify account, the time from clean cache to ready for use was
+ reduced from 35s to 12s.
+
+
+v0.7.2 (2012-05-07)
+===================
+
+This is a maintenance release to make Mopidy 0.7 build on systems without all
+of Mopidy's runtime dependencies, like Launchpad PPAs.
+
+**Changes**
+
+- Change from version tuple at :attr:`mopidy.VERSION` to :pep:`386` compliant
+ version string at :attr:`mopidy.__version__` to conform to :pep:`396`.
+
+
+v0.7.1 (2012-04-22)
+===================
+
+This is a maintenance release to make Mopidy 0.7 work with pyspotify >= 1.7.
+
+**Changes**
+
+- Don't override pyspotify's ``notify_main_thread`` callback. The default
+ implementation is sensible, while our override did nothing.
+
+
+v0.7.0 (2012-02-25)
+===================
+
+Not a big release with regard to features, but this release got some
+performance improvements over v0.6, especially for slower Atom systems. It also
+fixes a couple of other bugs, including one which made Mopidy crash when using
+GStreamer from the prereleases of Ubuntu 12.04.
+
+**Changes**
+
+- The MPD command ``playlistinfo`` is now faster, thanks to John Bäckstrand.
+
+- Added the method
+ :meth:`mopidy.backends.base.CurrentPlaylistController.length()`,
+ :meth:`mopidy.backends.base.CurrentPlaylistController.index()`, and
+ :meth:`mopidy.backends.base.CurrentPlaylistController.slice()` to reduce the
+ need for copying the entire current playlist from one thread to another.
+ Thanks to John Bäckstrand for pinpointing the issue.
+
+- Fix crash on creation of config and cache directories if intermediate
+ directories does not exist. This was especially the case on OS X, where
+ ``~/.config`` doesn't exist for most users.
+
+- Fix ``gst.LinkError`` which appeared when using newer versions of GStreamer,
+ e.g. on Ubuntu 12.04 Alpha. (Fixes: :issue:`144`)
+
+- Fix crash on mismatching quotation in ``list`` MPD queries. (Fixes:
+ :issue:`137`)
+
+- Volume is now reported to be the same as the volume was set to, also when
+ internal rounding have been done due to
+ :attr:`mopidy.settings.MIXER_MAX_VOLUME` has been set to cap the volume. This
+ should make it possible to manage capped volume from clients that only
+ increase volume with one step at a time, like ncmpcpp does.
+
+
+v0.6.1 (2011-12-28)
+===================
+
+This is a maintenance release to make Mopidy 0.6 work with pyspotify >= 1.5,
+which Mopidy's develop branch have supported for a long time. This should also
+make the Debian packages work out of the box again.
+
+**Important changes**
+
+- pyspotify 1.5 or greater is required.
+
+**Changes**
+
+- Spotify playlist folder boundaries are now properly detected. In other words,
+ if you use playlist folders, you will no longer get lots of log messages
+ about bad playlists.
+
+
+
+v0.6.0 (2011-10-09)
+===================
+
+The development of Mopidy have been quite slow for the last couple of months,
+but we do have some goodies to release which have been idling in the
+develop branch since the warmer days of the summer. This release brings support
+for the MPD ``idle`` command, which makes it possible for a client wait for
+updates from the server instead of polling every second. Also, we've added
+support for the MPRIS standard, so that Mopidy can be controlled over D-Bus
+from e.g. the Ubuntu Sound Menu.
+
+Please note that 0.6.0 requires some updated dependencies, as listed under
+*Important changes* below.
**Important changes**
- Pykka 0.12.3 or greater is required.
+- pyspotify 1.4 or greater is required.
+
+- All config, data, and cache locations are now based on the XDG spec.
+
+ - This means that your settings file will need to be moved from
+ ``~/.mopidy/settings.py`` to ``~/.config/mopidy/settings.py``.
+ - Your Spotify cache will now be stored in ``~/.cache/mopidy`` instead of
+ ``~/.mopidy/spotify_cache``.
+ - The local backend's ``tag_cache`` should now be in
+ ``~/.local/share/mopidy/tag_cache``, likewise your playlists will be in
+ ``~/.local/share/mopidy/playlists``.
+ - The local client now tries to lookup where your music is via XDG, it will
+ fall-back to ``~/music`` or use whatever setting you set manually.
+
+- The MPD command ``idle`` is now supported by Mopidy for the following
+ subsystems: player, playlist, options, and mixer. (Fixes: :issue:`32`)
+
+- A new frontend :mod:`mopidy.frontends.mpris` have been added. It exposes
+ Mopidy through the `MPRIS interface `_ over D-Bus. In
+ practice, this makes it possible to control Mopidy through the `Ubuntu Sound
+ Menu `_.
+
**Changes**
- Replace :attr:`mopidy.backends.base.Backend.uri_handlers` with
@@ -22,12 +152,26 @@ v0.6.0 (in development)
wanting to receive events from the backend. This is a formalization of the
ad hoc events the Last.fm scrobbler has already been using for some time.
-- Fix metadata update in Shoutcast streaming (Fixes: :issue:`122`)
+- Replaced all of the MPD network code that was provided by asyncore with
+ custom stack. This change was made to facilitate support for the ``idle``
+ command, and to reduce the number of event loops being used.
-- Multiple simultaneously playing outputs was considered more trouble than what
- it is worth maintnance wise. Thus, this feature has been axed for now.
- Switching outputs is still posible, but only one can be active at a time, and
- it is still the case that switching during playback does not funtion.
+- Fix metadata update in Shoutcast streaming. (Fixes: :issue:`122`)
+
+- Unescape all incoming MPD requests. (Fixes: :issue:`113`)
+
+- Increase the maximum number of results returned by Spotify searches from 32
+ to 100.
+
+- Send Spotify search queries to pyspotify as unicode objects, as required by
+ pyspotify 1.4. (Fixes: :issue:`129`)
+
+- Add setting :attr:`mopidy.settings.MPD_SERVER_MAX_CONNECTIONS`. (Fixes:
+ :issue:`134`)
+
+- Remove `destroy()` methods from backend controller and provider APIs, as it
+ was not in use and actually not called by any code. Will reintroduce when
+ needed.
v0.5.0 (2011-06-15)
@@ -112,6 +256,18 @@ Please note that 0.5.0 requires some updated dependencies, as listed under
- Found and worked around strange WMA metadata behaviour.
+- Backend API:
+
+ - Calling on :meth:`mopidy.backends.base.playback.PlaybackController.next`
+ and :meth:`mopidy.backends.base.playback.PlaybackController.previous` no
+ longer implies that playback should be started. The playback state--whether
+ playing, paused or stopped--will now be kept.
+
+ - The method
+ :meth:`mopidy.backends.base.playback.PlaybackController.change_track`
+ has been added. Like ``next()``, and ``prev()``, it changes the current
+ track without changing the playback state.
+
v0.4.1 (2011-05-06)
===================
@@ -120,7 +276,7 @@ This is a bug fix release fixing audio problems on older GStreamer and some
minor bugs.
-**Bugfixes**
+**Bug fixes**
- Fix broken audio on at least GStreamer 0.10.30, which affects Ubuntu 10.10.
The GStreamer `appsrc` bin wasn't being linked due to lack of default caps.
@@ -165,7 +321,7 @@ loading from Mopidy 0.3.0 is still present.
**Important changes**
-- Mopidy now depends on `Pykka `_ >=0.12. If you
+- Mopidy now depends on `Pykka `_ >=0.12. If you
install from APT, Pykka will automatically be installed. If you are not
installing from APT, you may install Pykka from PyPI::
@@ -242,12 +398,12 @@ loading from Mopidy 0.3.0 is still present.
the debug log, to ease debugging of issues with attached debug logs.
-v0.3.1 (2010-01-22)
+v0.3.1 (2011-01-22)
===================
A couple of fixes to the 0.3.0 release is needed to get a smooth installation.
-**Bugfixes**
+**Bug fixes**
- The Spotify application key was missing from the Python package.
@@ -256,7 +412,7 @@ A couple of fixes to the 0.3.0 release is needed to get a smooth installation.
installed if the installation is executed as the root user.
-v0.3.0 (2010-01-22)
+v0.3.0 (2011-01-22)
===================
Mopidy 0.3.0 brings a bunch of small changes all over the place, but no large
@@ -416,7 +572,7 @@ v0.2.1 (2011-01-07)
This is a maintenance release without any new features.
-**Bugfixes**
+**Bug fixes**
- Fix crash in :mod:`mopidy.frontends.lastfm` which occurred at playback if
either :mod:`pylast` was not installed or the Last.fm scrobbling was not
@@ -746,7 +902,7 @@ As always, report problems at our IRC channel or our issue tracker. Thanks!
- Merged the ``gstreamer`` branch from Thomas Adamcik:
- - More than 200 new tests, and thus several bugfixes to existing code.
+ - More than 200 new tests, and thus several bug fixes to existing code.
- Several new generic features, like shuffle, consume, and playlist repeat.
(Fixes: :issue:`3`)
- **[Work in Progress]** A new backend for playing music from a local music
diff --git a/docs/clients/mpd.rst b/docs/clients/mpd.rst
index f5066210..4c789eba 100644
--- a/docs/clients/mpd.rst
+++ b/docs/clients/mpd.rst
@@ -20,9 +20,8 @@ A command line client. Version 0.14 had some issues with Mopidy (see
ncmpc
-----
-A console client. Uses the ``idle`` command heavily, which Mopidy doesn't
-support yet (see :issue:`32`). If you want a console client, use ncmpcpp
-instead.
+A console client. Works with Mopidy 0.6 and upwards. Uses the ``idle`` MPD
+command, but in a resource inefficient way.
ncmpcpp
@@ -48,15 +47,15 @@ from `Launchpad `_.
Communication mode
^^^^^^^^^^^^^^^^^^
-In newer versions of ncmpcpp, like 0.5.5 shipped with Ubuntu 11.04, ncmcpp
-defaults to "notifications" mode for MPD communications, which Mopidy currently
-does not support. To workaround this limitation in Mopidy, edit the ncmpcpp
-configuration file at ``~/.ncmpcpp/config`` and add the following setting::
+In newer versions of ncmpcpp, like ncmpcpp 0.5.5 shipped with Ubuntu 11.04,
+ncmcpp defaults to "notifications" mode for MPD communications, which Mopidy
+did not support before Mopidy 0.6. To workaround this limitation in earlier
+versions of Mopidy, edit the ncmpcpp configuration file at
+``~/.ncmpcpp/config`` and add the following setting::
mpd_communication_mode = "polling"
-You can track the development of "notifications" mode support in Mopidy in
-:issue:`32`.
+If you use Mopidy 0.6 or newer, you don't need to change anything.
Graphical clients
diff --git a/docs/conf.py b/docs/conf.py
index aeada340..a33a8f2d 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -11,7 +11,50 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
-import sys, os
+import os
+import re
+import sys
+
+class Mock(object):
+ def __init__(self, *args, **kwargs):
+ pass
+
+ def __call__(self, *args, **kwargs):
+ return Mock()
+
+ @classmethod
+ def __getattr__(self, name):
+ if name in ('__file__', '__path__'):
+ return '/dev/null'
+ elif name[0] == name[0].upper():
+ return type(name, (), {})
+ else:
+ return Mock()
+
+MOCK_MODULES = [
+ 'alsaaudio',
+ 'dbus',
+ 'dbus.mainloop',
+ 'dbus.mainloop.glib',
+ 'dbus.service',
+ 'glib',
+ 'gobject',
+ 'gst',
+ 'pygst',
+ 'pykka',
+ 'pykka.actor',
+ 'pykka.future',
+ 'pykka.registry',
+ 'pylast',
+ 'serial',
+]
+for mod_name in MOCK_MODULES:
+ sys.modules[mod_name] = Mock()
+
+def get_version():
+ init_py = open('../mopidy/__init__.py').read()
+ metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", init_py))
+ return metadata['version']
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
@@ -19,14 +62,15 @@ import sys, os
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/../'))
-import mopidy
+# When RTD builds the project, it sets the READTHEDOCS environment variable to
+# the string True.
+on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
# -- General configuration -----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.autodoc',
- 'sphinx.ext.graphviz', 'sphinx.ext.inheritance_diagram',
+extensions = ['sphinx.ext.autodoc', 'sphinx.ext.graphviz',
'sphinx.ext.extlinks', 'sphinx.ext.viewcode']
# Add any paths that contain templates here, relative to this directory.
@@ -43,14 +87,14 @@ master_doc = 'index'
# General information about the project.
project = u'Mopidy'
-copyright = u'2010-2011, Stein Magnus Jodal and contributors'
+copyright = u'2010-2012, Stein Magnus Jodal and contributors'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The full version, including alpha/beta/rc tags.
-release = mopidy.get_version()
+release = get_version()
# The short X.Y version.
version = '.'.join(release.split('.')[:2])
@@ -97,7 +141,7 @@ modindex_common_prefix = ['mopidy.']
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
-html_theme = 'nature'
+html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
@@ -116,7 +160,8 @@ html_theme_path = ['_themes']
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
-html_logo = '_static/mopidy.png'
+if on_rtd:
+ html_logo = '_static/mopidy.png'
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
@@ -130,7 +175,7 @@ html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
-html_last_updated_fmt = '%b %d, %Y'
+#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
@@ -202,4 +247,4 @@ latex_documents = [
needs_sphinx = '1.0'
-extlinks = {'issue': ('http://github.com/mopidy/mopidy/issues/%s', 'GH-')}
+extlinks = {'issue': ('https://github.com/mopidy/mopidy/issues/%s', 'GH-')}
diff --git a/docs/development/contributing.rst b/docs/development/contributing.rst
index 9ea3533f..782d2f20 100644
--- a/docs/development/contributing.rst
+++ b/docs/development/contributing.rst
@@ -74,7 +74,7 @@ Running tests
To run tests, you need a couple of dependencies. They can be installed through
Debian/Ubuntu package management::
- sudo aptitude install python-coverage python-mock python-nose
+ sudo apt-get install python-coverage python-mock python-nose
Or, they can be installed using ``pip``::
@@ -126,7 +126,7 @@ from the documentation files, you need some additional dependencies.
You can install them through Debian/Ubuntu package management::
- sudo aptitude install python-sphinx python-pygraphviz graphviz
+ sudo apt-get install python-sphinx python-pygraphviz graphviz
Then, to generate docs::
@@ -134,18 +134,8 @@ Then, to generate docs::
make # For help on available targets
make html # To generate HTML docs
-.. note::
-
- The documentation at http://www.mopidy.com/ is automatically updated when a
- documentation update is pushed to ``mopidy/mopidy`` at GitHub.
-
- Documentation generated from the ``master`` branch is published at
- http://www.mopidy.com/docs/master/, and will always be valid for the latest
- release.
-
- Documentation generated from the ``develop`` branch is published at
- http://www.mopidy.com/docs/develop/, and will always be valid for the
- latest development snapshot.
+The documentation at http://docs.mopidy.com/ is automatically updated when a
+documentation update is pushed to ``mopidy/mopidy`` at GitHub.
Creating releases
diff --git a/docs/index.rst b/docs/index.rst
index 769aed20..7e757de0 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -19,9 +19,7 @@ please create an issue in the `issue tracker
Project resources
=================
-- `Documentation for the latest release `_
-- `Documentation for the development version
- `_
+- `Documentation `_
- `Source code `_
- `Issue tracker `_
- IRC: ``#mopidy`` at `irc.freenode.net `_
diff --git a/docs/installation/gstreamer.rst b/docs/installation/gstreamer.rst
index 8f2ea07e..c6359f6f 100644
--- a/docs/installation/gstreamer.rst
+++ b/docs/installation/gstreamer.rst
@@ -2,19 +2,21 @@
GStreamer installation
**********************
-To use the Mopidy, you first need to install GStreamer and its Python bindings.
+To use the Mopidy, you first need to install GStreamer and the GStreamer Python
+bindings.
-Installing GStreamer
-====================
-
-On Linux
---------
+Installing GStreamer on Linux
+=============================
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.
+
+Debian/Ubuntu
+-------------
+
If you use Debian/Ubuntu you can install GStreamer like this::
sudo apt-get install python-gst0.10 gstreamer0.10-plugins-good \
@@ -24,30 +26,67 @@ If you install Mopidy from our APT archive, you don't need to install GStreamer
yourself. The Mopidy Debian package will handle it for you.
-On OS X from Homebrew
----------------------
+Arch Linux
+----------
+
+If you use Arch Linux, install the following packages from the official
+repository::
+
+ sudo pacman -S gstreamer0.10-python gstreamer0.10-good-plugins \
+ gstreamer0.10-ugly-plugins
+
+
+Installing GStreamer on OS X
+============================
.. note::
- We have created GStreamer formulas for Homebrew to make the GStreamer
- installation easy for you, but not all our formulas have been merged into
- Homebrew's master branch yet. You should either fetch the formula files
- from `Homebrew's issue #1612
- `_ yourself, or fall
- back to using MacPorts.
+ We have been working with `Homebrew `_ to
+ make all the GStreamer packages easily installable on OS X using Homebrew.
+ We've gotten most of our packages included, but the Homebrew guys aren't
+ very happy to include Python specific packages into Homebrew, even though
+ they are not installable by pip. If you're interested, see the discussion
+ in `Homebrew's issue #1612
+ `_ for details.
-To install GStreamer on OS X using Homebrew::
+The following is currently the shortest path to installing GStreamer with
+Python bindings on OS X using Homebrew.
- brew install gst-python gst-plugins-good gst-plugins-ugly
+#. Install `Homebrew `_.
+#. Download our Homebrew formulas for ``pycairo``, ``pygobject``, ``pygtk``,
+ and ``gst-python``::
-On OS X from MacPorts
----------------------
+ curl -o $(brew --prefix)/Library/Formula/pycairo.rb \
+ https://raw.github.com/jodal/homebrew/gst-python/Library/Formula/pycairo.rb
+ curl -o $(brew --prefix)/Library/Formula/pygobject.rb \
+ https://raw.github.com/jodal/homebrew/gst-python/Library/Formula/pygobject.rb
+ curl -o $(brew --prefix)/Library/Formula/pygtk.rb \
+ https://raw.github.com/jodal/homebrew/gst-python/Library/Formula/pygtk.rb
+ curl -o $(brew --prefix)/Library/Formula/gst-python.rb \
+ https://raw.github.com/jodal/homebrew/gst-python/Library/Formula/gst-python.rb
-To install GStreamer on OS X using MacPorts::
+#. Install the required packages::
- sudo port install py26-gst-python gstreamer-plugins-good \
- gstreamer-plugins-ugly
+ brew install gst-python gst-plugins-good gst-plugins-ugly
+
+#. Make sure to include Homebrew's Python ``site-packages`` directory in your
+ ``PYTHONPATH``. If you don't include this, Mopidy will not find GStreamer
+ and crash.
+
+ You can either amend your ``PYTHONPATH`` permanently, by adding the
+ following statement to your shell's init file, e.g. ``~/.bashrc``::
+
+ export PYTHONPATH=$(brew --prefix)/lib/python2.6/site-packages:$PYTHONPATH
+
+ Or, you can prefix the Mopidy command every time you run it::
+
+ PYTHONPATH=$(brew --prefix)/lib/python2.6/site-packages mopidy
+
+ Note that you need to replace ``python2.6`` with ``python2.7`` if that's
+ the Python version you are using. To find your Python version, run::
+
+ python --version
Testing the installation
diff --git a/docs/installation/index.rst b/docs/installation/index.rst
index 198ac9e8..fae50a1b 100644
--- a/docs/installation/index.rst
+++ b/docs/installation/index.rst
@@ -18,33 +18,36 @@ Requirements
gstreamer
libspotify
-If you install Mopidy from the APT archive, as described below, you can skip
-the dependency installation part.
+If you install Mopidy from the APT archive, as described below, APT will take
+care of all the dependencies for you. Otherwise, make sure you got the required
+dependencies installed.
-Otherwise, make sure you got the required dependencies installed.
+- Hard dependencies:
-- Python >= 2.6, < 3
+ - Python >= 2.6, < 3
-- `Pykka `_ >= 0.12.3
+ - Pykka >= 0.12.3::
-- GStreamer >= 0.10, with Python bindings. See :doc:`gstreamer`.
+ sudo pip install -U pykka
-- Mixer dependencies: The default mixer does not require any additional
- dependencies. If you use another mixer, see the mixer's docs for any
- additional requirements.
-
-- Dependencies for at least one Mopidy backend:
-
- - The default backend, :mod:`mopidy.backends.spotify`, requires libspotify
- and pyspotify. See :doc:`libspotify`.
-
- - The local backend, :mod:`mopidy.backends.local`, requires no additional
- dependencies.
+ - GStreamer 0.10.x, with Python bindings. See :doc:`gstreamer`.
- Optional dependencies:
- - To use the Last.FM scrobbler, see :mod:`mopidy.frontends.lastfm` for
- additional requirements.
+ - For Spotify support, you need libspotify and pyspotify. See
+ :doc:`libspotify`.
+
+ - To scrobble your played tracks to Last.fm, you need pylast::
+
+ sudo pip install -U pylast
+
+ - To use MPRIS, e.g. for controlling Mopidy from the Ubuntu Sound Menu, you
+ need some additional requirements::
+
+ sudo apt-get install python-dbus python-indicate
+
+ - Some custom mixers (but not the default one) require additional
+ dependencies. See the docs for each mixer.
Install latest stable release
@@ -97,8 +100,8 @@ install Mopidy from PyPI using Pip.
#. Then, you need to install Pip::
- sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian
- sudo easy_install pip # On OS X
+ sudo apt-get install python-setuptools python-pip # On Ubuntu/Debian
+ sudo easy_install pip # On OS X
#. To install the currently latest stable release of Mopidy::
@@ -109,8 +112,6 @@ install Mopidy from PyPI using Pip.
#. Next, you need to set a couple of :doc:`settings `, and then
you're ready to :doc:`run Mopidy `.
-If you for some reason can't use Pip, try ``easy_install`` instead.
-
Install development version
===========================
@@ -131,8 +132,8 @@ Mopidy's ``develop`` branch.
#. Then, you need to install Pip::
- sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian
- sudo easy_install pip # On OS X
+ sudo apt-get install python-setuptools python-pip # On Ubuntu/Debian
+ sudo easy_install pip # On OS X
#. To install the latest snapshot of Mopidy, run::
@@ -154,7 +155,7 @@ If you want to contribute to Mopidy, you should install Mopidy using Git.
#. Then install Git, if haven't already::
- sudo aptitude install git-core # On Ubuntu/Debian
+ sudo apt-get install git-core # On Ubuntu/Debian
sudo brew install git # On OS X using Homebrew
#. Clone the official Mopidy repository, or your own fork of it::
diff --git a/docs/installation/libspotify.rst b/docs/installation/libspotify.rst
index 2728be94..223e4ed7 100644
--- a/docs/installation/libspotify.rst
+++ b/docs/installation/libspotify.rst
@@ -12,12 +12,6 @@ install libspotify and `pyspotify `_.
This backend requires a paid `Spotify premium account
`_.
-.. note::
-
- This product uses SPOTIFY CORE but is not endorsed, certified or otherwise
- approved in any way by Spotify. Spotify is the registered trade mark of the
- Spotify Group.
-
Installing libspotify
=====================
@@ -26,23 +20,20 @@ Installing libspotify
On Linux from APT archive
-------------------------
-If you run a Debian based Linux distribution, like Ubuntu, see
-http://apt.mopidy.com/ for how to the Mopidy APT archive as a software source
-on your installation. Then, simply run::
-
- sudo apt-get install libspotify8
-
-When libspotify has been installed, continue with
-:ref:`pyspotify_installation`.
+If you install from APT, jump directly to :ref:`pyspotify_installation` below.
On Linux from source
--------------------
-Download and install libspotify 0.0.8 for your OS and CPU architecture from
-https://developer.spotify.com/en/libspotify/.
+First, check pyspotify's changelog to see what's the latest version of
+libspotify which is supported. The versions of libspotify and pyspotify are
+tightly coupled.
-For 64-bit Linux the process is as follows::
+Download and install the appropriate version of libspotify for your OS and CPU
+architecture from https://developer.spotify.com/en/libspotify/.
+
+For libspotify 0.0.8 for 64-bit Linux the process is as follows::
wget http://developer.spotify.com/download/libspotify/libspotify-0.0.8-linux6-x86_64.tar.gz
tar zxfv libspotify-0.0.8-linux6-x86_64.tar.gz
@@ -50,6 +41,9 @@ For 64-bit Linux the process is as follows::
sudo make install prefix=/usr/local
sudo ldconfig
+Remember to adjust for the latest libspotify version supported by pyspotify,
+your OS and your CPU architecture.
+
When libspotify has been installed, continue with
:ref:`pyspotify_installation`.
@@ -66,7 +60,7 @@ libspotify::
To update your existing libspotify installation using Homebrew::
brew update
- brew install `brew outdated`
+ brew upgrade
When libspotify has been installed, continue with
:ref:`pyspotify_installation`.
@@ -84,29 +78,35 @@ by installing pyspotify.
On Linux from APT archive
-------------------------
-Assuming that you've already set up http://apt.mopidy.com/ as a software
-source, run::
+If you run a Debian based Linux distribution, like Ubuntu, see
+http://apt.mopidy.com/ for how to use the Mopidy APT archive as a software
+source on your system. Then, simply run::
sudo apt-get install python-spotify
-If you haven't already installed libspotify, this command will install both
-libspotify and pyspotify for you.
+This command will install both libspotify and pyspotify for you.
-On Linux/OS X from source
+On Linux from source
-------------------------
+If you have have already installed libspotify, you can continue with installing
+the libspotify Python bindings, called pyspotify.
+
On Linux, you need to get the Python development files installed. On
Debian/Ubuntu systems run::
sudo apt-get install python-dev
-On OS X no additional dependencies are needed.
-
Then get, build, and install the latest releast of pyspotify using ``pip``::
sudo pip install -U pyspotify
-Or using the older ``easy_install``::
- sudo easy_install pyspotify
+On OS X from source
+-------------------
+
+If you have already installed libspotify, you can get, build, and install the
+latest releast of pyspotify using ``pip``::
+
+ sudo pip install -U pyspotify
diff --git a/docs/licenses.rst b/docs/licenses.rst
index 7f4ed0ce..11e0a906 100644
--- a/docs/licenses.rst
+++ b/docs/licenses.rst
@@ -8,7 +8,7 @@ contributed what, please refer to our git repository.
Source code license
===================
-Copyright 2009-2011 Stein Magnus Jodal and contributors
+Copyright 2009-2012 Stein Magnus Jodal and contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -26,7 +26,7 @@ limitations under the License.
Documentation license
=====================
-Copyright 2010-2011 Stein Magnus Jodal and contributors
+Copyright 2010-2012 Stein Magnus Jodal and contributors
This work is licensed under the Creative Commons Attribution-ShareAlike 3.0
Unported License. To view a copy of this license, visit
diff --git a/docs/modules/frontends/mpd.rst b/docs/modules/frontends/mpd.rst
index 6f69b2a9..0ce138a2 100644
--- a/docs/modules/frontends/mpd.rst
+++ b/docs/modules/frontends/mpd.rst
@@ -2,38 +2,14 @@
:mod:`mopidy.frontends.mpd` -- MPD server
*****************************************
-.. inheritance-diagram:: mopidy.frontends.mpd
-
.. automodule:: mopidy.frontends.mpd
:synopsis: MPD server frontend
:members:
-MPD server
-==========
-
-.. inheritance-diagram:: mopidy.frontends.mpd.server
-
-.. automodule:: mopidy.frontends.mpd.server
- :synopsis: MPD server
- :members:
-
-
-MPD session
-===========
-
-.. inheritance-diagram:: mopidy.frontends.mpd.session
-
-.. automodule:: mopidy.frontends.mpd.session
- :synopsis: MPD client session
- :members:
-
-
MPD dispatcher
==============
-.. inheritance-diagram:: mopidy.frontends.mpd.dispatcher
-
.. automodule:: mopidy.frontends.mpd.dispatcher
:synopsis: MPD request dispatcher
:members:
diff --git a/docs/modules/frontends/mpris.rst b/docs/modules/frontends/mpris.rst
new file mode 100644
index 00000000..05a6e287
--- /dev/null
+++ b/docs/modules/frontends/mpris.rst
@@ -0,0 +1,7 @@
+***********************************************
+:mod:`mopidy.frontends.mpris` -- MPRIS frontend
+***********************************************
+
+.. automodule:: mopidy.frontends.mpris
+ :synopsis: MPRIS frontend
+ :members:
diff --git a/docs/modules/gstreamer.rst b/docs/modules/gstreamer.rst
index adbf5fda..205b0a3e 100644
--- a/docs/modules/gstreamer.rst
+++ b/docs/modules/gstreamer.rst
@@ -2,8 +2,6 @@
:mod:`mopidy.gstreamer` -- GStreamer adapter
********************************************
-.. inheritance-diagram:: mopidy.gstreamer
-
.. automodule:: mopidy.gstreamer
:synopsis: GStreamer adapter
:members:
diff --git a/docs/modules/mixers/alsa.rst b/docs/modules/mixers/alsa.rst
index 05f429eb..e8b7ed6c 100644
--- a/docs/modules/mixers/alsa.rst
+++ b/docs/modules/mixers/alsa.rst
@@ -2,8 +2,6 @@
:mod:`mopidy.mixers.alsa` -- ALSA mixer for Linux
*************************************************
-.. inheritance-diagram:: mopidy.mixers.alsa
-
.. automodule:: mopidy.mixers.alsa
:synopsis: ALSA mixer for Linux
:members:
diff --git a/docs/modules/mixers/denon.rst b/docs/modules/mixers/denon.rst
index ac944ccc..7fb2d6cc 100644
--- a/docs/modules/mixers/denon.rst
+++ b/docs/modules/mixers/denon.rst
@@ -2,8 +2,6 @@
:mod:`mopidy.mixers.denon` -- Hardware mixer for Denon amplifiers
*****************************************************************
-.. inheritance-diagram:: mopidy.mixers.denon
-
.. automodule:: mopidy.mixers.denon
:synopsis: Hardware mixer for Denon amplifiers
:members:
diff --git a/docs/modules/mixers/dummy.rst b/docs/modules/mixers/dummy.rst
index 6665f949..8ac18e10 100644
--- a/docs/modules/mixers/dummy.rst
+++ b/docs/modules/mixers/dummy.rst
@@ -2,8 +2,6 @@
:mod:`mopidy.mixers.dummy` -- Dummy mixer for testing
*****************************************************
-.. inheritance-diagram:: mopidy.mixers.dummy
-
.. automodule:: mopidy.mixers.dummy
:synopsis: Dummy mixer for testing
:members:
diff --git a/docs/modules/mixers/gstreamer_software.rst b/docs/modules/mixers/gstreamer_software.rst
index ef8cc310..98e09f44 100644
--- a/docs/modules/mixers/gstreamer_software.rst
+++ b/docs/modules/mixers/gstreamer_software.rst
@@ -2,8 +2,6 @@
:mod:`mopidy.mixers.gstreamer_software` -- Software mixer for all platforms
***************************************************************************
-.. inheritance-diagram:: mopidy.mixers.gstreamer_software
-
.. automodule:: mopidy.mixers.gstreamer_software
:synopsis: Software mixer for all platforms
:members:
diff --git a/docs/modules/mixers/nad.rst b/docs/modules/mixers/nad.rst
index d441b3fd..56291cbb 100644
--- a/docs/modules/mixers/nad.rst
+++ b/docs/modules/mixers/nad.rst
@@ -2,8 +2,6 @@
:mod:`mopidy.mixers.nad` -- Hardware mixer for NAD amplifiers
*************************************************************
-.. inheritance-diagram:: mopidy.mixers.nad
-
.. automodule:: mopidy.mixers.nad
:synopsis: Hardware mixer for NAD amplifiers
:members:
diff --git a/docs/modules/mixers/osa.rst b/docs/modules/mixers/osa.rst
index 14bf9a49..a4363cb4 100644
--- a/docs/modules/mixers/osa.rst
+++ b/docs/modules/mixers/osa.rst
@@ -2,8 +2,6 @@
:mod:`mopidy.mixers.osa` -- Osa mixer for OS X
**********************************************
-.. inheritance-diagram:: mopidy.mixers.osa
-
.. automodule:: mopidy.mixers.osa
:synopsis: Osa mixer for OS X
:members:
diff --git a/docs/modules/outputs.rst b/docs/modules/outputs.rst
index 87d23dab..f80c16e3 100644
--- a/docs/modules/outputs.rst
+++ b/docs/modules/outputs.rst
@@ -4,11 +4,8 @@
The following GStreamer audio outputs implements the :ref:`output-api`.
-.. inheritance-diagram:: mopidy.outputs.custom
.. autoclass:: mopidy.outputs.custom.CustomOutput
-.. inheritance-diagram:: mopidy.outputs.local
.. autoclass:: mopidy.outputs.local.LocalOutput
-.. inheritance-diagram:: mopidy.outputs.shoutcast
.. autoclass:: mopidy.outputs.shoutcast.ShoutcastOutput
diff --git a/docs/running.rst b/docs/running.rst
index 4912512f..6c8d0ede 100644
--- a/docs/running.rst
+++ b/docs/running.rst
@@ -10,4 +10,11 @@ When Mopidy says ``MPD server running at [127.0.0.1]:6600`` it's ready to
accept connections by any MPD client. Check out our non-exhaustive
:doc:`/clients/mpd` list to find recommended clients.
-To stop Mopidy, press ``CTRL+C``.
+To stop Mopidy, press ``CTRL+C`` in the terminal where you started Mopidy.
+
+Mopidy will also shut down properly if you send it the TERM signal, e.g. by
+using ``kill``::
+
+ kill `ps ax | grep mopidy | grep -v grep | cut -d' ' -f1`
+
+This can be useful e.g. if you create init script for managing Mopidy.
diff --git a/docs/settings.rst b/docs/settings.rst
index d3c9015e..980fcd4c 100644
--- a/docs/settings.rst
+++ b/docs/settings.rst
@@ -10,10 +10,10 @@ changes you may want to do, and a complete listing of available settings.
Changing settings
=================
-Mopidy reads settings from the file ``~/.mopidy/settings.py``, where ``~``
-means your *home directory*. If your username is ``alice`` and you are running
-Linux, the settings file should probably be at
-``/home/alice/.mopidy/settings.py``.
+Mopidy reads settings from the file ``~/.config/mopidy/settings.py``, where
+``~`` means your *home directory*. If your username is ``alice`` and you are
+running Linux, the settings file should probably be at
+``/home/alice/.config/mopidy/settings.py``.
You can either create the settings file yourself, or run the ``mopidy``
command, and it will create an empty settings file for you.
@@ -22,7 +22,7 @@ When you have created the settings file, open it in a text editor, and add
settings you want to change. If you want to keep the default value for setting,
you should *not* redefine it in your own settings file.
-A complete ``~/.mopidy/settings.py`` may look as simple as this::
+A complete ``~/.config/mopidy/settings.py`` may look as simple as this::
MPD_SERVER_HOSTNAME = u'::'
SPOTIFY_USERNAME = u'alice'
@@ -77,7 +77,7 @@ To make a ``tag_cache`` of your local music available for Mopidy:
mopidy --list-settings
-#. Scan your music library. Currently the command outputs the ``tag_cache`` to
+#. Scan your music library. The command outputs the ``tag_cache`` to
``stdout``, which means that you will need to redirect the output to a file
yourself::
@@ -92,7 +92,6 @@ To make a ``tag_cache`` of your local music available for Mopidy:
.. _use_mpd_on_a_network:
-
Connecting from other machines on the network
=============================================
@@ -120,6 +119,33 @@ file::
LASTFM_PASSWORD = u'mysecret'
+.. _install_desktop_file:
+
+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 `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 into the
+``/usr/share/applications`` dir by hand::
+
+ cd /path/to/mopidy/source
+ sudo cp data/mopidy.desktop /usr/share/applications/
+
+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,
+:mod:`mopidy.frontends.mpris`. The MPRIS frontend supports the minimum
+requirements of the `MPRIS specification `_. The
+``TrackList`` and the ``Playlists`` interfaces of the spec are not supported.
+
+
Streaming audio through a SHOUTcast/Icecast server
==================================================
diff --git a/mopidy/__init__.py b/mopidy/__init__.py
index 7b25c525..11293446 100644
--- a/mopidy/__init__.py
+++ b/mopidy/__init__.py
@@ -1,17 +1,25 @@
-import platform
import sys
if not (2, 6) <= sys.version_info < (3,):
sys.exit(u'Mopidy requires Python >= 2.6, < 3')
+import os
+import platform
from subprocess import PIPE, Popen
-VERSION = (0, 6, 0)
+import glib
+
+__version__ = '0.7.3'
+
+DATA_PATH = os.path.join(str(glib.get_user_data_dir()), 'mopidy')
+CACHE_PATH = os.path.join(str(glib.get_user_cache_dir()), 'mopidy')
+SETTINGS_PATH = os.path.join(str(glib.get_user_config_dir()), 'mopidy')
+SETTINGS_FILE = os.path.join(SETTINGS_PATH, 'settings.py')
def get_version():
try:
return get_git_version()
except EnvironmentError:
- return get_plain_version()
+ return __version__
def get_git_version():
process = Popen(['git', 'describe'], stdout=PIPE, stderr=PIPE)
@@ -22,9 +30,6 @@ def get_git_version():
version = version[1:]
return version
-def get_plain_version():
- return '.'.join(map(str, VERSION))
-
def get_platform():
return platform.platform()
diff --git a/mopidy/backends/base/current_playlist.py b/mopidy/backends/base/current_playlist.py
index 2633f166..d7e6c331 100644
--- a/mopidy/backends/base/current_playlist.py
+++ b/mopidy/backends/base/current_playlist.py
@@ -2,6 +2,7 @@ from copy import copy
import logging
import random
+from mopidy.listeners import BackendListener
from mopidy.models import CpTrack
logger = logging.getLogger('mopidy.backends.base')
@@ -16,13 +17,10 @@ class CurrentPlaylistController(object):
def __init__(self, backend):
self.backend = backend
+ self.cp_id = 0
self._cp_tracks = []
self._version = 0
- def destroy(self):
- """Cleanup after component."""
- pass
-
@property
def cp_tracks(self):
"""
@@ -30,7 +28,7 @@ class CurrentPlaylistController(object):
Read-only.
"""
- return [copy(ct) for ct in self._cp_tracks]
+ return [copy(cp_track) for cp_track in self._cp_tracks]
@property
def tracks(self):
@@ -39,7 +37,14 @@ class CurrentPlaylistController(object):
Read-only.
"""
- return [ct[1] for ct in self._cp_tracks]
+ return [cp_track.track for cp_track in self._cp_tracks]
+
+ @property
+ def length(self):
+ """
+ Length of the current playlist.
+ """
+ return len(self._cp_tracks)
@property
def version(self):
@@ -53,8 +58,9 @@ class CurrentPlaylistController(object):
def version(self, version):
self._version = version
self.backend.playback.on_current_playlist_change()
+ self._trigger_playlist_changed()
- def add(self, track, at_position=None):
+ def add(self, track, at_position=None, increase_version=True):
"""
Add the track to the end of, or at the given position in the current
playlist.
@@ -68,12 +74,14 @@ class CurrentPlaylistController(object):
"""
assert at_position <= len(self._cp_tracks), \
u'at_position can not be greater than playlist length'
- cp_track = CpTrack(self.version, track)
+ cp_track = CpTrack(self.cp_id, track)
if at_position is not None:
self._cp_tracks.insert(at_position, cp_track)
else:
self._cp_tracks.append(cp_track)
- self.version += 1
+ if increase_version:
+ self.version += 1
+ self.cp_id += 1
return cp_track
def append(self, tracks):
@@ -84,7 +92,10 @@ class CurrentPlaylistController(object):
:type tracks: list of :class:`mopidy.models.Track`
"""
for track in tracks:
- self.add(track)
+ self.add(track, increase_version=False)
+
+ if tracks:
+ self.version += 1
def clear(self):
"""Clear the current playlist."""
@@ -112,9 +123,9 @@ class CurrentPlaylistController(object):
matches = self._cp_tracks
for (key, value) in criteria.iteritems():
if key == 'cpid':
- matches = filter(lambda ct: ct[0] == value, matches)
+ matches = filter(lambda ct: ct.cpid == value, matches)
else:
- matches = filter(lambda ct: getattr(ct[1], key) == value,
+ matches = filter(lambda ct: getattr(ct.track, key) == value,
matches)
if len(matches) == 1:
return matches[0]
@@ -125,6 +136,19 @@ class CurrentPlaylistController(object):
else:
raise LookupError(u'"%s" match multiple tracks' % criteria_string)
+ def index(self, cp_track):
+ """
+ Get index of the given (CPID integer, :class:`mopidy.models.Track`)
+ two-tuple in the current playlist.
+
+ Raises :exc:`ValueError` if not found.
+
+ :param cp_track: track to find the index of
+ :type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`)
+ :rtype: int
+ """
+ return self._cp_tracks.index(cp_track)
+
def move(self, start, end, to_position):
"""
Move the tracks in the slice ``[start:end]`` to ``to_position``.
@@ -164,7 +188,6 @@ class CurrentPlaylistController(object):
:param criteria: on or more criteria to match by
:type criteria: dict
- :type track: :class:`mopidy.models.Track`
"""
cp_track = self.get(**criteria)
position = self._cp_tracks.index(cp_track)
@@ -199,3 +222,20 @@ class CurrentPlaylistController(object):
random.shuffle(shuffled)
self._cp_tracks = before + shuffled + after
self.version += 1
+
+ def slice(self, start, end):
+ """
+ Returns a slice of the current playlist, limited by the given
+ start and end positions.
+
+ :param start: position of first track to include in slice
+ :type start: int
+ :param end: position after last track to include in slice
+ :type end: int
+ :rtype: two-tuple of (CPID integer, :class:`mopidy.models.Track`)
+ """
+ return [copy(cp_track) for cp_track in self._cp_tracks[start:end]]
+
+ def _trigger_playlist_changed(self):
+ logger.debug(u'Triggering playlist changed event')
+ BackendListener.send('playlist_changed')
diff --git a/mopidy/backends/base/library.py b/mopidy/backends/base/library.py
index a30ed412..9e3afe9a 100644
--- a/mopidy/backends/base/library.py
+++ b/mopidy/backends/base/library.py
@@ -16,10 +16,6 @@ class LibraryController(object):
self.backend = backend
self.provider = provider
- def destroy(self):
- """Cleanup after component."""
- self.provider.destroy()
-
def find_exact(self, **query):
"""
Search the library for tracks where ``field`` is ``values``.
@@ -89,14 +85,6 @@ class BaseLibraryProvider(object):
def __init__(self, backend):
self.backend = backend
- def destroy(self):
- """
- Cleanup after component.
-
- *MAY be implemented by subclasses.*
- """
- pass
-
def find_exact(self, **query):
"""
See :meth:`mopidy.backends.base.LibraryController.find_exact`.
diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py
index 088a5ad4..16ac75d1 100644
--- a/mopidy/backends/base/playback.py
+++ b/mopidy/backends/base/playback.py
@@ -2,12 +2,21 @@ import logging
import random
import time
-from pykka.registry import ActorRegistry
-
from mopidy.listeners import BackendListener
logger = logging.getLogger('mopidy.backends.base')
+
+def option_wrapper(name, default):
+ def get_option(self):
+ return getattr(self, name, default)
+ def set_option(self, value):
+ if getattr(self, name, default) != value:
+ self._trigger_options_changed()
+ return setattr(self, name, value)
+ return property(get_option, set_option)
+
+
class PlaybackController(object):
"""
:param backend: the backend
@@ -34,7 +43,7 @@ class PlaybackController(object):
#: Tracks are removed from the playlist when they have been played.
#: :class:`False`
#: Tracks are not removed from the playlist.
- consume = False
+ consume = option_wrapper('_consume', False)
#: The currently playing or selected track.
#:
@@ -46,21 +55,21 @@ class PlaybackController(object):
#: Tracks are selected at random from the playlist.
#: :class:`False`
#: Tracks are played in the order of the playlist.
- random = False
+ random = option_wrapper('_random', False)
#: :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.
- repeat = False
+ repeat = option_wrapper('_repeat', False)
#: :class:`True`
#: Playback is stopped after current song, unless in :attr:`repeat`
#: mode.
#: :class:`False`
#: Playback continues after current song.
- single = False
+ single = option_wrapper('_single', False)
def __init__(self, backend, provider):
self.backend = backend
@@ -71,12 +80,6 @@ class PlaybackController(object):
self.play_time_accumulated = 0
self.play_time_started = None
- def destroy(self):
- """
- Cleanup after component.
- """
- self.provider.destroy()
-
def _get_cpid(self, cp_track):
if cp_track is None:
return None
@@ -276,6 +279,9 @@ class PlaybackController(object):
def state(self, new_state):
(old_state, self._state) = (self.state, new_state)
logger.debug(u'Changing state: %s -> %s', old_state, new_state)
+
+ self._trigger_playback_state_changed()
+
# FIXME play_time stuff assumes backend does not have a better way of
# handeling this stuff :/
if (old_state in (self.PLAYING, self.STOPPED)
@@ -313,6 +319,26 @@ class PlaybackController(object):
def _current_wall_time(self):
return int(time.time() * 1000)
+ def change_track(self, cp_track, on_error_step=1):
+ """
+ Change to the given track, keeping the current playback state.
+
+ :param cp_track: track to change to
+ :type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`)
+ or :class:`None`
+ :param on_error_step: direction to step at play error, 1 for next
+ track (default), -1 for previous track
+ :type on_error_step: int, -1 or 1
+
+ """
+ old_state = self.state
+ self.stop()
+ self.current_cp_track = cp_track
+ if old_state == self.PLAYING:
+ self.play(on_error_step=on_error_step)
+ elif old_state == self.PAUSED:
+ self.pause()
+
def on_end_of_track(self):
"""
Tell the playback controller that end of track is reached.
@@ -326,7 +352,7 @@ class PlaybackController(object):
original_cp_track = self.current_cp_track
if self.cp_track_at_eot:
- self._trigger_stopped_playing_event()
+ self._trigger_track_playback_ended()
self.play(self.cp_track_at_eot)
else:
self.stop(clear_current_track=True)
@@ -349,20 +375,23 @@ class PlaybackController(object):
self.stop(clear_current_track=True)
def next(self):
- """Play the next track."""
- if self.state == self.STOPPED:
- return
+ """
+ Change to the next track.
+ 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.cp_track_at_next:
- self._trigger_stopped_playing_event()
- self.play(self.cp_track_at_next)
+ self._trigger_track_playback_ended()
+ self.change_track(self.cp_track_at_next)
else:
self.stop(clear_current_track=True)
def pause(self):
"""Pause playback."""
- if self.state == self.PLAYING and self.provider.pause():
+ if self.provider.pause():
self.state = self.PAUSED
+ self._trigger_track_playback_paused()
def play(self, cp_track=None, on_error_step=1):
"""
@@ -379,15 +408,17 @@ class PlaybackController(object):
if cp_track is not None:
assert cp_track in self.backend.current_playlist.cp_tracks
-
- if cp_track is None and self.current_cp_track is None:
- cp_track = self.cp_track_at_next
-
- if cp_track is None and self.state == self.PAUSED:
- self.resume()
+ elif cp_track is None:
+ if self.state == self.PAUSED:
+ return self.resume()
+ elif self.current_cp_track is not None:
+ cp_track = self.current_cp_track
+ elif self.current_cp_track is None and on_error_step == 1:
+ cp_track = self.cp_track_at_next
+ elif self.current_cp_track is None and on_error_step == -1:
+ cp_track = self.cp_track_at_previous
if cp_track is not None:
- self.state = self.STOPPED
self.current_cp_track = cp_track
self.state = self.PLAYING
if not self.provider.play(cp_track.track):
@@ -402,21 +433,23 @@ class PlaybackController(object):
if self.random and self.current_cp_track in self._shuffled:
self._shuffled.remove(self.current_cp_track)
- self._trigger_started_playing_event()
+ self._trigger_track_playback_started()
def previous(self):
- """Play the previous track."""
- if self.cp_track_at_previous is None:
- return
- if self.state == self.STOPPED:
- return
- self._trigger_stopped_playing_event()
- self.play(self.cp_track_at_previous, on_error_step=-1)
+ """
+ Change to the previous track.
+
+ The current playback state will be kept. If it was playing, playing
+ will continue. If it was paused, it will still be paused, etc.
+ """
+ self._trigger_track_playback_ended()
+ self.change_track(self.cp_track_at_previous, on_error_step=-1)
def resume(self):
"""If paused, resume playing the current track."""
if self.state == self.PAUSED and self.provider.resume():
self.state = self.PLAYING
+ self._trigger_track_playback_resumed()
def seek(self, time_position):
"""
@@ -443,7 +476,10 @@ class PlaybackController(object):
self.play_time_started = self._current_wall_time
self.play_time_accumulated = time_position
- return self.provider.seek(time_position)
+ success = self.provider.seek(time_position)
+ if success:
+ self._trigger_seeked()
+ return success
def stop(self, clear_current_track=False):
"""
@@ -454,37 +490,54 @@ class PlaybackController(object):
:type clear_current_track: boolean
"""
if self.state != self.STOPPED:
- self._trigger_stopped_playing_event()
if self.provider.stop():
+ self._trigger_track_playback_ended()
self.state = self.STOPPED
if clear_current_track:
self.current_cp_track = None
- def _trigger_started_playing_event(self):
- logger.debug(u'Triggering started playing event')
+ def _trigger_track_playback_paused(self):
+ logger.debug(u'Triggering track playback paused event')
if self.current_track is None:
return
- ActorRegistry.broadcast({
- 'command': 'pykka_call',
- 'attr_path': ('started_playing',),
- 'args': [],
- 'kwargs': {'track': self.current_track},
- }, target_class=BackendListener)
+ BackendListener.send('track_playback_paused',
+ track=self.current_track,
+ time_position=self.time_position)
- def _trigger_stopped_playing_event(self):
- # TODO Test that this is called on next/prev/end-of-track
- logger.debug(u'Triggering stopped playing event')
+ def _trigger_track_playback_resumed(self):
+ logger.debug(u'Triggering track playback resumed event')
if self.current_track is None:
return
- ActorRegistry.broadcast({
- 'command': 'pykka_call',
- 'attr_path': ('stopped_playing',),
- 'args': [],
- 'kwargs': {
- 'track': self.current_track,
- 'time_position': self.time_position,
- },
- }, target_class=BackendListener)
+ BackendListener.send('track_playback_resumed',
+ track=self.current_track,
+ time_position=self.time_position)
+
+ def _trigger_track_playback_started(self):
+ logger.debug(u'Triggering track playback started event')
+ if self.current_track is None:
+ return
+ BackendListener.send('track_playback_started',
+ track=self.current_track)
+
+ def _trigger_track_playback_ended(self):
+ logger.debug(u'Triggering track playback ended event')
+ if self.current_track is None:
+ return
+ BackendListener.send('track_playback_ended',
+ track=self.current_track,
+ time_position=self.time_position)
+
+ def _trigger_playback_state_changed(self):
+ logger.debug(u'Triggering playback state change event')
+ BackendListener.send('playback_state_changed')
+
+ def _trigger_options_changed(self):
+ logger.debug(u'Triggering options changed event')
+ BackendListener.send('options_changed')
+
+ def _trigger_seeked(self):
+ logger.debug(u'Triggering seeked event')
+ BackendListener.send('seeked')
class BasePlaybackProvider(object):
@@ -498,14 +551,6 @@ class BasePlaybackProvider(object):
def __init__(self, backend):
self.backend = backend
- def destroy(self):
- """
- Cleanup after component.
-
- *MAY be implemented by subclasses.*
- """
- pass
-
def pause(self):
"""
Pause playback.
diff --git a/mopidy/backends/base/stored_playlists.py b/mopidy/backends/base/stored_playlists.py
index aca78a8c..0ce2e196 100644
--- a/mopidy/backends/base/stored_playlists.py
+++ b/mopidy/backends/base/stored_playlists.py
@@ -17,10 +17,6 @@ class StoredPlaylistsController(object):
self.backend = backend
self.provider = provider
- def destroy(self):
- """Cleanup after component."""
- self.provider.destroy()
-
@property
def playlists(self):
"""
@@ -133,14 +129,6 @@ class BaseStoredPlaylistsProvider(object):
self.backend = backend
self._playlists = []
- def destroy(self):
- """
- Cleanup after component.
-
- *MAY be implemented by subclass.*
- """
- pass
-
@property
def playlists(self):
"""
@@ -201,4 +189,3 @@ class BaseStoredPlaylistsProvider(object):
*MUST be implemented by subclass.*
"""
raise NotImplementedError
-
diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py
index af80a8eb..e8638a3a 100644
--- a/mopidy/backends/local/__init__.py
+++ b/mopidy/backends/local/__init__.py
@@ -1,4 +1,5 @@
import glob
+import glib
import logging
import os
import shutil
@@ -6,7 +7,7 @@ import shutil
from pykka.actor import ThreadingActor
from pykka.registry import ActorRegistry
-from mopidy import settings
+from mopidy import settings, DATA_PATH
from mopidy.backends.base import (Backend, CurrentPlaylistController,
LibraryController, BaseLibraryProvider, PlaybackController,
BasePlaybackProvider, StoredPlaylistsController,
@@ -18,6 +19,14 @@ from .translator import parse_m3u, parse_mpd_tag_cache
logger = logging.getLogger(u'mopidy.backends.local')
+DEFAULT_PLAYLIST_PATH = os.path.join(DATA_PATH, 'playlists')
+DEFAULT_TAG_CACHE_FILE = os.path.join(DATA_PATH, 'tag_cache')
+DEFAULT_MUSIC_PATH = str(glib.get_user_special_dir(glib.USER_DIRECTORY_MUSIC))
+
+if not DEFAULT_MUSIC_PATH or DEFAULT_MUSIC_PATH == os.path.expanduser(u'~'):
+ DEFAULT_MUSIC_PATH = os.path.expanduser(u'~/music')
+
+
class LocalBackend(ThreadingActor, Backend):
"""
A backend for playing music from a local music archive.
@@ -58,7 +67,8 @@ class LocalBackend(ThreadingActor, Backend):
def on_start(self):
gstreamer_refs = ActorRegistry.get_by_class(GStreamer)
- assert len(gstreamer_refs) == 1, 'Expected exactly one running gstreamer.'
+ assert len(gstreamer_refs) == 1, \
+ 'Expected exactly one running GStreamer.'
self.gstreamer = gstreamer_refs[0].proxy()
@@ -96,7 +106,7 @@ class LocalPlaybackProvider(BasePlaybackProvider):
class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider):
def __init__(self, *args, **kwargs):
super(LocalStoredPlaylistsProvider, self).__init__(*args, **kwargs)
- self._folder = settings.LOCAL_PLAYLIST_PATH
+ self._folder = settings.LOCAL_PLAYLIST_PATH or DEFAULT_PLAYLIST_PATH
self.refresh()
def lookup(self, uri):
@@ -173,8 +183,8 @@ class LocalLibraryProvider(BaseLibraryProvider):
self.refresh()
def refresh(self, uri=None):
- tag_cache = settings.LOCAL_TAG_CACHE_FILE
- music_folder = settings.LOCAL_MUSIC_PATH
+ tag_cache = settings.LOCAL_TAG_CACHE_FILE or DEFAULT_TAG_CACHE_FILE
+ music_folder = settings.LOCAL_MUSIC_PATH or DEFAULT_MUSIC_PATH
tracks = parse_mpd_tag_cache(tag_cache, music_folder)
diff --git a/mopidy/backends/local/translator.py b/mopidy/backends/local/translator.py
index be7ab8a8..3b610a94 100644
--- a/mopidy/backends/local/translator.py
+++ b/mopidy/backends/local/translator.py
@@ -4,6 +4,7 @@ import os
logger = logging.getLogger('mopidy.backends.local.translator')
from mopidy.models import Track, Artist, Album
+from mopidy.utils import locale_decode
from mopidy.utils.path import path_to_uri
def parse_m3u(file_path):
@@ -33,8 +34,8 @@ def parse_m3u(file_path):
try:
with open(file_path) as m3u:
contents = m3u.readlines()
- except IOError, e:
- logger.error('Couldn\'t open m3u: %s', e)
+ except IOError as error:
+ logger.error('Couldn\'t open m3u: %s', locale_decode(error))
return uris
for line in contents:
@@ -61,8 +62,8 @@ def parse_mpd_tag_cache(tag_cache, music_dir=''):
try:
with open(tag_cache) as library:
contents = library.read()
- except IOError, e:
- logger.error('Could not open tag cache: %s', e)
+ except IOError as error:
+ logger.error('Could not open tag cache: %s', locale_decode(error))
return tracks
current = {}
diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py
index 02ccd802..56775926 100644
--- a/mopidy/backends/spotify/__init__.py
+++ b/mopidy/backends/spotify/__init__.py
@@ -10,7 +10,6 @@ from mopidy.gstreamer import GStreamer
logger = logging.getLogger('mopidy.backends.spotify')
-ENCODING = 'utf-8'
BITRATES = {96: 2, 160: 0, 320: 1}
class SpotifyBackend(ThreadingActor, Backend):
@@ -32,8 +31,8 @@ class SpotifyBackend(ThreadingActor, Backend):
**Dependencies:**
- - libspotify == 0.0.8 (libspotify8 package from apt.mopidy.com)
- - pyspotify == 1.3 (python-spotify package from apt.mopidy.com)
+ - libspotify >= 10, < 11 (libspotify10 package from apt.mopidy.com)
+ - pyspotify >= 1.5 (python-spotify package from apt.mopidy.com)
**Settings:**
@@ -78,12 +77,16 @@ class SpotifyBackend(ThreadingActor, Backend):
def on_start(self):
gstreamer_refs = ActorRegistry.get_by_class(GStreamer)
- assert len(gstreamer_refs) == 1, 'Expected exactly one running gstreamer.'
+ assert len(gstreamer_refs) == 1, \
+ 'Expected exactly one running GStreamer.'
self.gstreamer = gstreamer_refs[0].proxy()
logger.info(u'Mopidy uses SPOTIFY(R) CORE')
self.spotify = self._connect()
+ def on_stop(self):
+ self.spotify.logout()
+
def _connect(self):
from .session_manager import SpotifySessionManager
diff --git a/mopidy/backends/spotify/container_manager.py b/mopidy/backends/spotify/container_manager.py
index 520cfb68..27a4d78a 100644
--- a/mopidy/backends/spotify/container_manager.py
+++ b/mopidy/backends/spotify/container_manager.py
@@ -13,13 +13,15 @@ class SpotifyContainerManager(PyspotifyContainerManager):
def container_loaded(self, container, userdata):
"""Callback used by pyspotify"""
logger.debug(u'Callback called: playlist container loaded')
+
self.session_manager.refresh_stored_playlists()
- playlist_container = self.session_manager.session.playlist_container()
- for playlist in playlist_container:
- self.session_manager.playlist_manager.watch(playlist)
- logger.debug(u'Watching %d playlist(s) for changes',
- len(playlist_container))
+ 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(u'Watching %d playlist(s) for changes', count)
def playlist_added(self, container, playlist, position, userdata):
"""Callback used by pyspotify"""
diff --git a/mopidy/backends/spotify/library.py b/mopidy/backends/spotify/library.py
index 40d4a099..a080c7bd 100644
--- a/mopidy/backends/spotify/library.py
+++ b/mopidy/backends/spotify/library.py
@@ -4,7 +4,6 @@ import Queue
from spotify import Link, SpotifyError
from mopidy.backends.base import BaseLibraryProvider
-from mopidy.backends.spotify import ENCODING
from mopidy.backends.spotify.translator import SpotifyTranslator
from mopidy.models import Playlist
@@ -55,7 +54,7 @@ class SpotifyLibraryProvider(BaseLibraryProvider):
spotify_query = u' '.join(spotify_query)
logger.debug(u'Spotify search query: %s' % spotify_query)
queue = Queue.Queue()
- self.backend.spotify.search(spotify_query.encode(ENCODING), queue)
+ self.backend.spotify.search(spotify_query, queue)
try:
return queue.get(timeout=3) # XXX What is an reasonable timeout?
except Queue.Empty:
diff --git a/mopidy/backends/spotify/playlist_manager.py b/mopidy/backends/spotify/playlist_manager.py
index f72ac4ca..05f9514d 100644
--- a/mopidy/backends/spotify/playlist_manager.py
+++ b/mopidy/backends/spotify/playlist_manager.py
@@ -27,7 +27,8 @@ class SpotifyPlaylistManager(PyspotifyPlaylistManager):
def tracks_removed(self, playlist, tracks, userdata):
"""Callback used by pyspotify"""
logger.debug(u'Callback called: '
- u'%d track(s) removed from playlist "%s"', len(tracks), playlist.name())
+ u'%d track(s) removed from playlist "%s"',
+ len(tracks), playlist.name())
self.session_manager.refresh_stored_playlists()
def playlist_renamed(self, playlist, userdata):
diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py
index 2c6509ed..481f7a94 100644
--- a/mopidy/backends/spotify/session_manager.py
+++ b/mopidy/backends/spotify/session_manager.py
@@ -6,7 +6,7 @@ from spotify.manager import SpotifySessionManager as PyspotifySessionManager
from pykka.registry import ActorRegistry
-from mopidy import get_version, settings
+from mopidy import get_version, settings, CACHE_PATH
from mopidy.backends.base import Backend
from mopidy.backends.spotify import BITRATES
from mopidy.backends.spotify.container_manager import SpotifyContainerManager
@@ -21,9 +21,11 @@ logger = logging.getLogger('mopidy.backends.spotify.session_manager')
# pylint: disable = R0901
# SpotifySessionManager: Too many ancestors (9/7)
+
class SpotifySessionManager(BaseThread, PyspotifySessionManager):
- cache_location = settings.SPOTIFY_CACHE_PATH
- settings_location = settings.SPOTIFY_CACHE_PATH
+ cache_location = (settings.SPOTIFY_CACHE_PATH
+ or os.path.join(CACHE_PATH, 'spotify'))
+ settings_location = cache_location
appkey_file = os.path.join(os.path.dirname(__file__), 'spotify_appkey.key')
user_agent = 'Mopidy %s' % get_version()
@@ -41,6 +43,8 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
self.container_manager = None
self.playlist_manager = None
+ self._initial_data_receive_completed = False
+
def run_inside_try(self):
self.setup()
self.connect()
@@ -95,10 +99,6 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
"""Callback used by pyspotify"""
logger.debug(u'User message: %s', message.strip())
- def notify_main_thread(self, session):
- """Callback used by pyspotify"""
- logger.debug(u'notify_main_thread() called')
-
def music_delivery(self, session, frames, frame_size, num_frames,
sample_type, sample_rate, channels):
"""Callback used by pyspotify"""
@@ -128,6 +128,17 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
def log_message(self, session, data):
"""Callback used by pyspotify"""
logger.debug(u'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 stored 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 better.
+ self._initial_data_receive_completed = True
+ self.refresh_stored_playlists()
def end_of_track(self, session):
"""Callback used by pyspotify"""
@@ -137,10 +148,11 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
def refresh_stored_playlists(self):
"""Refresh the stored playlists in the backend with fresh meta data
from Spotify"""
- playlists = []
- for spotify_playlist in self.session.playlist_container():
- playlists.append(
- SpotifyTranslator.to_mopidy_playlist(spotify_playlist))
+ if not self._initial_data_receive_completed:
+ logger.debug(u'Still getting data; skipped refresh of playlists')
+ return
+ playlists = map(SpotifyTranslator.to_mopidy_playlist,
+ self.session.playlist_container())
playlists = filter(None, playlists)
self.backend.stored_playlists.playlists = playlists
logger.debug(u'Refreshed %d stored playlist(s)', len(playlists))
@@ -149,9 +161,18 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
"""Search method used by Mopidy backend"""
def callback(results, userdata=None):
# TODO Include results from results.albums(), etc. too
+ # TODO Consider launching a second search if results.total_tracks()
+ # is larger than len(results.tracks())
playlist = Playlist(tracks=[
SpotifyTranslator.to_mopidy_track(t)
for t in results.tracks()])
queue.put(playlist)
self.connected.wait()
- self.session.search(query, callback)
+ self.session.search(query, callback, track_count=100,
+ album_count=0, artist_count=0)
+
+ def logout(self):
+ """Log out from spotify"""
+ logger.debug(u'Logging out from Spotify')
+ if self.session:
+ self.session.logout()
diff --git a/mopidy/backends/spotify/translator.py b/mopidy/backends/spotify/translator.py
index 95287d77..2f47a42b 100644
--- a/mopidy/backends/spotify/translator.py
+++ b/mopidy/backends/spotify/translator.py
@@ -4,7 +4,6 @@ import logging
from spotify import Link, SpotifyError
from mopidy import settings
-from mopidy.backends.spotify import ENCODING
from mopidy.models import Artist, Album, Track, Playlist
logger = logging.getLogger('mopidy.backends.spotify.translator')
@@ -31,9 +30,10 @@ class SpotifyTranslator(object):
uri = str(Link.from_track(spotify_track, 0))
if not spotify_track.is_loaded():
return Track(uri=uri, name=u'[loading...]')
- if (spotify_track.album() is not None and
- dt.MINYEAR <= int(spotify_track.album().year()) <= dt.MAXYEAR):
- date = dt.date(spotify_track.album().year(), 1, 1)
+ spotify_album = spotify_track.album()
+ if (spotify_album is not None and spotify_album.is_loaded()
+ and dt.MINYEAR <= int(spotify_album.year()) <= dt.MAXYEAR):
+ date = dt.date(spotify_album.year(), 1, 1)
else:
date = None
return Track(
@@ -51,9 +51,8 @@ class SpotifyTranslator(object):
def to_mopidy_playlist(cls, spotify_playlist):
if not spotify_playlist.is_loaded():
return Playlist(name=u'[loading...]')
- # FIXME Replace this try-except with a check on the playlist type,
- # which is currently not supported by pyspotify, to avoid handling
- # playlist folder boundaries like normal playlists.
+ if spotify_playlist.type() != 'playlist':
+ return
try:
return Playlist(
uri=str(Link.from_playlist(spotify_playlist)),
@@ -63,5 +62,4 @@ class SpotifyTranslator(object):
if str(Link.from_track(t, 0))],
)
except SpotifyError, e:
- logger.info(u'Failed translating Spotify playlist '
- '(probably a playlist folder boundary): %s', e)
+ logger.warning(u'Failed translating Spotify playlist: %s', e)
diff --git a/mopidy/core.py b/mopidy/core.py
index b3ce9070..596e0fe5 100644
--- a/mopidy/core.py
+++ b/mopidy/core.py
@@ -1,47 +1,48 @@
import logging
import optparse
+import os
import signal
import sys
-import time
+
+import gobject
+gobject.threads_init()
# Extract any non-GStreamer arguments, and leave the GStreamer arguments for
# processing by GStreamer. This needs to be done before GStreamer is imported,
# so that GStreamer doesn't hijack e.g. ``--help``.
# NOTE This naive fix does not support values like ``bar`` in
# ``--gst-foo bar``. Use equals to pass values, like ``--gst-foo=bar``.
-def is_gst_arg(arg):
- return arg.startswith('--gst') or arg == '--help-gst'
+def is_gst_arg(argument):
+ return argument.startswith('--gst') or argument == '--help-gst'
gstreamer_args = [arg for arg in sys.argv[1:] if is_gst_arg(arg)]
mopidy_args = [arg for arg in sys.argv[1:] if not is_gst_arg(arg)]
sys.argv[1:] = gstreamer_args
-from pykka.registry import ActorRegistry
-
from mopidy import (get_version, settings, OptionalDependencyError,
- SettingsError)
+ SettingsError, DATA_PATH, SETTINGS_PATH, SETTINGS_FILE)
from mopidy.gstreamer import GStreamer
from mopidy.utils import get_class
from mopidy.utils.log import setup_logging
from mopidy.utils.path import get_or_create_folder, get_or_create_file
-from mopidy.utils.process import (GObjectEventThread, exit_handler,
- stop_remaining_actors, stop_actors_by_class)
+from mopidy.utils.process import (exit_handler, stop_remaining_actors,
+ stop_actors_by_class)
from mopidy.utils.settings import list_settings_optparse_callback
logger = logging.getLogger('mopidy.core')
def main():
signal.signal(signal.SIGTERM, exit_handler)
+ loop = gobject.MainLoop()
try:
options = parse_options()
setup_logging(options.verbosity_level, options.save_debug_log)
+ check_old_folders()
setup_settings(options.interactive)
- setup_gobject_loop()
setup_gstreamer()
setup_mixer()
setup_backend()
setup_frontends()
- while True:
- time.sleep(1)
+ loop.run()
except SettingsError as e:
logger.error(e.message)
except KeyboardInterrupt:
@@ -49,6 +50,7 @@ def main():
except Exception as e:
logger.exception(e)
finally:
+ loop.quit()
stop_frontends()
stop_backend()
stop_mixer()
@@ -67,7 +69,7 @@ def parse_options():
action='store_const', const=0, dest='verbosity_level',
help='less output (warning level)')
parser.add_option('-v', '--verbose',
- action='store_const', const=2, dest='verbosity_level',
+ action='count', default=1, dest='verbosity_level',
help='more output (debug level)')
parser.add_option('--save-debug-log',
action='store_true', dest='save_debug_log',
@@ -77,18 +79,26 @@ def parse_options():
help='list current settings')
return parser.parse_args(args=mopidy_args)[0]
+def check_old_folders():
+ old_settings_folder = os.path.expanduser(u'~/.mopidy')
+
+ if not os.path.isdir(old_settings_folder):
+ return
+
+ logger.warning(u'Old settings folder found at %s, settings.py should be '
+ 'moved to %s, any cache data should be deleted. See release notes '
+ 'for further instructions.', old_settings_folder, SETTINGS_PATH)
+
def setup_settings(interactive):
- get_or_create_folder('~/.mopidy/')
- get_or_create_file('~/.mopidy/settings.py')
+ get_or_create_folder(SETTINGS_PATH)
+ get_or_create_folder(DATA_PATH)
+ get_or_create_file(SETTINGS_FILE)
try:
settings.validate(interactive)
except SettingsError, e:
logger.error(e.message)
sys.exit(1)
-def setup_gobject_loop():
- GObjectEventThread().start()
-
def setup_gstreamer():
GStreamer.start()
diff --git a/mopidy/frontends/lastfm.py b/mopidy/frontends/lastfm.py
index d50f8dd8..0e79024b 100644
--- a/mopidy/frontends/lastfm.py
+++ b/mopidy/frontends/lastfm.py
@@ -37,6 +37,7 @@ class LastfmFrontend(ThreadingActor, BackendListener):
"""
def __init__(self):
+ super(LastfmFrontend, self).__init__()
self.lastfm = None
self.last_start_time = None
@@ -57,7 +58,7 @@ class LastfmFrontend(ThreadingActor, BackendListener):
logger.error(u'Error during Last.fm setup: %s', e)
self.stop()
- def started_playing(self, track):
+ def track_playback_started(self, 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())
@@ -74,7 +75,7 @@ class LastfmFrontend(ThreadingActor, BackendListener):
pylast.MalformedResponseError, pylast.WSError) as e:
logger.warning(u'Error submitting playing track to Last.fm: %s', e)
- def stopped_playing(self, track, time_position):
+ def track_playback_ended(self, track, time_position):
artists = ', '.join([a.name for a in track.artists])
duration = track.length and track.length // 1000 or 0
time_position = time_position // 1000
diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py
index f37b2deb..e8b2aabe 100644
--- a/mopidy/frontends/mpd/__init__.py
+++ b/mopidy/frontends/mpd/__init__.py
@@ -1,14 +1,15 @@
-import asyncore
import logging
+import sys
-from pykka.actor import ThreadingActor
+from pykka import registry, actor
-from mopidy.frontends.mpd.server import MpdServer
-from mopidy.utils.process import BaseThread
+from mopidy import listeners, settings
+from mopidy.frontends.mpd import dispatcher, protocol
+from mopidy.utils import locale_decode, log, network, process
logger = logging.getLogger('mopidy.frontends.mpd')
-class MpdFrontend(ThreadingActor):
+class MpdFrontend(actor.ThreadingActor, listeners.BackendListener):
"""
The MPD frontend.
@@ -24,23 +25,86 @@ class MpdFrontend(ThreadingActor):
"""
def __init__(self):
- self._thread = None
+ super(MpdFrontend, self).__init__()
+ hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME)
+ port = settings.MPD_SERVER_PORT
+
+ try:
+ network.Server(hostname, port, protocol=MpdSession,
+ max_connections=settings.MPD_SERVER_MAX_CONNECTIONS)
+ except IOError as error:
+ logger.error(u'MPD server startup failed: %s', locale_decode(error))
+ sys.exit(1)
+
+ logger.info(u'MPD server running at [%s]:%s', hostname, port)
+
+ def on_stop(self):
+ process.stop_actors_by_class(MpdSession)
+
+ def send_idle(self, subsystem):
+ # FIXME this should be updated once pykka supports non-blocking calls
+ # on proxies or some similar solution
+ registry.ActorRegistry.broadcast({
+ 'command': 'pykka_call',
+ 'attr_path': ('on_idle',),
+ 'args': [subsystem],
+ 'kwargs': {},
+ }, target_class=MpdSession)
+
+ def playback_state_changed(self):
+ self.send_idle('player')
+
+ def playlist_changed(self):
+ self.send_idle('playlist')
+
+ def options_changed(self):
+ self.send_idle('options')
+
+ def volume_changed(self):
+ self.send_idle('mixer')
+
+
+class MpdSession(network.LineProtocol):
+ """
+ The MPD client session. Keeps track of a single client session. Any
+ requests from the client is passed on to the MPD request dispatcher.
+ """
+
+ terminator = protocol.LINE_TERMINATOR
+ encoding = protocol.ENCODING
+ delimeter = r'\r?\n'
+
+ def __init__(self, connection):
+ super(MpdSession, self).__init__(connection)
+ self.dispatcher = dispatcher.MpdDispatcher(self)
def on_start(self):
- self._thread = MpdThread()
- self._thread.start()
+ logger.info(u'New MPD connection from [%s]:%s', self.host, self.port)
+ self.send_lines([u'OK MPD %s' % protocol.VERSION])
- def on_receive(self, message):
- pass # Ignore any messages
+ def on_line_received(self, line):
+ logger.debug(u'Request from [%s]:%s to %s: %s', self.host, self.port,
+ self.actor_urn, line)
+ response = self.dispatcher.handle_request(line)
+ if not response:
+ return
-class MpdThread(BaseThread):
- def __init__(self):
- super(MpdThread, self).__init__()
- self.name = u'MpdThread'
+ logger.debug(u'Response to [%s]:%s from %s: %s', self.host, self.port,
+ self.actor_urn, log.indent(self.terminator.join(response)))
- def run_inside_try(self):
- logger.debug(u'Starting MPD server thread')
- server = MpdServer()
- server.start()
- asyncore.loop()
+ self.send_lines(response)
+
+ def on_idle(self, subsystem):
+ self.dispatcher.handle_idle(subsystem)
+
+ def decode(self, line):
+ try:
+ return super(MpdSession, self).decode(line.decode('string_escape'))
+ except ValueError:
+ logger.warning(u'Stopping actor due to unescaping error, data '
+ 'supplied by client was not valid.')
+ self.stop()
+
+ def close(self):
+ self.stop()
diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py
index 18f994de..2b012c7c 100644
--- a/mopidy/frontends/mpd/dispatcher.py
+++ b/mopidy/frontends/mpd/dispatcher.py
@@ -27,6 +27,8 @@ class MpdDispatcher(object):
back to the MPD session.
"""
+ _noidle = re.compile(r'^noidle$')
+
def __init__(self, session=None):
self.authenticated = False
self.command_list = False
@@ -42,11 +44,28 @@ class MpdDispatcher(object):
self._catch_mpd_ack_errors_filter,
self._authenticate_filter,
self._command_list_filter,
+ self._idle_filter,
self._add_ok_filter,
self._call_handler_filter,
]
return self._call_next_filter(request, response, filter_chain)
+ def handle_idle(self, subsystem):
+ self.context.events.add(subsystem)
+
+ subsystems = self.context.subscriptions.intersection(
+ self.context.events)
+ if not subsystems:
+ return
+
+ response = []
+ for subsystem in subsystems:
+ response.append(u'changed: %s' % subsystem)
+ response.append(u'OK')
+ self.context.subscriptions = set()
+ self.context.events = set()
+ self.context.session.send_lines(response)
+
def _call_next_filter(self, request, response, filter_chain):
if filter_chain:
next_filter = filter_chain.pop(0)
@@ -71,7 +90,7 @@ class MpdDispatcher(object):
def _authenticate_filter(self, request, response, filter_chain):
if self.authenticated:
return self._call_next_filter(request, response, filter_chain)
- elif settings.MPD_SERVER_PASSWORD is None:
+ elif settings.MPD_SERVER_PASSWORD is None:
self.authenticated = True
return self._call_next_filter(request, response, filter_chain)
else:
@@ -108,6 +127,29 @@ class MpdDispatcher(object):
and request != u'command_list_end')
+ ### Filter: idle
+
+ def _idle_filter(self, request, response, filter_chain):
+ if self._is_currently_idle() and not self._noidle.match(request):
+ logger.debug(u'Client sent us %s, only %s is allowed while in '
+ 'the idle state', repr(request), repr(u'noidle'))
+ self.context.session.close()
+ return []
+
+ if not self._is_currently_idle() and self._noidle.match(request):
+ return [] # noidle was called before idle
+
+ response = self._call_next_filter(request, response, filter_chain)
+
+ if self._is_currently_idle():
+ return []
+ else:
+ return response
+
+ def _is_currently_idle(self):
+ return bool(self.context.subscriptions)
+
+
### Filter: add OK
def _add_ok_filter(self, request, response, filter_chain):
@@ -178,12 +220,20 @@ class MpdContext(object):
#: The current :class:`MpdDispatcher`.
dispatcher = None
- #: The current :class:`mopidy.frontends.mpd.session.MpdSession`.
+ #: The current :class:`mopidy.frontends.mpd.MpdSession`.
session = None
+ #: The active subsystems that have pending events.
+ events = None
+
+ #: The subsytems that we want to be notified about in idle mode.
+ subscriptions = None
+
def __init__(self, dispatcher, session=None):
self.dispatcher = dispatcher
self.session = session
+ self.events = set()
+ self.subscriptions = set()
self._backend = None
self._mixer = None
@@ -192,11 +242,11 @@ class MpdContext(object):
"""
The backend. An instance of :class:`mopidy.backends.base.Backend`.
"""
- if self._backend is not None:
- return self._backend
- backend_refs = ActorRegistry.get_by_class(Backend)
- assert len(backend_refs) == 1, 'Expected exactly one running backend.'
- self._backend = backend_refs[0].proxy()
+ if self._backend is None:
+ backend_refs = ActorRegistry.get_by_class(Backend)
+ assert len(backend_refs) == 1, \
+ 'Expected exactly one running backend.'
+ self._backend = backend_refs[0].proxy()
return self._backend
@property
@@ -204,9 +254,8 @@ class MpdContext(object):
"""
The mixer. An instance of :class:`mopidy.mixers.base.BaseMixer`.
"""
- if self._mixer is not None:
- return self._mixer
- mixer_refs = ActorRegistry.get_by_class(BaseMixer)
- assert len(mixer_refs) == 1, 'Expected exactly one running mixer.'
- self._mixer = mixer_refs[0].proxy()
+ if self._mixer is None:
+ mixer_refs = ActorRegistry.get_by_class(BaseMixer)
+ assert len(mixer_refs) == 1, 'Expected exactly one running mixer.'
+ self._mixer = mixer_refs[0].proxy()
return self._mixer
diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py
index c7136804..0d61c887 100644
--- a/mopidy/frontends/mpd/protocol/current_playlist.py
+++ b/mopidy/frontends/mpd/protocol/current_playlist.py
@@ -1,7 +1,8 @@
from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError,
MpdNotImplemented)
from mopidy.frontends.mpd.protocol import handle_request
-from mopidy.frontends.mpd.translator import tracks_to_mpd_format
+from mopidy.frontends.mpd.translator import (track_to_mpd_format,
+ tracks_to_mpd_format)
@handle_request(r'^add "(?P[^"]*)"$')
def add(context, uri):
@@ -74,8 +75,8 @@ def delete_range(context, start, end=None):
if end is not None:
end = int(end)
else:
- end = len(context.backend.current_playlist.tracks.get())
- cp_tracks = context.backend.current_playlist.cp_tracks.get()[start:end]
+ end = context.backend.current_playlist.length.get()
+ cp_tracks = context.backend.current_playlist.slice(start, end).get()
if not cp_tracks:
raise MpdArgError(u'Bad song index', command=u'delete')
for (cpid, _) in cp_tracks:
@@ -86,7 +87,8 @@ def delete_songpos(context, songpos):
"""See :meth:`delete_range`"""
try:
songpos = int(songpos)
- (cpid, _) = context.backend.current_playlist.cp_tracks.get()[songpos]
+ (cpid, _) = context.backend.current_playlist.slice(
+ songpos, songpos + 1).get()[0]
context.backend.current_playlist.remove(cpid=cpid)
except IndexError:
raise MpdArgError(u'Bad song index', command=u'delete')
@@ -157,8 +159,7 @@ def moveid(context, cpid, to):
cpid = int(cpid)
to = int(to)
cp_track = context.backend.current_playlist.get(cpid=cpid).get()
- position = context.backend.current_playlist.cp_tracks.get().index(
- cp_track)
+ position = context.backend.current_playlist.index(cp_track).get()
context.backend.current_playlist.move(position, position + 1, to)
@handle_request(r'^playlist$')
@@ -193,10 +194,8 @@ def playlistfind(context, tag, needle):
if tag == 'filename':
try:
cp_track = context.backend.current_playlist.get(uri=needle).get()
- (cpid, track) = cp_track
- position = context.backend.current_playlist.cp_tracks.get().index(
- cp_track)
- return track.mpd_format(cpid=cpid, position=position)
+ position = context.backend.current_playlist.index(cp_track).get()
+ return track_to_mpd_format(cp_track, position=position)
except LookupError:
return None
raise MpdNotImplemented # TODO
@@ -215,18 +214,16 @@ def playlistid(context, cpid=None):
try:
cpid = int(cpid)
cp_track = context.backend.current_playlist.get(cpid=cpid).get()
- position = context.backend.current_playlist.cp_tracks.get().index(
- cp_track)
- return cp_track.track.mpd_format(position=position, cpid=cpid)
+ position = context.backend.current_playlist.index(cp_track).get()
+ return track_to_mpd_format(cp_track, position=position)
except LookupError:
raise MpdNoExistError(u'No such song', command=u'playlistid')
else:
- cpids = [ct[0] for ct in
- context.backend.current_playlist.cp_tracks.get()]
return tracks_to_mpd_format(
- context.backend.current_playlist.tracks.get(), cpids=cpids)
+ context.backend.current_playlist.cp_tracks.get())
@handle_request(r'^playlistinfo$')
+@handle_request(r'^playlistinfo "-1"$')
@handle_request(r'^playlistinfo "(?P-?\d+)"$')
@handle_request(r'^playlistinfo "(?P\d+):(?P\d+)*"$')
def playlistinfo(context, songpos=None,
@@ -245,36 +242,22 @@ def playlistinfo(context, songpos=None,
- uses negative indexes, like ``playlistinfo "-1"``, to request
the entire playlist
"""
- if songpos == "-1":
- songpos = None
-
if songpos is not None:
songpos = int(songpos)
- start = songpos
- end = songpos + 1
- if start == -1:
- end = None
- cpids = [ct[0] for ct in
- context.backend.current_playlist.cp_tracks.get()]
- return tracks_to_mpd_format(
- context.backend.current_playlist.tracks.get(),
- start, end, cpids=cpids)
+ cp_track = context.backend.current_playlist.get(cpid=songpos).get()
+ return track_to_mpd_format(cp_track, position=songpos)
else:
if start is None:
start = 0
start = int(start)
- if not (0 <= start <= len(
- context.backend.current_playlist.tracks.get())):
+ if not (0 <= start <= context.backend.current_playlist.length.get()):
raise MpdArgError(u'Bad song index', command=u'playlistinfo')
if end is not None:
end = int(end)
- if end > len(context.backend.current_playlist.tracks.get()):
+ if end > context.backend.current_playlist.length.get():
end = None
- cpids = [ct[0] for ct in
- context.backend.current_playlist.cp_tracks.get()]
- return tracks_to_mpd_format(
- context.backend.current_playlist.tracks.get(),
- start, end, cpids=cpids)
+ cp_tracks = context.backend.current_playlist.cp_tracks.get()
+ return tracks_to_mpd_format(cp_tracks, start, end)
@handle_request(r'^playlistsearch "(?P[^"]+)" "(?P[^"]+)"$')
@handle_request(r'^playlistsearch (?P\S+) "(?P[^"]+)"$')
@@ -313,10 +296,8 @@ def plchanges(context, version):
"""
# XXX Naive implementation that returns all tracks as changed
if int(version) < context.backend.current_playlist.version:
- cpids = [ct[0] for ct in
- context.backend.current_playlist.cp_tracks.get()]
return tracks_to_mpd_format(
- context.backend.current_playlist.tracks.get(), cpids=cpids)
+ context.backend.current_playlist.cp_tracks.get())
@handle_request(r'^plchangesposid "(?P\d+)"$')
def plchangesposid(context, version):
@@ -392,7 +373,6 @@ def swapid(context, cpid1, cpid2):
cpid2 = int(cpid2)
cp_track1 = context.backend.current_playlist.get(cpid=cpid1).get()
cp_track2 = context.backend.current_playlist.get(cpid=cpid2).get()
- cp_tracks = context.backend.current_playlist.cp_tracks.get()
- position1 = cp_tracks.index(cp_track1)
- position2 = cp_tracks.index(cp_track2)
+ position1 = context.backend.current_playlist.index(cp_track1).get()
+ position2 = context.backend.current_playlist.index(cp_track2).get()
swap(context, position1, position2)
diff --git a/mopidy/frontends/mpd/protocol/empty.py b/mopidy/frontends/mpd/protocol/empty.py
index 0e418551..4cdafd87 100644
--- a/mopidy/frontends/mpd/protocol/empty.py
+++ b/mopidy/frontends/mpd/protocol/empty.py
@@ -1,6 +1,6 @@
from mopidy.frontends.mpd.protocol import handle_request
-@handle_request(r'^$')
+@handle_request(r'^[ ]*$')
def empty(context):
"""The original MPD server returns ``OK`` on an empty request."""
pass
diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py
index 0343b3ab..cde2754a 100644
--- a/mopidy/frontends/mpd/protocol/music_db.py
+++ b/mopidy/frontends/mpd/protocol/music_db.py
@@ -1,8 +1,9 @@
import re
import shlex
-from mopidy.frontends.mpd.protocol import handle_request, stored_playlists
from mopidy.frontends.mpd.exceptions import MpdArgError, MpdNotImplemented
+from mopidy.frontends.mpd.protocol import handle_request, stored_playlists
+from mopidy.frontends.mpd.translator import playlist_to_mpd_format
def _build_query(mpd_query):
"""
@@ -68,7 +69,8 @@ def find(context, mpd_query):
- also uses the search type "date".
"""
query = _build_query(mpd_query)
- return context.backend.library.find_exact(**query).get().mpd_format()
+ return playlist_to_mpd_format(
+ context.backend.library.find_exact(**query).get())
@handle_request(r'^findadd '
r'(?P("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? '
@@ -187,8 +189,14 @@ def _list_build_query(field, mpd_query):
"""Converts a ``list`` query to a Mopidy query."""
if mpd_query is None:
return {}
- # shlex does not seem to be friends with unicode objects
- tokens = shlex.split(mpd_query.encode('utf-8'))
+ try:
+ # shlex does not seem to be friends with unicode objects
+ tokens = shlex.split(mpd_query.encode('utf-8'))
+ except ValueError as error:
+ if error.message == 'No closing quotation':
+ raise MpdArgError(u'Invalid unquoted character', command=u'list')
+ else:
+ raise error
tokens = [t.decode('utf-8') for t in tokens]
if len(tokens) == 1:
if field == u'album':
@@ -324,7 +332,8 @@ def search(context, mpd_query):
- also uses the search type "date".
"""
query = _build_query(mpd_query)
- return context.backend.library.search(**query).get().mpd_format()
+ return playlist_to_mpd_format(
+ context.backend.library.search(**query).get())
@handle_request(r'^update( "(?P[^"]+)")*$')
def update(context, uri=None, rescan_unmodified_files=False):
diff --git a/mopidy/frontends/mpd/protocol/playback.py b/mopidy/frontends/mpd/protocol/playback.py
index 63cfe649..948083a8 100644
--- a/mopidy/frontends/mpd/protocol/playback.py
+++ b/mopidy/frontends/mpd/protocol/playback.py
@@ -178,7 +178,8 @@ def playpos(context, songpos):
if songpos == -1:
return _play_minus_one(context)
try:
- cp_track = context.backend.current_playlist.cp_tracks.get()[songpos]
+ cp_track = context.backend.current_playlist.slice(
+ songpos, songpos + 1).get()[0]
return context.backend.playback.play(cp_track).get()
except IndexError:
raise MpdArgError(u'Bad song index', command=u'play')
@@ -191,8 +192,8 @@ def _play_minus_one(context):
elif context.backend.playback.current_cp_track.get() is not None:
cp_track = context.backend.playback.current_cp_track.get()
return context.backend.playback.play(cp_track).get()
- elif context.backend.current_playlist.cp_tracks.get():
- cp_track = context.backend.current_playlist.cp_tracks.get()[0]
+ elif context.backend.current_playlist.slice(0, 1).get():
+ cp_track = context.backend.current_playlist.slice(0, 1).get()[0]
return context.backend.playback.play(cp_track).get()
else:
return # Fail silently
diff --git a/mopidy/frontends/mpd/protocol/reflection.py b/mopidy/frontends/mpd/protocol/reflection.py
index 3618f5e1..df13b4b4 100644
--- a/mopidy/frontends/mpd/protocol/reflection.py
+++ b/mopidy/frontends/mpd/protocol/reflection.py
@@ -11,28 +11,16 @@ def commands(context):
Shows which commands the current user has access to.
"""
if context.dispatcher.authenticated:
- command_names = [command.name for command in mpd_commands]
+ command_names = set([command.name for command in mpd_commands])
else:
- command_names = [command.name for command in mpd_commands
- if not command.auth_required]
+ command_names = set([command.name for command in mpd_commands
+ if not command.auth_required])
- # No permission to use
- if 'kill' in command_names:
- command_names.remove('kill')
-
- # Not shown by MPD in its command list
- if 'command_list_begin' in command_names:
- command_names.remove('command_list_begin')
- if 'command_list_ok_begin' in command_names:
- command_names.remove('command_list_ok_begin')
- if 'command_list_end' in command_names:
- command_names.remove('command_list_end')
- if 'idle' in command_names:
- command_names.remove('idle')
- if 'noidle' in command_names:
- command_names.remove('noidle')
- if 'sticker' in command_names:
- command_names.remove('sticker')
+ # No one is permited to use kill, rest of commands are not listed by MPD,
+ # so we shouldn't either.
+ command_names = command_names - set(['kill', 'command_list_begin',
+ 'command_list_ok_begin', 'command_list_ok_begin', 'command_list_end',
+ 'idle', 'noidle', 'sticker'])
return [('command', command_name) for command_name in sorted(command_names)]
diff --git a/mopidy/frontends/mpd/protocol/status.py b/mopidy/frontends/mpd/protocol/status.py
index abbb8d7f..f32c46c8 100644
--- a/mopidy/frontends/mpd/protocol/status.py
+++ b/mopidy/frontends/mpd/protocol/status.py
@@ -1,8 +1,13 @@
import pykka.future
from mopidy.backends.base import PlaybackController
-from mopidy.frontends.mpd.protocol import handle_request
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
+from mopidy.frontends.mpd.protocol import handle_request
+from mopidy.frontends.mpd.translator import track_to_mpd_format
+
+#: Subsystems that can be registered with idle command.
+SUBSYSTEMS = ['database', 'mixer', 'options', 'output',
+ 'player', 'playlist', 'stored_playlist', 'update', ]
@handle_request(r'^clearerror$')
def clearerror(context):
@@ -28,9 +33,8 @@ def currentsong(context):
"""
current_cp_track = context.backend.playback.current_cp_track.get()
if current_cp_track is not None:
- return current_cp_track.track.mpd_format(
- position=context.backend.playback.current_playlist_position.get(),
- cpid=current_cp_track.cpid)
+ position = context.backend.playback.current_playlist_position.get()
+ return track_to_mpd_format(current_cp_track, position=position)
@handle_request(r'^idle$')
@handle_request(r'^idle (?P.+)$')
@@ -67,12 +71,36 @@ def idle(context, subsystems=None):
notifications when something changed in one of the specified
subsystems.
"""
- pass # TODO
+
+ if subsystems:
+ subsystems = subsystems.split()
+ else:
+ subsystems = SUBSYSTEMS
+
+ for subsystem in subsystems:
+ context.subscriptions.add(subsystem)
+
+ active = context.subscriptions.intersection(context.events)
+ if not active:
+ context.session.prevent_timeout = True
+ return
+
+ response = []
+ context.events = set()
+ context.subscriptions = set()
+
+ for subsystem in active:
+ response.append(u'changed: %s' % subsystem)
+ return response
@handle_request(r'^noidle$')
def noidle(context):
"""See :meth:`_status_idle`."""
- pass # TODO
+ if not context.subscriptions:
+ return
+ context.subscriptions = set()
+ context.events = set()
+ context.session.prevent_timeout = False
@handle_request(r'^stats$')
def stats(context):
@@ -125,15 +153,20 @@ def status(context):
- ``nextsongid``: playlist songid of the next song to be played
- ``time``: total time elapsed (of current playing/paused song)
- ``elapsed``: Total time elapsed within the current song, but with
- higher resolution.
+ higher resolution.
- ``bitrate``: instantaneous bitrate in kbps
- ``xfade``: crossfade in seconds
- ``audio``: sampleRate``:bits``:channels
- ``updatings_db``: job id
- ``error``: if there is an error, returns message here
+
+ *Clarifications based on experience implementing*
+ - ``volume``: can also be -1 if no output is set.
+ - ``elapsed``: Higher resolution means time in seconds with three
+ decimal places for millisecond precision.
"""
futures = {
- 'current_playlist.tracks': context.backend.current_playlist.tracks,
+ 'current_playlist.length': context.backend.current_playlist.length,
'current_playlist.version': context.backend.current_playlist.version,
'mixer.volume': context.mixer.volume,
'playback.consume': context.backend.playback.consume,
@@ -180,7 +213,7 @@ def _status_consume(futures):
return 0
def _status_playlist_length(futures):
- return len(futures['current_playlist.tracks'].get())
+ return futures['current_playlist.length'].get()
def _status_playlist_version(futures):
return futures['current_playlist.version'].get()
@@ -214,11 +247,11 @@ def _status_state(futures):
return u'pause'
def _status_time(futures):
- return u'%s:%s' % (_status_time_elapsed(futures) // 1000,
+ return u'%d:%d' % (futures['playback.time_position'].get() // 1000,
_status_time_total(futures) // 1000)
def _status_time_elapsed(futures):
- return futures['playback.time_position'].get()
+ return u'%.3f' % (futures['playback.time_position'].get() / 1000.0)
def _status_time_total(futures):
current_cp_track = futures['playback.current_cp_track'].get()
diff --git a/mopidy/frontends/mpd/protocol/stored_playlists.py b/mopidy/frontends/mpd/protocol/stored_playlists.py
index 0a157f66..bb39d328 100644
--- a/mopidy/frontends/mpd/protocol/stored_playlists.py
+++ b/mopidy/frontends/mpd/protocol/stored_playlists.py
@@ -1,7 +1,8 @@
import datetime as dt
-from mopidy.frontends.mpd.protocol import handle_request
from mopidy.frontends.mpd.exceptions import MpdNoExistError, MpdNotImplemented
+from mopidy.frontends.mpd.protocol import handle_request
+from mopidy.frontends.mpd.translator import playlist_to_mpd_format
@handle_request(r'^listplaylist "(?P[^"]+)"$')
def listplaylist(context, name):
@@ -40,7 +41,7 @@ def listplaylistinfo(context, name):
"""
try:
playlist = context.backend.stored_playlists.get(name=name).get()
- return playlist.mpd_format()
+ return playlist_to_mpd_format(playlist)
except LookupError:
raise MpdNoExistError(
u'No such playlist', command=u'listplaylistinfo')
diff --git a/mopidy/frontends/mpd/server.py b/mopidy/frontends/mpd/server.py
deleted file mode 100644
index 62e443fb..00000000
--- a/mopidy/frontends/mpd/server.py
+++ /dev/null
@@ -1,38 +0,0 @@
-import asyncore
-import logging
-import sys
-
-from mopidy import settings
-from mopidy.utils import network
-from .session import MpdSession
-
-logger = logging.getLogger('mopidy.frontends.mpd.server')
-
-class MpdServer(asyncore.dispatcher):
- """
- The MPD server. Creates a :class:`mopidy.frontends.mpd.session.MpdSession`
- for each client connection.
- """
-
- def start(self):
- """Start MPD server."""
- try:
- self.set_socket(network.create_socket())
- self.set_reuse_addr()
- hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME)
- port = settings.MPD_SERVER_PORT
- logger.debug(u'MPD server is binding to [%s]:%s', hostname, port)
- self.bind((hostname, port))
- self.listen(1)
- logger.info(u'MPD server running at [%s]:%s', hostname, port)
- except IOError, e:
- logger.error(u'MPD server startup failed: %s' %
- str(e).decode('utf-8'))
- sys.exit(1)
-
- def handle_accept(self):
- """Called by asyncore when a new client connects."""
- (client_socket, client_socket_address) = self.accept()
- logger.info(u'MPD client connection from [%s]:%s',
- client_socket_address[0], client_socket_address[1])
- MpdSession(self, client_socket, client_socket_address)
diff --git a/mopidy/frontends/mpd/session.py b/mopidy/frontends/mpd/session.py
deleted file mode 100644
index ce5d3be7..00000000
--- a/mopidy/frontends/mpd/session.py
+++ /dev/null
@@ -1,58 +0,0 @@
-import asynchat
-import logging
-
-from mopidy.frontends.mpd.dispatcher import MpdDispatcher
-from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR, VERSION
-from mopidy.utils.log import indent
-
-logger = logging.getLogger('mopidy.frontends.mpd.session')
-
-class MpdSession(asynchat.async_chat):
- """
- The MPD client session. Keeps track of a single client session. Any
- requests from the client is passed on to the MPD request dispatcher.
- """
-
- def __init__(self, server, client_socket, client_socket_address):
- asynchat.async_chat.__init__(self, sock=client_socket)
- self.server = server
- self.client_address = client_socket_address[0]
- self.client_port = client_socket_address[1]
- self.input_buffer = []
- self.authenticated = False
- self.set_terminator(LINE_TERMINATOR.encode(ENCODING))
- self.dispatcher = MpdDispatcher(session=self)
- self.send_response([u'OK MPD %s' % VERSION])
-
- def collect_incoming_data(self, data):
- """Called by asynchat when new data arrives."""
- self.input_buffer.append(data)
-
- def found_terminator(self):
- """Called by asynchat when a terminator is found in incoming data."""
- data = ''.join(self.input_buffer).strip()
- self.input_buffer = []
- try:
- self.send_response(self.handle_request(data))
- except UnicodeDecodeError as e:
- logger.warning(u'Received invalid data: %s', e)
-
- def handle_request(self, request):
- """Handle the request using the MPD command handlers."""
- request = request.decode(ENCODING)
- logger.debug(u'Request from [%s]:%s: %s', self.client_address,
- self.client_port, indent(request))
- return self.dispatcher.handle_request(request)
-
- def send_response(self, response):
- """
- Format a response from the MPD command handlers and send it to the
- client.
- """
- if response:
- response = LINE_TERMINATOR.join(response)
- logger.debug(u'Response to [%s]:%s: %s', self.client_address,
- self.client_port, indent(response))
- response = u'%s%s' % (response, LINE_TERMINATOR)
- data = response.encode(ENCODING)
- self.push(data)
diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py
index 562b2d2d..6ae32c9e 100644
--- a/mopidy/frontends/mpd/translator.py
+++ b/mopidy/frontends/mpd/translator.py
@@ -2,26 +2,28 @@ import os
import re
from mopidy import settings
-from mopidy.utils.path import mtime as get_mtime
from mopidy.frontends.mpd import protocol
-from mopidy.utils.path import uri_to_path, split_path
+from mopidy.models import CpTrack
+from mopidy.utils.path import mtime as get_mtime, uri_to_path, split_path
-def track_to_mpd_format(track, position=None, cpid=None):
+def track_to_mpd_format(track, position=None):
"""
Format track for output to MPD client.
:param track: the track
- :type track: :class:`mopidy.models.Track`
+ :type track: :class:`mopidy.models.Track` or :class:`mopidy.models.CpTrack`
:param position: track's position in playlist
:type position: integer
- :param cpid: track's CPID (current playlist ID)
- :type cpid: integer
:param key: if we should set key
:type key: boolean
:param mtime: if we should set mtime
:type mtime: boolean
:rtype: list of two-tuples
"""
+ if isinstance(track, CpTrack):
+ (cpid, track) = track
+ else:
+ (cpid, track) = (None, track)
result = [
('file', track.uri or ''),
('Time', track.length and (track.length // 1000) or 0),
@@ -88,14 +90,15 @@ def artists_to_mpd_format(artists):
artists.sort(key=lambda a: a.name)
return u', '.join([a.name for a in artists if a.name])
-def tracks_to_mpd_format(tracks, start=0, end=None, cpids=None):
+def tracks_to_mpd_format(tracks, start=0, end=None):
"""
Format list of tracks for output to MPD client.
Optionally limit output to the slice ``[start:end]`` of the list.
:param tracks: the tracks
- :type tracks: list of :class:`mopidy.models.Track`
+ :type tracks: list of :class:`mopidy.models.Track` or
+ :class:`mopidy.models.CpTrack`
:param start: position of first track to include in output
:type start: int (positive or negative)
:param end: position after last track to include in output
@@ -106,11 +109,10 @@ def tracks_to_mpd_format(tracks, start=0, end=None, cpids=None):
end = len(tracks)
tracks = tracks[start:end]
positions = range(start, end)
- cpids = cpids and cpids[start:end] or [None for _ in tracks]
- assert len(tracks) == len(positions) == len(cpids)
+ assert len(tracks) == len(positions)
result = []
- for track, position, cpid in zip(tracks, positions, cpids):
- result.append(track_to_mpd_format(track, position, cpid))
+ for track, position in zip(tracks, positions):
+ result.append(track_to_mpd_format(track, position))
return result
def playlist_to_mpd_format(playlist, *args, **kwargs):
diff --git a/mopidy/frontends/mpris/__init__.py b/mopidy/frontends/mpris/__init__.py
new file mode 100644
index 00000000..0f5d35c5
--- /dev/null
+++ b/mopidy/frontends/mpris/__init__.py
@@ -0,0 +1,131 @@
+import logging
+
+logger = logging.getLogger('mopidy.frontends.mpris')
+
+try:
+ import indicate
+except ImportError as import_error:
+ indicate = None
+ logger.debug(u'Startup notification will not be sent (%s)', import_error)
+
+from pykka.actor import ThreadingActor
+
+from mopidy import settings
+from mopidy.frontends.mpris import objects
+from mopidy.listeners import BackendListener
+
+
+class MprisFrontend(ThreadingActor, BackendListener):
+ """
+ Frontend which lets you control Mopidy through the Media Player Remote
+ Interfacing Specification (`MPRIS `_) D-Bus
+ interface.
+
+ An example of an MPRIS client is the `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
+ :attr:`mopidy.settings.DESKTOP_FILE`. See :ref:`install_desktop_file` for
+ details.
+
+ **Testing the frontend**
+
+ To test, 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')
+ """
+
+ def __init__(self):
+ super(MprisFrontend, self).__init__()
+ self.indicate_server = None
+ self.mpris_object = None
+
+ def on_start(self):
+ try:
+ self.mpris_object = objects.MprisObject()
+ self._send_startup_notification()
+ except Exception as e:
+ logger.error(u'MPRIS frontend setup failed (%s)', e)
+ self.stop()
+
+ def on_stop(self):
+ logger.debug(u'Removing MPRIS object from D-Bus connection...')
+ if self.mpris_object:
+ self.mpris_object.remove_from_connection()
+ self.mpris_object = None
+ logger.debug(u'Removed MPRIS object from D-Bus connection')
+
+ def _send_startup_notification(self):
+ """
+ Send startup notification using libindicate to make Mopidy appear in
+ e.g. `Ubuntu's sound menu `_.
+
+ 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(u'Sending startup notification...')
+ self.indicate_server = indicate.Server()
+ self.indicate_server.set_type('music.mopidy')
+ self.indicate_server.set_desktop_file(settings.DESKTOP_FILE)
+ self.indicate_server.show()
+ logger.debug(u'Startup notification sent')
+
+ def _emit_properties_changed(self, *changed_properties):
+ if self.mpris_object is None:
+ return
+ props_with_new_values = [
+ (p, self.mpris_object.Get(objects.PLAYER_IFACE, p))
+ for p in changed_properties]
+ self.mpris_object.PropertiesChanged(objects.PLAYER_IFACE,
+ dict(props_with_new_values), [])
+
+ def track_playback_paused(self, track, time_position):
+ logger.debug(u'Received track playback paused event')
+ self._emit_properties_changed('PlaybackStatus')
+
+ def track_playback_resumed(self, track, time_position):
+ logger.debug(u'Received track playback resumed event')
+ self._emit_properties_changed('PlaybackStatus')
+
+ def track_playback_started(self, track):
+ logger.debug(u'Received track playback started event')
+ self._emit_properties_changed('PlaybackStatus', 'Metadata')
+
+ def track_playback_ended(self, track, time_position):
+ logger.debug(u'Received track playback ended event')
+ self._emit_properties_changed('PlaybackStatus', 'Metadata')
+
+ def volume_changed(self):
+ logger.debug(u'Received volume changed event')
+ self._emit_properties_changed('Volume')
+
+ def seeked(self):
+ logger.debug(u'Received seeked event')
+ if self.mpris_object is None:
+ return
+ self.mpris_object.Seeked(
+ self.mpris_object.Get(objects.PLAYER_IFACE, 'Position'))
diff --git a/mopidy/frontends/mpris/objects.py b/mopidy/frontends/mpris/objects.py
new file mode 100644
index 00000000..9ed1fe2c
--- /dev/null
+++ b/mopidy/frontends/mpris/objects.py
@@ -0,0 +1,437 @@
+import logging
+import os
+
+logger = logging.getLogger('mopidy.frontends.mpris')
+
+try:
+ import dbus
+ import dbus.mainloop.glib
+ import dbus.service
+ import gobject
+except ImportError as import_error:
+ from mopidy import OptionalDependencyError
+ raise OptionalDependencyError(import_error)
+
+from pykka.registry import ActorRegistry
+
+from mopidy import settings
+from mopidy.backends.base import Backend
+from mopidy.backends.base.playback import PlaybackController
+from mopidy.mixers.base import BaseMixer
+from mopidy.utils.process import exit_process
+
+# 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'
+
+
+class MprisObject(dbus.service.Object):
+ """Implements http://www.mpris.org/2.1/spec/"""
+
+ properties = None
+
+ def __init__(self):
+ self._backend = None
+ self._mixer = None
+ self.properties = {
+ ROOT_IFACE: self._get_root_iface_properties(),
+ PLAYER_IFACE: self._get_player_iface_properties(),
+ }
+ bus_name = self._connect_to_dbus()
+ super(MprisObject, self).__init__(bus_name, OBJECT_PATH)
+
+ def _get_root_iface_properties(self):
+ return {
+ 'CanQuit': (True, 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 _connect_to_dbus(self):
+ logger.debug(u'Connecting to D-Bus...')
+ mainloop = dbus.mainloop.glib.DBusGMainLoop()
+ bus_name = dbus.service.BusName(BUS_NAME,
+ dbus.SessionBus(mainloop=mainloop))
+ logger.info(u'Connected to D-Bus')
+ return bus_name
+
+ @property
+ def backend(self):
+ if self._backend is None:
+ backend_refs = ActorRegistry.get_by_class(Backend)
+ assert len(backend_refs) == 1, \
+ 'Expected exactly one running backend.'
+ self._backend = backend_refs[0].proxy()
+ return self._backend
+
+ @property
+ def mixer(self):
+ if self._mixer is None:
+ mixer_refs = ActorRegistry.get_by_class(BaseMixer)
+ assert len(mixer_refs) == 1, 'Expected exactly one running mixer.'
+ self._mixer = mixer_refs[0].proxy()
+ return self._mixer
+
+ def _get_track_id(self, cp_track):
+ return '/com/mopidy/track/%d' % cp_track.cpid
+
+ def _get_cpid(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(u'%s.Get(%s, %s) called',
+ dbus.PROPERTIES_IFACE, repr(interface), repr(prop))
+ (getter, setter) = 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(u'%s.GetAll(%s) called',
+ dbus.PROPERTIES_IFACE, repr(interface))
+ getters = {}
+ for key, (getter, setter) 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(u'%s.Set(%s, %s, %s) called',
+ dbus.PROPERTIES_IFACE, repr(interface), repr(prop), repr(value))
+ getter, 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(u'%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(u'%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(u'%s.Quit called', ROOT_IFACE)
+ exit_process()
+
+
+ ### Root interface properties
+
+ def get_DesktopEntry(self):
+ return os.path.splitext(os.path.basename(settings.DESKTOP_FILE))[0]
+
+ def get_SupportedUriSchemes(self):
+ return dbus.Array(self.backend.uri_schemes.get(), signature='s')
+
+
+ ### Player interface methods
+
+ @dbus.service.method(dbus_interface=PLAYER_IFACE)
+ def Next(self):
+ logger.debug(u'%s.Next called', PLAYER_IFACE)
+ if not self.get_CanGoNext():
+ logger.debug(u'%s.Next not allowed', PLAYER_IFACE)
+ return
+ self.backend.playback.next().get()
+
+ @dbus.service.method(dbus_interface=PLAYER_IFACE)
+ def Previous(self):
+ logger.debug(u'%s.Previous called', PLAYER_IFACE)
+ if not self.get_CanGoPrevious():
+ logger.debug(u'%s.Previous not allowed', PLAYER_IFACE)
+ return
+ self.backend.playback.previous().get()
+
+ @dbus.service.method(dbus_interface=PLAYER_IFACE)
+ def Pause(self):
+ logger.debug(u'%s.Pause called', PLAYER_IFACE)
+ if not self.get_CanPause():
+ logger.debug(u'%s.Pause not allowed', PLAYER_IFACE)
+ return
+ self.backend.playback.pause().get()
+
+ @dbus.service.method(dbus_interface=PLAYER_IFACE)
+ def PlayPause(self):
+ logger.debug(u'%s.PlayPause called', PLAYER_IFACE)
+ if not self.get_CanPause():
+ logger.debug(u'%s.PlayPause not allowed', PLAYER_IFACE)
+ return
+ state = self.backend.playback.state.get()
+ if state == PlaybackController.PLAYING:
+ self.backend.playback.pause().get()
+ elif state == PlaybackController.PAUSED:
+ self.backend.playback.resume().get()
+ elif state == PlaybackController.STOPPED:
+ self.backend.playback.play().get()
+
+ @dbus.service.method(dbus_interface=PLAYER_IFACE)
+ def Stop(self):
+ logger.debug(u'%s.Stop called', PLAYER_IFACE)
+ if not self.get_CanControl():
+ logger.debug(u'%s.Stop not allowed', PLAYER_IFACE)
+ return
+ self.backend.playback.stop().get()
+
+ @dbus.service.method(dbus_interface=PLAYER_IFACE)
+ def Play(self):
+ logger.debug(u'%s.Play called', PLAYER_IFACE)
+ if not self.get_CanPlay():
+ logger.debug(u'%s.Play not allowed', PLAYER_IFACE)
+ return
+ state = self.backend.playback.state.get()
+ if state == PlaybackController.PAUSED:
+ self.backend.playback.resume().get()
+ else:
+ self.backend.playback.play().get()
+
+ @dbus.service.method(dbus_interface=PLAYER_IFACE)
+ def Seek(self, offset):
+ logger.debug(u'%s.Seek called', PLAYER_IFACE)
+ if not self.get_CanSeek():
+ logger.debug(u'%s.Seek not allowed', PLAYER_IFACE)
+ return
+ offset_in_milliseconds = offset // 1000
+ current_position = self.backend.playback.time_position.get()
+ new_position = current_position + offset_in_milliseconds
+ self.backend.playback.seek(new_position)
+
+ @dbus.service.method(dbus_interface=PLAYER_IFACE)
+ def SetPosition(self, track_id, position):
+ logger.debug(u'%s.SetPosition called', PLAYER_IFACE)
+ if not self.get_CanSeek():
+ logger.debug(u'%s.SetPosition not allowed', PLAYER_IFACE)
+ return
+ position = position // 1000
+ current_cp_track = self.backend.playback.current_cp_track.get()
+ if current_cp_track is None:
+ return
+ if track_id != self._get_track_id(current_cp_track):
+ return
+ if position < 0:
+ return
+ if current_cp_track.track.length < position:
+ return
+ self.backend.playback.seek(position)
+
+ @dbus.service.method(dbus_interface=PLAYER_IFACE)
+ def OpenUri(self, uri):
+ logger.debug(u'%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(u'%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.
+ uri_schemes = self.backend.uri_schemes.get()
+ if not any([uri.startswith(uri_scheme) for uri_scheme in uri_schemes]):
+ return
+ track = self.backend.library.lookup(uri).get()
+ if track is not None:
+ cp_track = self.backend.current_playlist.add(track).get()
+ self.backend.playback.play(cp_track)
+ else:
+ logger.debug(u'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(u'%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.backend.playback.state.get()
+ if state == PlaybackController.PLAYING:
+ return 'Playing'
+ elif state == PlaybackController.PAUSED:
+ return 'Paused'
+ elif state == PlaybackController.STOPPED:
+ return 'Stopped'
+
+ def get_LoopStatus(self):
+ repeat = self.backend.playback.repeat.get()
+ single = self.backend.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(u'Setting %s.LoopStatus not allowed', PLAYER_IFACE)
+ return
+ if value == 'None':
+ self.backend.playback.repeat = False
+ self.backend.playback.single = False
+ elif value == 'Track':
+ self.backend.playback.repeat = True
+ self.backend.playback.single = True
+ elif value == 'Playlist':
+ self.backend.playback.repeat = True
+ self.backend.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(u'Setting %s.Rate not allowed', PLAYER_IFACE)
+ return
+ if value == 0:
+ self.Pause()
+
+ def get_Shuffle(self):
+ return self.backend.playback.random.get()
+
+ def set_Shuffle(self, value):
+ if not self.get_CanControl():
+ logger.debug(u'Setting %s.Shuffle not allowed', PLAYER_IFACE)
+ return
+ if value:
+ self.backend.playback.random = True
+ else:
+ self.backend.playback.random = False
+
+ def get_Metadata(self):
+ current_cp_track = self.backend.playback.current_cp_track.get()
+ if current_cp_track is None:
+ return {'mpris:trackid': ''}
+ else:
+ (cpid, track) = current_cp_track
+ metadata = {'mpris:trackid': self._get_track_id(current_cp_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.track_no:
+ metadata['xesam:trackNumber'] = track.track_no
+ return dbus.Dictionary(metadata, signature='sv')
+
+ def get_Volume(self):
+ volume = self.mixer.volume.get()
+ if volume is not None:
+ return volume / 100.0
+
+ def set_Volume(self, value):
+ if not self.get_CanControl():
+ logger.debug(u'Setting %s.Volume not allowed', PLAYER_IFACE)
+ return
+ if value is None:
+ return
+ elif value < 0:
+ self.mixer.volume = 0
+ elif value > 1:
+ self.mixer.volume = 100
+ elif 0 <= value <= 1:
+ self.mixer.volume = int(value * 100)
+
+ def get_Position(self):
+ return self.backend.playback.time_position.get() * 1000
+
+ def get_CanGoNext(self):
+ if not self.get_CanControl():
+ return False
+ return (self.backend.playback.cp_track_at_next.get() !=
+ self.backend.playback.current_cp_track.get())
+
+ def get_CanGoPrevious(self):
+ if not self.get_CanControl():
+ return False
+ return (self.backend.playback.cp_track_at_previous.get() !=
+ self.backend.playback.current_cp_track.get())
+
+ def get_CanPlay(self):
+ if not self.get_CanControl():
+ return False
+ return (self.backend.playback.current_track.get() is not None
+ or self.backend.playback.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
diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py
index b43089e0..8781a4b2 100644
--- a/mopidy/gstreamer.py
+++ b/mopidy/gstreamer.py
@@ -13,15 +13,6 @@ from mopidy.backends.base import Backend
logger = logging.getLogger('mopidy.gstreamer')
-default_caps = gst.Caps("""
- audio/x-raw-int,
- endianness=(int)1234,
- channels=(int)2,
- width=(int)16,
- depth=(int)16,
- signed=(boolean)true,
- rate=(int)44100""")
-
class GStreamer(ThreadingActor):
"""
@@ -34,6 +25,15 @@ class GStreamer(ThreadingActor):
"""
def __init__(self):
+ super(GStreamer, self).__init__()
+ self._default_caps = gst.Caps("""
+ audio/x-raw-int,
+ endianness=(int)1234,
+ channels=(int)2,
+ width=(int)16,
+ depth=(int)16,
+ signed=(boolean)true,
+ rate=(int)44100""")
self._pipeline = None
self._source = None
self._uridecodebin = None
@@ -42,9 +42,6 @@ class GStreamer(ThreadingActor):
self._handlers = {}
def on_start(self):
- # **Warning:** :class:`GStreamer` requires
- # :class:`mopidy.utils.process.GObjectEventThread` to be running. This
- # is not enforced by :class:`GStreamer` itself.
self._setup_pipeline()
self._setup_outputs()
self._setup_message_processor()
@@ -78,12 +75,14 @@ class GStreamer(ThreadingActor):
def _on_new_source(self, element, pad):
self._source = element.get_property('source')
try:
- self._source.set_property('caps', default_caps)
+ self._source.set_property('caps', self._default_caps)
except TypeError:
pass
def _on_new_pad(self, source, pad, target_pad):
if not pad.is_linked():
+ if target_pad.is_linked():
+ target_pad.get_peer().unlink(target_pad)
pad.link(target_pad)
def _on_message(self, bus, message):
@@ -300,5 +299,3 @@ class GStreamer(ThreadingActor):
output.sync_state_with_parent() # Required to add to running pipe
gst.element_link_many(self._volume, output)
logger.debug('Output set to %s', output.get_name())
-
- # FIXME re-add disconnect / swap output code?
diff --git a/mopidy/listeners.py b/mopidy/listeners.py
index dfc5c60b..ee360bf3 100644
--- a/mopidy/listeners.py
+++ b/mopidy/listeners.py
@@ -1,3 +1,5 @@
+from pykka import registry
+
class BackendListener(object):
"""
Marker interface for recipients of events sent by the backend.
@@ -9,7 +11,46 @@ class BackendListener(object):
interested in all events.
"""
- def started_playing(self, track):
+ @staticmethod
+ def send(event, **kwargs):
+ """Helper to allow calling of backend listener events"""
+ # FIXME this should be updated once Pykka supports non-blocking calls
+ # on proxies or some similar solution.
+ registry.ActorRegistry.broadcast({
+ 'command': 'pykka_call',
+ 'attr_path': (event,),
+ 'args': [],
+ 'kwargs': kwargs,
+ }, target_class=BackendListener)
+
+ def track_playback_paused(self, track, time_position):
+ """
+ Called whenever track playback is paused.
+
+ *MAY* be implemented by actor.
+
+ :param track: the track that was playing when playback paused
+ :type track: :class:`mopidy.models.Track`
+ :param time_position: the time position in milliseconds
+ :type time_position: int
+ """
+ pass
+
+ def track_playback_resumed(self, track, time_position):
+ """
+ Called whenever track playback is resumed.
+
+ *MAY* be implemented by actor.
+
+ :param track: the track that was playing when playback resumed
+ :type track: :class:`mopidy.models.Track`
+ :param time_position: the time position in milliseconds
+ :type time_position: int
+ """
+ pass
+
+
+ def track_playback_started(self, track):
"""
Called whenever a new track starts playing.
@@ -20,9 +61,9 @@ class BackendListener(object):
"""
pass
- def stopped_playing(self, track, time_position):
+ def track_playback_ended(self, track, time_position):
"""
- Called whenever playback is stopped.
+ Called whenever playback of a track ends.
*MAY* be implemented by actor.
@@ -32,3 +73,44 @@ class BackendListener(object):
:type time_position: int
"""
pass
+
+ def playback_state_changed(self):
+ """
+ Called whenever playback state is changed.
+
+ *MAY* be implemented by actor.
+ """
+ pass
+
+ def playlist_changed(self):
+ """
+ Called whenever a playlist is changed.
+
+ *MAY* be implemented by actor.
+ """
+ pass
+
+ def options_changed(self):
+ """
+ Called whenever an option is changed.
+
+ *MAY* be implemented by actor.
+ """
+ pass
+
+ def volume_changed(self):
+ """
+ Called whenever the volume is changed.
+
+ *MAY* be implemented by actor.
+ """
+ pass
+
+ def seeked(self):
+ """
+ Called whenever the time position changes by an unexpected amount, e.g.
+ at seek to a new time position.
+
+ *MAY* be implemented by actor.
+ """
+ pass
diff --git a/mopidy/mixers/alsa.py b/mopidy/mixers/alsa.py
index ae4bd031..acb12e66 100644
--- a/mopidy/mixers/alsa.py
+++ b/mopidy/mixers/alsa.py
@@ -23,6 +23,7 @@ class AlsaMixer(ThreadingActor, BaseMixer):
"""
def __init__(self):
+ super(AlsaMixer, self).__init__()
self._mixer = None
def on_start(self):
diff --git a/mopidy/mixers/base.py b/mopidy/mixers/base.py
index ec3d8ae5..82783be1 100644
--- a/mopidy/mixers/base.py
+++ b/mopidy/mixers/base.py
@@ -1,4 +1,8 @@
-from mopidy import settings
+import logging
+
+from mopidy import listeners, settings
+
+logger = logging.getLogger('mopidy.mixers')
class BaseMixer(object):
"""
@@ -17,19 +21,31 @@ class BaseMixer(object):
Integer in range [0, 100]. :class:`None` if unknown. Values below 0 is
equal to 0. Values above 100 is equal to 100.
"""
+ if not hasattr(self, '_user_volume'):
+ self._user_volume = 0
volume = self.get_volume()
- if volume is None:
- return None
- return int(volume / self.amplification_factor)
+ if volume is None or not self.amplification_factor < 1:
+ return volume
+ else:
+ user_volume = int(volume / self.amplification_factor)
+ if (user_volume - 1) <= self._user_volume <= (user_volume + 1):
+ return self._user_volume
+ else:
+ return user_volume
@volume.setter
def volume(self, volume):
- volume = int(int(volume) * self.amplification_factor)
+ if not hasattr(self, '_user_volume'):
+ self._user_volume = 0
+ volume = int(volume)
if volume < 0:
volume = 0
elif volume > 100:
volume = 100
- self.set_volume(volume)
+ self._user_volume = volume
+ real_volume = int(volume * self.amplification_factor)
+ self.set_volume(real_volume)
+ self._trigger_volume_changed()
def get_volume(self):
"""
@@ -46,3 +62,7 @@ class BaseMixer(object):
*MUST be implemented by subclass.*
"""
raise NotImplementedError
+
+ def _trigger_volume_changed(self):
+ logger.debug(u'Triggering volume changed event')
+ listeners.BackendListener.send('volume_changed')
diff --git a/mopidy/mixers/denon.py b/mopidy/mixers/denon.py
index d0dc5f54..b0abbdb9 100644
--- a/mopidy/mixers/denon.py
+++ b/mopidy/mixers/denon.py
@@ -25,8 +25,9 @@ class DenonMixer(ThreadingActor, BaseMixer):
- :attr:`mopidy.settings.MIXER_EXT_PORT` -- Example: ``/dev/ttyUSB0``
"""
- def __init__(self, *args, **kwargs):
- self._device = kwargs.get('device', None)
+ def __init__(self, device=None):
+ super(DenonMixer, self).__init__()
+ self._device = device
self._levels = ['99'] + ["%(#)02d" % {'#': v} for v in range(0, 99)]
self._volume = 0
diff --git a/mopidy/mixers/dummy.py b/mopidy/mixers/dummy.py
index 23f96c4c..7262e83c 100644
--- a/mopidy/mixers/dummy.py
+++ b/mopidy/mixers/dummy.py
@@ -6,6 +6,7 @@ class DummyMixer(ThreadingActor, BaseMixer):
"""Mixer which just stores and reports the chosen volume."""
def __init__(self):
+ super(DummyMixer, self).__init__()
self._volume = None
def get_volume(self):
diff --git a/mopidy/mixers/gstreamer_software.py b/mopidy/mixers/gstreamer_software.py
index 523c3387..a38692db 100644
--- a/mopidy/mixers/gstreamer_software.py
+++ b/mopidy/mixers/gstreamer_software.py
@@ -8,6 +8,7 @@ class GStreamerSoftwareMixer(ThreadingActor, BaseMixer):
"""Mixer which uses GStreamer to control volume in software."""
def __init__(self):
+ super(GStreamerSoftwareMixer, self).__init__()
self.output = None
def on_start(self):
diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py
index 4dbf27be..78473308 100644
--- a/mopidy/mixers/nad.py
+++ b/mopidy/mixers/nad.py
@@ -37,6 +37,7 @@ class NadMixer(ThreadingActor, BaseMixer):
"""
def __init__(self):
+ super(NadMixer, self).__init__()
self._volume_cache = None
self._nad_talker = NadTalker.start().proxy()
@@ -71,6 +72,7 @@ class NadTalker(ThreadingActor):
_nad_volume = None
def __init__(self):
+ super(NadTalker, self).__init__()
self._device = None
def on_start(self):
diff --git a/mopidy/models.py b/mopidy/models.py
index ed323b71..9a508ba7 100644
--- a/mopidy/models.py
+++ b/mopidy/models.py
@@ -185,10 +185,6 @@ class Track(ImmutableObject):
self.__dict__['artists'] = frozenset(kwargs.pop('artists', []))
super(Track, self).__init__(*args, **kwargs)
- def mpd_format(self, *args, **kwargs):
- from mopidy.frontends.mpd import translator
- return translator.track_to_mpd_format(self, *args, **kwargs)
-
class Playlist(ImmutableObject):
"""
@@ -224,7 +220,3 @@ class Playlist(ImmutableObject):
def length(self):
"""The number of tracks in the playlist. Read-only."""
return len(self.tracks)
-
- def mpd_format(self, *args, **kwargs):
- from mopidy.frontends.mpd import translator
- return translator.playlist_to_mpd_format(self, *args, **kwargs)
diff --git a/mopidy/settings.py b/mopidy/settings.py
index 392c9ad7..a47b389d 100644
--- a/mopidy/settings.py
+++ b/mopidy/settings.py
@@ -4,7 +4,7 @@ Available settings and their default values.
.. warning::
Do *not* change settings directly in :mod:`mopidy.settings`. Instead, add a
- file called ``~/.mopidy/settings.py`` and redefine settings there.
+ file called ``~/.config/mopidy/settings.py`` and redefine settings there.
"""
#: List of playback backends to use. See :mod:`mopidy.backends` for all
@@ -49,6 +49,15 @@ DEBUG_LOG_FORMAT = u'%(levelname)-8s %(asctime)s' + \
#: DEBUG_LOG_FILENAME = u'mopidy.log'
DEBUG_LOG_FILENAME = u'mopidy.log'
+#: Location of the Mopidy .desktop file.
+#:
+#: Used by :mod:`mopidy.frontends.mpris`.
+#:
+#: Default::
+#:
+#: DESKTOP_FILE = u'/usr/share/applications/mopidy.desktop'
+DESKTOP_FILE = u'/usr/share/applications/mopidy.desktop'
+
#: List of server frontends to use.
#:
#: Default::
@@ -56,10 +65,12 @@ DEBUG_LOG_FILENAME = u'mopidy.log'
#: FRONTENDS = (
#: u'mopidy.frontends.mpd.MpdFrontend',
#: u'mopidy.frontends.lastfm.LastfmFrontend',
+#: u'mopidy.frontends.mpris.MprisFrontend',
#: )
FRONTENDS = (
u'mopidy.frontends.mpd.MpdFrontend',
u'mopidy.frontends.lastfm.LastfmFrontend',
+ u'mopidy.frontends.mpris.MprisFrontend',
)
#: Your `Last.fm `_ username.
@@ -78,8 +89,9 @@ LASTFM_PASSWORD = u''
#:
#: Default::
#:
-#: LOCAL_MUSIC_PATH = u'~/music'
-LOCAL_MUSIC_PATH = u'~/music'
+#: # Defaults to asking glib where music is stored, fallback is ~/music
+#: LOCAL_MUSIC_PATH = None
+LOCAL_MUSIC_PATH = None
#: Path to playlist folder with m3u files for local music.
#:
@@ -87,8 +99,8 @@ LOCAL_MUSIC_PATH = u'~/music'
#:
#: Default::
#:
-#: LOCAL_PLAYLIST_PATH = u'~/.mopidy/playlists'
-LOCAL_PLAYLIST_PATH = u'~/.mopidy/playlists'
+#: LOCAL_PLAYLIST_PATH = None # Implies $XDG_DATA_DIR/mopidy/playlists
+LOCAL_PLAYLIST_PATH = None
#: Path to tag cache for local music.
#:
@@ -96,8 +108,8 @@ LOCAL_PLAYLIST_PATH = u'~/.mopidy/playlists'
#:
#: Default::
#:
-#: LOCAL_TAG_CACHE_FILE = u'~/.mopidy/tag_cache'
-LOCAL_TAG_CACHE_FILE = u'~/.mopidy/tag_cache'
+#: LOCAL_TAG_CACHE_FILE = None # Implies $XDG_DATA_DIR/mopidy/tag_cache
+LOCAL_TAG_CACHE_FILE = None
#: Sound mixer to use. See :mod:`mopidy.mixers` for all available mixers.
#:
@@ -168,6 +180,11 @@ MPD_SERVER_PORT = 6600
#: Default: :class:`None`, which means no password required.
MPD_SERVER_PASSWORD = None
+#: The maximum number of concurrent connections the MPD server will accept.
+#:
+#: Default: 20
+MPD_SERVER_MAX_CONNECTIONS = 20
+
#: List of outputs to use. See :mod:`mopidy.outputs` for all available
#: backends
#:
@@ -233,7 +250,7 @@ SHOUTCAST_OUTPUT_ENCODER = u'lame mode=stereo bitrate=320'
#: Path to the Spotify cache.
#:
#: Used by :mod:`mopidy.backends.spotify`.
-SPOTIFY_CACHE_PATH = u'~/.mopidy/spotify_cache'
+SPOTIFY_CACHE_PATH = None
#: Your Spotify Premium username.
#:
diff --git a/mopidy/utils/__init__.py b/mopidy/utils/__init__.py
index 9d7532a0..00129cdd 100644
--- a/mopidy/utils/__init__.py
+++ b/mopidy/utils/__init__.py
@@ -1,3 +1,4 @@
+import locale
import logging
import os
import sys
@@ -29,3 +30,9 @@ def get_class(name):
except (ImportError, AttributeError):
raise ImportError("Couldn't load: %s" % name)
return class_object
+
+def locale_decode(bytestr):
+ try:
+ return unicode(bytestr)
+ except UnicodeError:
+ return str(bytestr).decode(locale.getpreferredencoding())
diff --git a/mopidy/utils/log.py b/mopidy/utils/log.py
index 03b85b48..0e5dfc29 100644
--- a/mopidy/utils/log.py
+++ b/mopidy/utils/log.py
@@ -20,7 +20,7 @@ def setup_console_logging(verbosity_level):
if verbosity_level == 0:
log_level = logging.WARNING
log_format = settings.CONSOLE_LOG_FORMAT
- elif verbosity_level == 2:
+ elif verbosity_level >= 2:
log_level = logging.DEBUG
log_format = settings.DEBUG_LOG_FORMAT
else:
@@ -33,6 +33,9 @@ def setup_console_logging(verbosity_level):
root = logging.getLogger('')
root.addHandler(handler)
+ if verbosity_level < 3:
+ logging.getLogger('pykka').setLevel(logging.INFO)
+
def setup_debug_logging_to_file():
formatter = logging.Formatter(settings.DEBUG_LOG_FORMAT)
handler = logging.handlers.RotatingFileHandler(
diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py
index 1dedf7d7..4b8a9ac9 100644
--- a/mopidy/utils/network.py
+++ b/mopidy/utils/network.py
@@ -1,23 +1,35 @@
+import errno
+import gobject
import logging
import re
import socket
+import threading
+
+from pykka import ActorDeadError
+from pykka.actor import ThreadingActor
+from pykka.registry import ActorRegistry
+
+from mopidy.utils import locale_decode
logger = logging.getLogger('mopidy.utils.server')
-def _try_ipv6_socket():
+class ShouldRetrySocketCall(Exception):
+ """Indicate that attempted socket call should be retried"""
+
+def try_ipv6_socket():
"""Determine if system really supports IPv6"""
if not socket.has_ipv6:
return False
try:
socket.socket(socket.AF_INET6).close()
return True
- except IOError, e:
+ except IOError as error:
logger.debug(u'Platform supports IPv6, but socket '
- 'creation failed, disabling: %s', e)
+ 'creation failed, disabling: %s', locale_decode(error))
return False
#: Boolean value that indicates if creating an IPv6 socket will succeed.
-has_ipv6 = _try_ipv6_socket()
+has_ipv6 = try_ipv6_socket()
def create_socket():
"""Create a TCP socket with or without IPv6 depending on system support"""
@@ -27,6 +39,7 @@ def create_socket():
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
else:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
return sock
def format_hostname(hostname):
@@ -34,3 +47,351 @@ def format_hostname(hostname):
if (has_ipv6 and re.match('\d+.\d+.\d+.\d+', hostname) is not None):
hostname = '::ffff:%s' % hostname
return hostname
+
+class Server(object):
+ """Setup listener and register it with gobject's event loop."""
+
+ def __init__(self, host, port, protocol, max_connections=5, timeout=30):
+ self.protocol = protocol
+ self.max_connections = max_connections
+ self.timeout = timeout
+ self.server_socket = self.create_server_socket(host, port)
+
+ self.register_server_socket(self.server_socket.fileno())
+
+ def create_server_socket(self, host, port):
+ sock = create_socket()
+ sock.setblocking(False)
+ sock.bind((host, port))
+ sock.listen(1)
+ return sock
+
+ def register_server_socket(self, fileno):
+ gobject.io_add_watch(fileno, gobject.IO_IN, self.handle_connection)
+
+ def handle_connection(self, fd, flags):
+ try:
+ sock, addr = self.accept_connection()
+ except ShouldRetrySocketCall:
+ return True
+
+ if self.maximum_connections_exceeded():
+ self.reject_connection(sock, addr)
+ else:
+ self.init_connection(sock, addr)
+ return True
+
+ def accept_connection(self):
+ try:
+ return self.server_socket.accept()
+ except socket.error as e:
+ if e.errno in (errno.EAGAIN, errno.EINTR):
+ raise ShouldRetrySocketCall
+ raise
+
+ def maximum_connections_exceeded(self):
+ return (self.max_connections is not None and
+ self.number_of_connections() >= self.max_connections)
+
+ def number_of_connections(self):
+ return len(ActorRegistry.get_by_class(self.protocol))
+
+ def reject_connection(self, sock, addr):
+ # FIXME provide more context in logging?
+ logger.warning(u'Rejected connection from [%s]:%s', addr[0], addr[1])
+ try:
+ sock.close()
+ except socket.error:
+ pass
+
+ def init_connection(self, sock, addr):
+ Connection(self.protocol, sock, addr, self.timeout)
+
+
+class Connection(object):
+ # NOTE: the callback code is _not_ run in the actor's thread, but in the
+ # same one as the event loop. If code in the callbacks blocks, the rest of
+ # gobject code will likely be blocked as well...
+ #
+ # Also note that source_remove() return values are ignored on purpose, a
+ # false return value would only tell us that what we thought was registered
+ # is already gone, there is really nothing more we can do.
+
+ def __init__(self, protocol, sock, addr, timeout):
+ sock.setblocking(False)
+
+ self.host, self.port = addr[:2] # IPv6 has larger addr
+
+ self.sock = sock
+ self.protocol = protocol
+ self.timeout = timeout
+
+ self.send_lock = threading.Lock()
+ self.send_buffer = ''
+
+ self.stopping = False
+
+ self.recv_id = None
+ self.send_id = None
+ self.timeout_id = None
+
+ self.actor_ref = self.protocol.start(self)
+
+ self.enable_recv()
+ self.enable_timeout()
+
+ def stop(self, reason, level=logging.DEBUG):
+ if self.stopping:
+ logger.log(level, 'Already stopping: %s' % reason)
+ return
+ else:
+ self.stopping = True
+
+ logger.log(level, reason)
+
+ try:
+ self.actor_ref.stop()
+ except ActorDeadError:
+ pass
+
+ self.disable_timeout()
+ self.disable_recv()
+ self.disable_send()
+
+ try:
+ self.sock.close()
+ except socket.error:
+ pass
+
+ def queue_send(self, data):
+ """Try to send data to client exactly as is and queue rest."""
+ self.send_lock.acquire(True)
+ self.send_buffer = self.send(self.send_buffer + data)
+ self.send_lock.release()
+ if self.send_buffer:
+ self.enable_send()
+
+ def send(self, data):
+ """Send data to client, return any unsent data."""
+ try:
+ sent = self.sock.send(data)
+ return data[sent:]
+ except socket.error as e:
+ if e.errno in (errno.EWOULDBLOCK, errno.EINTR):
+ return data
+ self.stop(u'Unexpected client error: %s' % e)
+ return ''
+
+ def enable_timeout(self):
+ """Reactivate timeout mechanism."""
+ if self.timeout <= 0:
+ return
+
+ self.disable_timeout()
+ self.timeout_id = gobject.timeout_add_seconds(
+ self.timeout, self.timeout_callback)
+
+ def disable_timeout(self):
+ """Deactivate timeout mechanism."""
+ if self.timeout_id is None:
+ return
+ gobject.source_remove(self.timeout_id)
+ self.timeout_id = None
+
+ def enable_recv(self):
+ if self.recv_id is not None:
+ return
+
+ try:
+ self.recv_id = gobject.io_add_watch(self.sock.fileno(),
+ gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP,
+ self.recv_callback)
+ except socket.error as e:
+ self.stop(u'Problem with connection: %s' % e)
+
+ def disable_recv(self):
+ if self.recv_id is None:
+ return
+ gobject.source_remove(self.recv_id)
+ self.recv_id = None
+
+ def enable_send(self):
+ if self.send_id is not None:
+ return
+
+ try:
+ self.send_id = gobject.io_add_watch(self.sock.fileno(),
+ gobject.IO_OUT | gobject.IO_ERR | gobject.IO_HUP,
+ self.send_callback)
+ except socket.error as e:
+ self.stop(u'Problem with connection: %s' % e)
+
+ def disable_send(self):
+ if self.send_id is None:
+ return
+
+ gobject.source_remove(self.send_id)
+ self.send_id = None
+
+ def recv_callback(self, fd, flags):
+ if flags & (gobject.IO_ERR | gobject.IO_HUP):
+ self.stop(u'Bad client flags: %s' % flags)
+ return True
+
+ try:
+ data = self.sock.recv(4096)
+ except socket.error as e:
+ if e.errno not in (errno.EWOULDBLOCK, errno.EINTR):
+ self.stop(u'Unexpected client error: %s' % e)
+ return True
+
+ if not data:
+ self.stop(u'Client most likely disconnected.')
+ return True
+
+ try:
+ self.actor_ref.send_one_way({'received': data})
+ except ActorDeadError:
+ self.stop(u'Actor is dead.')
+
+ return True
+
+ def send_callback(self, fd, flags):
+ if flags & (gobject.IO_ERR | gobject.IO_HUP):
+ self.stop(u'Bad client flags: %s' % flags)
+ return True
+
+ # If with can't get the lock, simply try again next time socket is
+ # ready for sending.
+ if not self.send_lock.acquire(False):
+ return True
+
+ try:
+ self.send_buffer = self.send(self.send_buffer)
+ if not self.send_buffer:
+ self.disable_send()
+ finally:
+ self.send_lock.release()
+
+ return True
+
+ def timeout_callback(self):
+ self.stop(u'Client timeout out after %s seconds' % self.timeout)
+ return False
+
+
+class LineProtocol(ThreadingActor):
+ """
+ Base class for handling line based protocols.
+
+ Takes care of receiving new data from server's client code, decoding and
+ then splitting data along line boundaries.
+ """
+
+ #: Line terminator to use for outputed lines.
+ terminator = '\n'
+
+ #: Regex to use for spliting lines, will be set compiled version of its
+ #: own value, or to ``terminator``s value if it is not set itself.
+ delimeter = None
+
+ #: What encoding to expect incomming data to be in, can be :class:`None`.
+ encoding = 'utf-8'
+
+ def __init__(self, connection):
+ super(LineProtocol, self).__init__()
+ self.connection = connection
+ self.prevent_timeout = False
+ self.recv_buffer = ''
+
+ if self.delimeter:
+ self.delimeter = re.compile(self.delimeter)
+ else:
+ self.delimeter = re.compile(self.terminator)
+
+ @property
+ def host(self):
+ return self.connection.host
+
+ @property
+ def port(self):
+ return self.connection.port
+
+ def on_line_received(self, line):
+ """
+ Called whenever a new line is found.
+
+ Should be implemented by subclasses.
+ """
+ raise NotImplementedError
+
+ def on_receive(self, message):
+ """Handle messages with new data from server."""
+ if 'received' not in message:
+ return
+
+ self.connection.disable_timeout()
+ self.recv_buffer += message['received']
+
+ for line in self.parse_lines():
+ line = self.decode(line)
+ if line is not None:
+ self.on_line_received(line)
+
+ if not self.prevent_timeout:
+ self.connection.enable_timeout()
+
+ def on_stop(self):
+ """Ensure that cleanup when actor stops."""
+ self.connection.stop(u'Actor is shutting down.')
+
+ def parse_lines(self):
+ """Consume new data and yield any lines found."""
+ while re.search(self.terminator, self.recv_buffer):
+ line, self.recv_buffer = self.delimeter.split(
+ self.recv_buffer, 1)
+ yield line
+
+ def encode(self, line):
+ """
+ Handle encoding of line.
+
+ Can be overridden by subclasses to change encoding behaviour.
+ """
+ try:
+ return line.encode(self.encoding)
+ except UnicodeError:
+ logger.warning(u'Stopping actor due to encode problem, data '
+ 'supplied by client was not valid %s', self.encoding)
+ self.stop()
+
+ def decode(self, line):
+ """
+ Handle decoding of line.
+
+ Can be overridden by subclasses to change decoding behaviour.
+ """
+ try:
+ return line.decode(self.encoding)
+ except UnicodeError:
+ logger.warning(u'Stopping actor due to decode problem, data '
+ 'supplied by client was not valid %s', self.encoding)
+ self.stop()
+
+ def join_lines(self, lines):
+ if not lines:
+ return u''
+ return self.terminator.join(lines) + self.terminator
+
+ def send_lines(self, lines):
+ """
+ Send array of lines to client via connection.
+
+ Join lines using the terminator that is set for this class, encode it
+ and send it to the client.
+ """
+ if not lines:
+ return
+
+ data = self.join_lines(lines)
+ self.connection.queue_send(self.encode(data))
diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py
index 540cb4fa..5d99ac12 100644
--- a/mopidy/utils/path.py
+++ b/mopidy/utils/path.py
@@ -8,9 +8,12 @@ logger = logging.getLogger('mopidy.utils.path')
def get_or_create_folder(folder):
folder = os.path.expanduser(folder)
- if not os.path.isdir(folder):
+ if os.path.isfile(folder):
+ raise OSError('A file with the same name as the desired ' \
+ 'dir, "%s", already exists.' % folder)
+ elif not os.path.isdir(folder):
logger.info(u'Creating dir %s', folder)
- os.mkdir(folder, 0755)
+ os.makedirs(folder, 0755)
return folder
def get_or_create_file(filename):
@@ -60,6 +63,7 @@ def find_files(path):
yield filename
# pylint: enable = W0612
+# FIXME replace with mock usage in tests.
class Mtime(object):
def __init__(self):
self.fake = None
diff --git a/mopidy/utils/process.py b/mopidy/utils/process.py
index 758f8943..80d850fe 100644
--- a/mopidy/utils/process.py
+++ b/mopidy/utils/process.py
@@ -3,9 +3,6 @@ import signal
import thread
import threading
-import gobject
-gobject.threads_init()
-
from pykka import ActorDeadError
from pykka.registry import ActorRegistry
@@ -68,25 +65,3 @@ class BaseThread(threading.Thread):
def run_inside_try(self):
raise NotImplementedError
-
-
-class GObjectEventThread(BaseThread):
- """
- A GObject event loop which is shared by all Mopidy components that uses
- libraries that need a GObject event loop, like GStreamer and D-Bus.
-
- Should be started by Mopidy's core and used by
- :mod:`mopidy.output.gstreamer`, :mod:`mopidy.frontend.mpris`, etc.
- """
-
- def __init__(self):
- super(GObjectEventThread, self).__init__()
- self.name = u'GObjectEventThread'
- self.loop = None
-
- def run_inside_try(self):
- self.loop = gobject.MainLoop().run()
-
- def destroy(self):
- self.loop.quit()
- super(GObjectEventThread, self).destroy()
diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py
index 3f7593af..ff449a61 100644
--- a/mopidy/utils/settings.py
+++ b/mopidy/utils/settings.py
@@ -7,7 +7,7 @@ import os
from pprint import pformat
import sys
-from mopidy import SettingsError
+from mopidy import SettingsError, SETTINGS_PATH, SETTINGS_FILE
from mopidy.utils.log import indent
logger = logging.getLogger('mopidy.utils.settings')
@@ -20,11 +20,9 @@ class SettingsProxy(object):
self.runtime = {}
def _get_local_settings(self):
- dotdir = os.path.expanduser(u'~/.mopidy/')
- settings_file = os.path.join(dotdir, u'settings.py')
- if not os.path.isfile(settings_file):
+ if not os.path.isfile(SETTINGS_FILE):
return {}
- sys.path.insert(0, dotdir)
+ sys.path.insert(0, SETTINGS_PATH)
# pylint: disable = F0401
import settings as local_settings_module
# pylint: enable = F0401
diff --git a/pylintrc b/pylintrc
index d2f84b77..98e10416 100644
--- a/pylintrc
+++ b/pylintrc
@@ -18,6 +18,7 @@
# R0921 - Abstract class not referenced
# W0141 - Used builtin function '%s'
# W0142 - Used * or ** magic
+# W0511 - TODO, FIXME and XXX in the code
# W0613 - Unused argument %r
#
-disable = C0103,C0111,E0102,E0202,E1101,R0201,R0801,R0903,R0904,R0921,W0141,W0142,W0613
+disable = C0103,C0111,E0102,E0202,E1101,R0201,R0801,R0903,R0904,R0921,W0141,W0142,W0511,W0613
diff --git a/requirements/tests.txt b/requirements/tests.txt
index f8cf2eb3..e24edd3c 100644
--- a/requirements/tests.txt
+++ b/requirements/tests.txt
@@ -1,4 +1,6 @@
coverage
-mock
+mock >= 0.7
nose
tox
+unittest2
+yappi
diff --git a/setup.py b/setup.py
index a8cf8ed1..ae6cc699 100644
--- a/setup.py
+++ b/setup.py
@@ -6,9 +6,13 @@ from distutils.core import setup
from distutils.command.install_data import install_data
from distutils.command.install import INSTALL_SCHEMES
import os
+import re
import sys
-from mopidy import get_version
+def get_version():
+ init_py = open('mopidy/__init__.py').read()
+ metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", init_py))
+ return metadata['version']
class osx_install_data(install_data):
# On MacOS, the platform-specific lib dir is
diff --git a/tests/__init__.py b/tests/__init__.py
index 1d4d2e3d..833ff239 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,24 +1,41 @@
import os
+import sys
-try: # 2.7
- # pylint: disable = E0611,F0401
- from unittest.case import SkipTest
- # pylint: enable = E0611,F0401
-except ImportError:
- try: # Nose
- from nose.plugins.skip import SkipTest
- except ImportError: # Failsafe
- class SkipTest(Exception):
- pass
+if sys.version_info < (2, 7):
+ import unittest2 as unittest
+else:
+ import unittest
from mopidy import settings
# Nuke any local settings to ensure same test env all over
settings.local.clear()
+
def path_to_data_dir(name):
path = os.path.dirname(__file__)
path = os.path.join(path, 'data')
path = os.path.abspath(path)
return os.path.join(path, name)
+
+class IsA(object):
+ def __init__(self, klass):
+ self.klass = klass
+
+ def __eq__(self, rhs):
+ try:
+ return isinstance(rhs, self.klass)
+ except TypeError:
+ return type(rhs) == type(self.klass)
+
+ def __ne__(self, rhs):
+ return not self.__eq__(rhs)
+
+ def __repr__(self):
+ return str(self.klass)
+
+
+any_int = IsA(int)
+any_str = IsA(str)
+any_unicode = IsA(unicode)
diff --git a/tests/__main__.py b/tests/__main__.py
index e2bb3e72..69113580 100644
--- a/tests/__main__.py
+++ b/tests/__main__.py
@@ -1,4 +1,8 @@
import nose
+import yappi
-if __name__ == '__main__':
+try:
+ yappi.start()
nose.main()
+finally:
+ yappi.print_stats()
diff --git a/tests/backends/base/current_playlist.py b/tests/backends/base/current_playlist.py
index b84391af..e99cd56c 100644
--- a/tests/backends/base/current_playlist.py
+++ b/tests/backends/base/current_playlist.py
@@ -1,12 +1,12 @@
import mock
-import multiprocessing
import random
-from mopidy.models import Playlist, Track
+from mopidy.models import CpTrack, Playlist, Track
from mopidy.gstreamer import GStreamer
from tests.backends.base import populate_playlist
+
class CurrentPlaylistControllerTest(object):
tracks = []
@@ -18,6 +18,13 @@ class CurrentPlaylistControllerTest(object):
assert len(self.tracks) == 3, 'Need three tracks to run tests.'
+ def test_length(self):
+ self.assertEqual(0, len(self.controller.cp_tracks))
+ self.assertEqual(0, self.controller.length)
+ self.controller.append(self.tracks)
+ self.assertEqual(3, len(self.controller.cp_tracks))
+ self.assertEqual(3, self.controller.length)
+
def test_add(self):
for track in self.tracks:
cp_track = self.controller.add(track)
@@ -136,6 +143,18 @@ class CurrentPlaylistControllerTest(object):
self.assertEqual(self.playback.state, self.playback.STOPPED)
self.assertEqual(self.playback.current_track, None)
+ def test_index_returns_index_of_track(self):
+ cp_tracks = []
+ for track in self.tracks:
+ cp_tracks.append(self.controller.add(track))
+ self.assertEquals(0, self.controller.index(cp_tracks[0]))
+ self.assertEquals(1, self.controller.index(cp_tracks[1]))
+ self.assertEquals(2, self.controller.index(cp_tracks[2]))
+
+ def test_index_raises_value_error_if_item_not_found(self):
+ test = lambda: self.controller.index(CpTrack(0, Track()))
+ self.assertRaises(ValueError, test)
+
@populate_playlist
def test_move_single(self):
self.controller.move(0, 0, 2)
@@ -241,6 +260,18 @@ class CurrentPlaylistControllerTest(object):
self.assertEqual(self.tracks[0], shuffled_tracks[0])
self.assertEqual(set(self.tracks), set(shuffled_tracks))
+ @populate_playlist
+ 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_playlist
+ 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_appending_nothing(self):
version = self.controller.version
self.controller.append([])
diff --git a/tests/backends/base/library.py b/tests/backends/base/library.py
index 2a3de730..4b3ef5c0 100644
--- a/tests/backends/base/library.py
+++ b/tests/backends/base/library.py
@@ -1,6 +1,7 @@
from mopidy.models import Playlist, Track, Album, Artist
-from tests import SkipTest, path_to_data_dir
+from tests import unittest, path_to_data_dir
+
class LibraryControllerTest(object):
artists = [Artist(name='artist1'), Artist(name='artist2'), Artist()]
@@ -20,11 +21,13 @@ class LibraryControllerTest(object):
def test_refresh(self):
self.library.refresh()
+ @unittest.SkipTest
def test_refresh_uri(self):
- raise SkipTest
+ pass
+ @unittest.SkipTest
def test_refresh_missing_uri(self):
- raise SkipTest
+ pass
def test_lookup(self):
track = self.library.lookup(self.tracks[0].uri)
diff --git a/tests/backends/base/playback.py b/tests/backends/base/playback.py
index 2d455225..40c49709 100644
--- a/tests/backends/base/playback.py
+++ b/tests/backends/base/playback.py
@@ -1,16 +1,16 @@
import mock
-import multiprocessing
import random
import time
from mopidy.models import Track
from mopidy.gstreamer import GStreamer
-from tests import SkipTest
+from tests import unittest
from tests.backends.base import populate_playlist
# TODO Test 'playlist repeat', e.g. repeat=1,single=0
+
class PlaybackControllerTest(object):
tracks = []
@@ -520,7 +520,7 @@ class PlaybackControllerTest(object):
self.assert_(wrapper.called)
- @SkipTest # Blocks for 10ms
+ @unittest.SkipTest # Blocks for 10ms
@populate_playlist
def test_end_of_track_callback_gets_called(self):
self.playback.play()
@@ -555,7 +555,7 @@ class PlaybackControllerTest(object):
@populate_playlist
def test_pause_when_stopped(self):
self.playback.pause()
- self.assertEqual(self.playback.state, self.playback.STOPPED)
+ self.assertEqual(self.playback.state, self.playback.PAUSED)
@populate_playlist
def test_pause_when_playing(self):
@@ -599,7 +599,7 @@ class PlaybackControllerTest(object):
self.playback.pause()
self.assertEqual(self.playback.resume(), None)
- @SkipTest # Uses sleep and might not work with LocalBackend
+ @unittest.SkipTest # Uses sleep and might not work with LocalBackend
@populate_playlist
def test_resume_continues_from_right_position(self):
self.playback.play()
@@ -668,7 +668,7 @@ class PlaybackControllerTest(object):
self.playback.seek(0)
self.assertEqual(self.playback.state, self.playback.PLAYING)
- @SkipTest
+ @unittest.SkipTest
@populate_playlist
def test_seek_beyond_end_of_song(self):
# FIXME need to decide return value
@@ -688,7 +688,7 @@ class PlaybackControllerTest(object):
self.playback.seek(self.current_playlist.tracks[-1].length * 100)
self.assertEqual(self.playback.state, self.playback.STOPPED)
- @SkipTest
+ @unittest.SkipTest
@populate_playlist
def test_seek_beyond_start_of_song(self):
# FIXME need to decide return value
@@ -741,7 +741,7 @@ class PlaybackControllerTest(object):
self.assertEqual(self.playback.time_position, 0)
- @SkipTest # Uses sleep and does might not work with LocalBackend
+ @unittest.SkipTest # Uses sleep and does might not work with LocalBackend
@populate_playlist
def test_time_position_when_playing(self):
self.playback.play()
@@ -750,7 +750,7 @@ class PlaybackControllerTest(object):
second = self.playback.time_position
self.assert_(second > first, '%s - %s' % (first, second))
- @SkipTest # Uses sleep
+ @unittest.SkipTest # Uses sleep
@populate_playlist
def test_time_position_when_paused(self):
self.playback.play()
diff --git a/tests/backends/base/stored_playlists.py b/tests/backends/base/stored_playlists.py
index 839d5bed..54315e62 100644
--- a/tests/backends/base/stored_playlists.py
+++ b/tests/backends/base/stored_playlists.py
@@ -5,7 +5,8 @@ import tempfile
from mopidy import settings
from mopidy.models import Playlist
-from tests import SkipTest, path_to_data_dir
+from tests import unittest, path_to_data_dir
+
class StoredPlaylistsControllerTest(object):
def setUp(self):
@@ -78,11 +79,13 @@ class StoredPlaylistsControllerTest(object):
except LookupError as e:
self.assertEqual(u'"name=c" match no playlists', e[0])
+ @unittest.SkipTest
def test_lookup(self):
- raise SkipTest
+ pass
+ @unittest.SkipTest
def test_refresh(self):
- raise SkipTest
+ pass
def test_rename(self):
playlist = self.stored.create('test')
@@ -100,5 +103,6 @@ class StoredPlaylistsControllerTest(object):
self.stored.save(playlist)
self.assert_(playlist in self.stored.playlists)
+ @unittest.SkipTest
def test_playlist_with_unknown_track(self):
- raise SkipTest
+ pass
diff --git a/tests/backends/events_test.py b/tests/backends/events_test.py
index 44529e90..d761676d 100644
--- a/tests/backends/events_test.py
+++ b/tests/backends/events_test.py
@@ -1,45 +1,53 @@
-import threading
-import unittest
+import mock
-from pykka.actor import ThreadingActor
from pykka.registry import ActorRegistry
from mopidy.backends.dummy import DummyBackend
from mopidy.listeners import BackendListener
from mopidy.models import Track
+from tests import unittest
+
+
+@mock.patch.object(BackendListener, 'send')
class BackendEventsTest(unittest.TestCase):
def setUp(self):
- self.events = {
- 'started_playing': threading.Event(),
- 'stopped_playing': threading.Event(),
- }
self.backend = DummyBackend.start().proxy()
- self.listener = DummyBackendListener.start(self.events).proxy()
def tearDown(self):
ActorRegistry.stop_all()
- def test_play_sends_started_playing_event(self):
- self.backend.current_playlist.add([Track(uri='a')])
+ def test_pause_sends_track_playback_paused_event(self, send):
+ self.backend.current_playlist.add(Track(uri='a'))
+ self.backend.playback.play().get()
+ send.reset_mock()
+ self.backend.playback.pause().get()
+ self.assertEqual(send.call_args[0][0], 'track_playback_paused')
+
+ def test_resume_sends_track_playback_resumed(self, send):
+ self.backend.current_playlist.add(Track(uri='a'))
self.backend.playback.play()
- self.events['started_playing'].wait(timeout=1)
- self.assertTrue(self.events['started_playing'].is_set())
+ self.backend.playback.pause().get()
+ send.reset_mock()
+ self.backend.playback.resume().get()
+ self.assertEqual(send.call_args[0][0], 'track_playback_resumed')
- def test_stop_sends_stopped_playing_event(self):
- self.backend.current_playlist.add([Track(uri='a')])
- self.backend.playback.play()
- self.backend.playback.stop()
- self.events['stopped_playing'].wait(timeout=1)
- self.assertTrue(self.events['stopped_playing'].is_set())
+ def test_play_sends_track_playback_started_event(self, send):
+ self.backend.current_playlist.add(Track(uri='a'))
+ send.reset_mock()
+ self.backend.playback.play().get()
+ self.assertEqual(send.call_args[0][0], 'track_playback_started')
+ def test_stop_sends_track_playback_ended_event(self, send):
+ self.backend.current_playlist.add(Track(uri='a'))
+ self.backend.playback.play().get()
+ send.reset_mock()
+ self.backend.playback.stop().get()
+ self.assertEqual(send.call_args_list[0][0][0], 'track_playback_ended')
-class DummyBackendListener(ThreadingActor, BackendListener):
- def __init__(self, events):
- self.events = events
-
- def started_playing(self, track):
- self.events['started_playing'].set()
-
- def stopped_playing(self, track, time_position):
- self.events['stopped_playing'].set()
+ def test_seek_sends_seeked_event(self, send):
+ self.backend.current_playlist.add(Track(uri='a', length=40000))
+ self.backend.playback.play().get()
+ send.reset_mock()
+ self.backend.playback.seek(1000).get()
+ self.assertEqual(send.call_args[0][0], 'seeked')
diff --git a/tests/backends/local/current_playlist_test.py b/tests/backends/local/current_playlist_test.py
index 6f72d7d5..a475a6fd 100644
--- a/tests/backends/local/current_playlist_test.py
+++ b/tests/backends/local/current_playlist_test.py
@@ -1,18 +1,16 @@
-import unittest
-
-# FIXME Our Windows build server does not support GStreamer yet
import sys
-if sys.platform == 'win32':
- from tests import SkipTest
- raise SkipTest
from mopidy import settings
from mopidy.backends.local import LocalBackend
from mopidy.models import Track
+from tests import unittest
from tests.backends.base.current_playlist import CurrentPlaylistControllerTest
from tests.backends.local import generate_song
+
+@unittest.skipIf(sys.platform == 'win32',
+ 'Our Windows build server does not support GStreamer yet')
class LocalCurrentPlaylistControllerTest(CurrentPlaylistControllerTest,
unittest.TestCase):
diff --git a/tests/backends/local/library_test.py b/tests/backends/local/library_test.py
index 68ab22e9..046e747a 100644
--- a/tests/backends/local/library_test.py
+++ b/tests/backends/local/library_test.py
@@ -1,17 +1,14 @@
-import unittest
-
-# FIXME Our Windows build server does not support GStreamer yet
import sys
-if sys.platform == 'win32':
- from tests import SkipTest
- raise SkipTest
from mopidy import settings
from mopidy.backends.local import LocalBackend
-from tests import path_to_data_dir
+from tests import unittest, path_to_data_dir
from tests.backends.base.library import LibraryControllerTest
+
+@unittest.skipIf(sys.platform == 'win32',
+ 'Our Windows build server does not support GStreamer yet')
class LocalLibraryControllerTest(LibraryControllerTest, unittest.TestCase):
backend_class = LocalBackend
diff --git a/tests/backends/local/playback_test.py b/tests/backends/local/playback_test.py
index 6aec680f..788fe33c 100644
--- a/tests/backends/local/playback_test.py
+++ b/tests/backends/local/playback_test.py
@@ -1,20 +1,17 @@
-import unittest
-
-# FIXME Our Windows build server does not support GStreamer yet
import sys
-if sys.platform == 'win32':
- from tests import SkipTest
- raise SkipTest
from mopidy import settings
from mopidy.backends.local import LocalBackend
from mopidy.models import Track
from mopidy.utils.path import path_to_uri
-from tests import path_to_data_dir
+from tests import unittest, path_to_data_dir
from tests.backends.base.playback import PlaybackControllerTest
from tests.backends.local import generate_song
+
+@unittest.skipIf(sys.platform == 'win32',
+ 'Our Windows build server does not support GStreamer yet')
class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase):
backend_class = LocalBackend
tracks = [Track(uri=generate_song(i), length=4464)
diff --git a/tests/backends/local/stored_playlists_test.py b/tests/backends/local/stored_playlists_test.py
index b426e9ce..56be92c4 100644
--- a/tests/backends/local/stored_playlists_test.py
+++ b/tests/backends/local/stored_playlists_test.py
@@ -1,24 +1,19 @@
-import unittest
import os
-
-from tests import SkipTest
-
-# FIXME Our Windows build server does not support GStreamer yet
import sys
-if sys.platform == 'win32':
- raise SkipTest
from mopidy import settings
from mopidy.backends.local import LocalBackend
-from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Playlist, Track
from mopidy.utils.path import path_to_uri
-from tests import path_to_data_dir
-from tests.backends.base.stored_playlists import \
- StoredPlaylistsControllerTest
+from tests import unittest, path_to_data_dir
+from tests.backends.base.stored_playlists import (
+ StoredPlaylistsControllerTest)
from tests.backends.local import generate_song
+
+@unittest.skipIf(sys.platform == 'win32',
+ 'Our Windows build server does not support GStreamer yet')
class LocalStoredPlaylistsControllerTest(StoredPlaylistsControllerTest,
unittest.TestCase):
@@ -77,14 +72,18 @@ class LocalStoredPlaylistsControllerTest(StoredPlaylistsControllerTest,
self.assertEqual('test', self.stored.playlists[0].name)
self.assertEqual(track.uri, self.stored.playlists[0].tracks[0].uri)
+ @unittest.SkipTest
def test_santitising_of_playlist_filenames(self):
- raise SkipTest
+ pass
+ @unittest.SkipTest
def test_playlist_folder_is_createad(self):
- raise SkipTest
+ pass
+ @unittest.SkipTest
def test_create_sets_playlist_uri(self):
- raise SkipTest
+ pass
+ @unittest.SkipTest
def test_save_sets_playlist_uri(self):
- raise SkipTest
+ pass
diff --git a/tests/backends/local/translator_test.py b/tests/backends/local/translator_test.py
index a4e9f317..1dceb737 100644
--- a/tests/backends/local/translator_test.py
+++ b/tests/backends/local/translator_test.py
@@ -2,13 +2,12 @@
import os
import tempfile
-import unittest
from mopidy.utils.path import path_to_uri
from mopidy.backends.local.translator import parse_m3u, parse_mpd_tag_cache
from mopidy.models import Track, Artist, Album
-from tests import SkipTest, path_to_data_dir
+from tests import unittest, path_to_data_dir
song1_path = path_to_data_dir('song1.mp3')
song2_path = path_to_data_dir('song2.mp3')
@@ -17,6 +16,9 @@ song1_uri = path_to_uri(song1_path)
song2_uri = path_to_uri(song2_path)
encoded_uri = path_to_uri(encoded_path)
+# FIXME use mock instead of tempfile.NamedTemporaryFile
+
+
class M3UToUriTest(unittest.TestCase):
def test_empty_file(self):
uris = parse_m3u(path_to_data_dir('empty.m3u'))
@@ -127,9 +129,10 @@ class MPDTagCacheToTracksTest(unittest.TestCase):
self.assertEqual(track, list(tracks)[0])
+ @unittest.SkipTest
def test_misencoded_cache(self):
# FIXME not sure if this can happen
- raise SkipTest
+ pass
def test_cache_with_blank_track_info(self):
tracks = parse_mpd_tag_cache(path_to_data_dir('blank_tag_cache'),
diff --git a/tests/frontends/mpd/audio_output_test.py b/tests/frontends/mpd/audio_output_test.py
deleted file mode 100644
index 82d9e203..00000000
--- a/tests/frontends/mpd/audio_output_test.py
+++ /dev/null
@@ -1,30 +0,0 @@
-import unittest
-
-from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd.dispatcher import MpdDispatcher
-from mopidy.mixers.dummy import DummyMixer
-
-class AudioOutputHandlerTest(unittest.TestCase):
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.dispatcher = MpdDispatcher()
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
-
- def test_enableoutput(self):
- result = self.dispatcher.handle_request(u'enableoutput "0"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_disableoutput(self):
- result = self.dispatcher.handle_request(u'disableoutput "0"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_outputs(self):
- result = self.dispatcher.handle_request(u'outputs')
- self.assert_(u'outputid: 0' in result)
- self.assert_(u'outputname: None' in result)
- self.assert_(u'outputenabled: 1' in result)
- self.assert_(u'OK' in result)
diff --git a/tests/frontends/mpd/command_list_test.py b/tests/frontends/mpd/command_list_test.py
deleted file mode 100644
index 8fd4c828..00000000
--- a/tests/frontends/mpd/command_list_test.py
+++ /dev/null
@@ -1,63 +0,0 @@
-import unittest
-
-from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd import dispatcher
-from mopidy.mixers.dummy import DummyMixer
-
-class CommandListsTest(unittest.TestCase):
- def setUp(self):
- self.b = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.dispatcher = dispatcher.MpdDispatcher()
-
- def tearDown(self):
- self.b.stop().get()
- self.mixer.stop().get()
-
- def test_command_list_begin(self):
- result = self.dispatcher.handle_request(u'command_list_begin')
- self.assertEquals(result, [])
-
- def test_command_list_end(self):
- self.dispatcher.handle_request(u'command_list_begin')
- result = self.dispatcher.handle_request(u'command_list_end')
- self.assert_(u'OK' in result)
-
- def test_command_list_end_without_start_first_is_an_unknown_command(self):
- result = self.dispatcher.handle_request(u'command_list_end')
- self.assertEquals(result[0],
- u'ACK [5@0] {} unknown command "command_list_end"')
-
- def test_command_list_with_ping(self):
- self.dispatcher.handle_request(u'command_list_begin')
- self.assertEqual([], self.dispatcher.command_list)
- self.assertEqual(False, self.dispatcher.command_list_ok)
- self.dispatcher.handle_request(u'ping')
- self.assert_(u'ping' in self.dispatcher.command_list)
- result = self.dispatcher.handle_request(u'command_list_end')
- self.assert_(u'OK' in result)
- self.assertEqual(False, self.dispatcher.command_list)
-
- def test_command_list_with_error_returns_ack_with_correct_index(self):
- self.dispatcher.handle_request(u'command_list_begin')
- self.dispatcher.handle_request(u'play') # Known command
- self.dispatcher.handle_request(u'paly') # Unknown command
- result = self.dispatcher.handle_request(u'command_list_end')
- self.assertEqual(len(result), 1, result)
- self.assertEqual(result[0], u'ACK [5@1] {} unknown command "paly"')
-
- def test_command_list_ok_begin(self):
- result = self.dispatcher.handle_request(u'command_list_ok_begin')
- self.assertEquals(result, [])
-
- def test_command_list_ok_with_ping(self):
- self.dispatcher.handle_request(u'command_list_ok_begin')
- self.assertEqual([], self.dispatcher.command_list)
- self.assertEqual(True, self.dispatcher.command_list_ok)
- self.dispatcher.handle_request(u'ping')
- self.assert_(u'ping' in self.dispatcher.command_list)
- result = self.dispatcher.handle_request(u'command_list_end')
- self.assert_(u'list_OK' in result)
- self.assert_(u'OK' in result)
- self.assertEqual(False, self.dispatcher.command_list)
- self.assertEqual(False, self.dispatcher.command_list_ok)
diff --git a/tests/frontends/mpd/connection_test.py b/tests/frontends/mpd/connection_test.py
deleted file mode 100644
index bc995a5e..00000000
--- a/tests/frontends/mpd/connection_test.py
+++ /dev/null
@@ -1,53 +0,0 @@
-import mock
-import unittest
-
-from mopidy import settings
-from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd.dispatcher import MpdDispatcher
-from mopidy.frontends.mpd.session import MpdSession
-from mopidy.mixers.dummy import DummyMixer
-
-class ConnectionHandlerTest(unittest.TestCase):
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.session = mock.Mock(spec=MpdSession)
- self.dispatcher = MpdDispatcher(session=self.session)
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
- settings.runtime.clear()
-
- def test_close_closes_the_client_connection(self):
- result = self.dispatcher.handle_request(u'close')
- self.assert_(self.session.close.called,
- u'Should call close() on MpdSession')
- self.assert_(u'OK' in result)
-
- def test_empty_request(self):
- result = self.dispatcher.handle_request(u'')
- self.assert_(u'OK' in result)
-
- def test_kill(self):
- result = self.dispatcher.handle_request(u'kill')
- self.assert_(u'ACK [4@0] {kill} you don\'t have permission for "kill"' in result)
-
- def test_valid_password_is_accepted(self):
- settings.MPD_SERVER_PASSWORD = u'topsecret'
- result = self.dispatcher.handle_request(u'password "topsecret"')
- self.assert_(u'OK' in result)
-
- def test_invalid_password_is_not_accepted(self):
- settings.MPD_SERVER_PASSWORD = u'topsecret'
- result = self.dispatcher.handle_request(u'password "secret"')
- self.assert_(u'ACK [3@0] {password} incorrect password' in result)
-
- def test_any_password_is_not_accepted_when_password_check_turned_off(self):
- settings.MPD_SERVER_PASSWORD = None
- result = self.dispatcher.handle_request(u'password "secret"')
- self.assert_(u'ACK [3@0] {password} incorrect password' in result)
-
- def test_ping(self):
- result = self.dispatcher.handle_request(u'ping')
- self.assert_(u'OK' in result)
diff --git a/tests/frontends/mpd/dispatcher_test.py b/tests/frontends/mpd/dispatcher_test.py
index 7708ce31..bfa7c548 100644
--- a/tests/frontends/mpd/dispatcher_test.py
+++ b/tests/frontends/mpd/dispatcher_test.py
@@ -1,11 +1,12 @@
-import unittest
-
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.frontends.mpd.exceptions import MpdAckError
from mopidy.frontends.mpd.protocol import request_handlers, handle_request
from mopidy.mixers.dummy import DummyMixer
+from tests import unittest
+
+
class MpdDispatcherTest(unittest.TestCase):
def setUp(self):
self.backend = DummyBackend.start().proxy()
diff --git a/tests/frontends/mpd/exception_test.py b/tests/frontends/mpd/exception_test.py
index df2cd65e..2ea3fe62 100644
--- a/tests/frontends/mpd/exception_test.py
+++ b/tests/frontends/mpd/exception_test.py
@@ -1,8 +1,9 @@
-import unittest
-
from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdPermissionError,
MpdUnknownCommand, MpdSystemError, MpdNotImplemented)
+from tests import unittest
+
+
class MpdExceptionsTest(unittest.TestCase):
def test_key_error_wrapped_in_mpd_ack_error(self):
try:
diff --git a/tests/frontends/mpd/music_db_test.py b/tests/frontends/mpd/music_db_test.py
deleted file mode 100644
index 3793db9e..00000000
--- a/tests/frontends/mpd/music_db_test.py
+++ /dev/null
@@ -1,412 +0,0 @@
-import unittest
-
-from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd.dispatcher import MpdDispatcher
-from mopidy.mixers.dummy import DummyMixer
-
-class MusicDatabaseHandlerTest(unittest.TestCase):
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.dispatcher = MpdDispatcher()
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
-
- def test_count(self):
- result = self.dispatcher.handle_request(u'count "tag" "needle"')
- self.assert_(u'songs: 0' in result)
- self.assert_(u'playtime: 0' in result)
- self.assert_(u'OK' in result)
-
- def test_findadd(self):
- result = self.dispatcher.handle_request(u'findadd "album" "what"')
- self.assert_(u'OK' in result)
-
- def test_listall(self):
- result = self.dispatcher.handle_request(
- u'listall "file:///dev/urandom"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_listallinfo(self):
- result = self.dispatcher.handle_request(
- u'listallinfo "file:///dev/urandom"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_lsinfo_without_path_returns_same_as_listplaylists(self):
- lsinfo_result = self.dispatcher.handle_request(u'lsinfo')
- listplaylists_result = self.dispatcher.handle_request(u'listplaylists')
- self.assertEqual(lsinfo_result, listplaylists_result)
-
- def test_lsinfo_with_empty_path_returns_same_as_listplaylists(self):
- lsinfo_result = self.dispatcher.handle_request(u'lsinfo ""')
- listplaylists_result = self.dispatcher.handle_request(u'listplaylists')
- self.assertEqual(lsinfo_result, listplaylists_result)
-
- def test_lsinfo_for_root_returns_same_as_listplaylists(self):
- lsinfo_result = self.dispatcher.handle_request(u'lsinfo "/"')
- listplaylists_result = self.dispatcher.handle_request(u'listplaylists')
- self.assertEqual(lsinfo_result, listplaylists_result)
-
- def test_update_without_uri(self):
- result = self.dispatcher.handle_request(u'update')
- self.assert_(u'OK' in result)
- self.assert_(u'updating_db: 0' in result)
-
- def test_update_with_uri(self):
- result = self.dispatcher.handle_request(u'update "file:///dev/urandom"')
- self.assert_(u'OK' in result)
- self.assert_(u'updating_db: 0' in result)
-
- def test_rescan_without_uri(self):
- result = self.dispatcher.handle_request(u'rescan')
- self.assert_(u'OK' in result)
- self.assert_(u'updating_db: 0' in result)
-
- def test_rescan_with_uri(self):
- result = self.dispatcher.handle_request(u'rescan "file:///dev/urandom"')
- self.assert_(u'OK' in result)
- self.assert_(u'updating_db: 0' in result)
-
-
-class MusicDatabaseFindTest(unittest.TestCase):
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.dispatcher = MpdDispatcher()
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
-
- def test_find_album(self):
- result = self.dispatcher.handle_request(u'find "album" "what"')
- self.assert_(u'OK' in result)
-
- def test_find_album_without_quotes(self):
- result = self.dispatcher.handle_request(u'find album "what"')
- self.assert_(u'OK' in result)
-
- def test_find_artist(self):
- result = self.dispatcher.handle_request(u'find "artist" "what"')
- self.assert_(u'OK' in result)
-
- def test_find_artist_without_quotes(self):
- result = self.dispatcher.handle_request(u'find artist "what"')
- self.assert_(u'OK' in result)
-
- def test_find_title(self):
- result = self.dispatcher.handle_request(u'find "title" "what"')
- self.assert_(u'OK' in result)
-
- def test_find_title_without_quotes(self):
- result = self.dispatcher.handle_request(u'find title "what"')
- self.assert_(u'OK' in result)
-
- def test_find_date(self):
- result = self.dispatcher.handle_request(u'find "date" "2002-01-01"')
- self.assert_(u'OK' in result)
-
- def test_find_date_without_quotes(self):
- result = self.dispatcher.handle_request(u'find date "2002-01-01"')
- self.assert_(u'OK' in result)
-
- def test_find_date_with_capital_d_and_incomplete_date(self):
- result = self.dispatcher.handle_request(u'find Date "2005"')
- self.assert_(u'OK' in result)
-
- def test_find_else_should_fail(self):
-
- result = self.dispatcher.handle_request(u'find "somethingelse" "what"')
- self.assertEqual(result[0], u'ACK [2@0] {find} incorrect arguments')
-
- def test_find_album_and_artist(self):
- result = self.dispatcher.handle_request(
- u'find album "album_what" artist "artist_what"')
- self.assert_(u'OK' in result)
-
-
-class MusicDatabaseListTest(unittest.TestCase):
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.dispatcher = MpdDispatcher()
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
-
- def test_list_foo_returns_ack(self):
- result = self.dispatcher.handle_request(u'list "foo"')
- self.assertEqual(result[0],
- u'ACK [2@0] {list} incorrect arguments')
-
- ### Artist
-
- def test_list_artist_with_quotes(self):
- result = self.dispatcher.handle_request(u'list "artist"')
- self.assert_(u'OK' in result)
-
- def test_list_artist_without_quotes(self):
- result = self.dispatcher.handle_request(u'list artist')
- self.assert_(u'OK' in result)
-
- def test_list_artist_without_quotes_and_capitalized(self):
- result = self.dispatcher.handle_request(u'list Artist')
- self.assert_(u'OK' in result)
-
- def test_list_artist_with_query_of_one_token(self):
- result = self.dispatcher.handle_request(u'list "artist" "anartist"')
- self.assertEqual(result[0],
- u'ACK [2@0] {list} should be "Album" for 3 arguments')
-
- def test_list_artist_with_unknown_field_in_query_returns_ack(self):
- result = self.dispatcher.handle_request(u'list "artist" "foo" "bar"')
- self.assertEqual(result[0],
- u'ACK [2@0] {list} not able to parse args')
-
- def test_list_artist_by_artist(self):
- result = self.dispatcher.handle_request(
- u'list "artist" "artist" "anartist"')
- self.assert_(u'OK' in result)
-
- def test_list_artist_by_album(self):
- result = self.dispatcher.handle_request(
- u'list "artist" "album" "analbum"')
- self.assert_(u'OK' in result)
-
- def test_list_artist_by_full_date(self):
- result = self.dispatcher.handle_request(
- u'list "artist" "date" "2001-01-01"')
- self.assert_(u'OK' in result)
-
- def test_list_artist_by_year(self):
- result = self.dispatcher.handle_request(
- u'list "artist" "date" "2001"')
- self.assert_(u'OK' in result)
-
- def test_list_artist_by_genre(self):
- result = self.dispatcher.handle_request(
- u'list "artist" "genre" "agenre"')
- self.assert_(u'OK' in result)
-
- def test_list_artist_by_artist_and_album(self):
- result = self.dispatcher.handle_request(
- u'list "artist" "artist" "anartist" "album" "analbum"')
- self.assert_(u'OK' in result)
-
- ### Album
-
- def test_list_album_with_quotes(self):
- result = self.dispatcher.handle_request(u'list "album"')
- self.assert_(u'OK' in result)
-
- def test_list_album_without_quotes(self):
- result = self.dispatcher.handle_request(u'list album')
- self.assert_(u'OK' in result)
-
- def test_list_album_without_quotes_and_capitalized(self):
- result = self.dispatcher.handle_request(u'list Album')
- self.assert_(u'OK' in result)
-
- def test_list_album_with_artist_name(self):
- result = self.dispatcher.handle_request(u'list "album" "anartist"')
- self.assert_(u'OK' in result)
-
- def test_list_album_by_artist(self):
- result = self.dispatcher.handle_request(
- u'list "album" "artist" "anartist"')
- self.assert_(u'OK' in result)
-
- def test_list_album_by_album(self):
- result = self.dispatcher.handle_request(
- u'list "album" "album" "analbum"')
- self.assert_(u'OK' in result)
-
- def test_list_album_by_full_date(self):
- result = self.dispatcher.handle_request(
- u'list "album" "date" "2001-01-01"')
- self.assert_(u'OK' in result)
-
- def test_list_album_by_year(self):
- result = self.dispatcher.handle_request(
- u'list "album" "date" "2001"')
- self.assert_(u'OK' in result)
-
- def test_list_album_by_genre(self):
- result = self.dispatcher.handle_request(
- u'list "album" "genre" "agenre"')
- self.assert_(u'OK' in result)
-
- def test_list_album_by_artist_and_album(self):
- result = self.dispatcher.handle_request(
- u'list "album" "artist" "anartist" "album" "analbum"')
- self.assert_(u'OK' in result)
-
- ### Date
-
- def test_list_date_with_quotes(self):
- result = self.dispatcher.handle_request(u'list "date"')
- self.assert_(u'OK' in result)
-
- def test_list_date_without_quotes(self):
- result = self.dispatcher.handle_request(u'list date')
- self.assert_(u'OK' in result)
-
- def test_list_date_without_quotes_and_capitalized(self):
- result = self.dispatcher.handle_request(u'list Date')
- self.assert_(u'OK' in result)
-
- def test_list_date_with_query_of_one_token(self):
- result = self.dispatcher.handle_request(u'list "date" "anartist"')
- self.assertEqual(result[0],
- u'ACK [2@0] {list} should be "Album" for 3 arguments')
-
- def test_list_date_by_artist(self):
- result = self.dispatcher.handle_request(
- u'list "date" "artist" "anartist"')
- self.assert_(u'OK' in result)
-
- def test_list_date_by_album(self):
- result = self.dispatcher.handle_request(
- u'list "date" "album" "analbum"')
- self.assert_(u'OK' in result)
-
- def test_list_date_by_full_date(self):
- result = self.dispatcher.handle_request(
- u'list "date" "date" "2001-01-01"')
- self.assert_(u'OK' in result)
-
- def test_list_date_by_year(self):
- result = self.dispatcher.handle_request(u'list "date" "date" "2001"')
- self.assert_(u'OK' in result)
-
- def test_list_date_by_genre(self):
- result = self.dispatcher.handle_request(u'list "date" "genre" "agenre"')
- self.assert_(u'OK' in result)
-
- def test_list_date_by_artist_and_album(self):
- result = self.dispatcher.handle_request(
- u'list "date" "artist" "anartist" "album" "analbum"')
- self.assert_(u'OK' in result)
-
- ### Genre
-
- def test_list_genre_with_quotes(self):
- result = self.dispatcher.handle_request(u'list "genre"')
- self.assert_(u'OK' in result)
-
- def test_list_genre_without_quotes(self):
- result = self.dispatcher.handle_request(u'list genre')
- self.assert_(u'OK' in result)
-
- def test_list_genre_without_quotes_and_capitalized(self):
- result = self.dispatcher.handle_request(u'list Genre')
- self.assert_(u'OK' in result)
-
- def test_list_genre_with_query_of_one_token(self):
- result = self.dispatcher.handle_request(u'list "genre" "anartist"')
- self.assertEqual(result[0],
- u'ACK [2@0] {list} should be "Album" for 3 arguments')
-
- def test_list_genre_by_artist(self):
- result = self.dispatcher.handle_request(
- u'list "genre" "artist" "anartist"')
- self.assert_(u'OK' in result)
-
- def test_list_genre_by_album(self):
- result = self.dispatcher.handle_request(
- u'list "genre" "album" "analbum"')
- self.assert_(u'OK' in result)
-
- def test_list_genre_by_full_date(self):
- result = self.dispatcher.handle_request(
- u'list "genre" "date" "2001-01-01"')
- self.assert_(u'OK' in result)
-
- def test_list_genre_by_year(self):
- result = self.dispatcher.handle_request(
- u'list "genre" "date" "2001"')
- self.assert_(u'OK' in result)
-
- def test_list_genre_by_genre(self):
- result = self.dispatcher.handle_request(
- u'list "genre" "genre" "agenre"')
- self.assert_(u'OK' in result)
-
- def test_list_genre_by_artist_and_album(self):
- result = self.dispatcher.handle_request(
- u'list "genre" "artist" "anartist" "album" "analbum"')
- self.assert_(u'OK' in result)
-
-
-class MusicDatabaseSearchTest(unittest.TestCase):
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.dispatcher = MpdDispatcher()
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
-
- def test_search_album(self):
- result = self.dispatcher.handle_request(u'search "album" "analbum"')
- self.assert_(u'OK' in result)
-
- def test_search_album_without_quotes(self):
- result = self.dispatcher.handle_request(u'search album "analbum"')
- self.assert_(u'OK' in result)
-
- def test_search_artist(self):
- result = self.dispatcher.handle_request(u'search "artist" "anartist"')
- self.assert_(u'OK' in result)
-
- def test_search_artist_without_quotes(self):
- result = self.dispatcher.handle_request(u'search artist "anartist"')
- self.assert_(u'OK' in result)
-
- def test_search_filename(self):
- result = self.dispatcher.handle_request(
- u'search "filename" "afilename"')
- self.assert_(u'OK' in result)
-
- def test_search_filename_without_quotes(self):
- result = self.dispatcher.handle_request(u'search filename "afilename"')
- self.assert_(u'OK' in result)
-
- def test_search_title(self):
- result = self.dispatcher.handle_request(u'search "title" "atitle"')
- self.assert_(u'OK' in result)
-
- def test_search_title_without_quotes(self):
- result = self.dispatcher.handle_request(u'search title "atitle"')
- self.assert_(u'OK' in result)
-
- def test_search_any(self):
- result = self.dispatcher.handle_request(u'search "any" "anything"')
- self.assert_(u'OK' in result)
-
- def test_search_any_without_quotes(self):
- result = self.dispatcher.handle_request(u'search any "anything"')
- self.assert_(u'OK' in result)
-
- def test_search_date(self):
- result = self.dispatcher.handle_request(u'search "date" "2002-01-01"')
- self.assert_(u'OK' in result)
-
- def test_search_date_without_quotes(self):
- result = self.dispatcher.handle_request(u'search date "2002-01-01"')
- self.assert_(u'OK' in result)
-
- def test_search_date_with_capital_d_and_incomplete_date(self):
- result = self.dispatcher.handle_request(u'search Date "2005"')
- self.assert_(u'OK' in result)
-
- def test_search_else_should_fail(self):
- result = self.dispatcher.handle_request(
- u'search "sometype" "something"')
- self.assertEqual(result[0], u'ACK [2@0] {search} incorrect arguments')
-
-
diff --git a/tests/frontends/mpd/protocol/__init__.py b/tests/frontends/mpd/protocol/__init__.py
new file mode 100644
index 00000000..b54906be
--- /dev/null
+++ b/tests/frontends/mpd/protocol/__init__.py
@@ -0,0 +1,62 @@
+import mock
+
+from mopidy import settings
+from mopidy.backends import dummy as backend
+from mopidy.frontends import mpd
+from mopidy.mixers import dummy as mixer
+
+from tests import unittest
+
+
+class MockConnection(mock.Mock):
+ def __init__(self, *args, **kwargs):
+ super(MockConnection, self).__init__(*args, **kwargs)
+ self.host = mock.sentinel.host
+ self.port = mock.sentinel.port
+ self.response = []
+
+ def queue_send(self, data):
+ lines = (line for line in data.split('\n') if line)
+ self.response.extend(lines)
+
+
+class BaseTestCase(unittest.TestCase):
+ def setUp(self):
+ self.backend = backend.DummyBackend.start().proxy()
+ self.mixer = mixer.DummyMixer.start().proxy()
+
+ self.connection = MockConnection()
+ self.session = mpd.MpdSession(self.connection)
+ self.dispatcher = self.session.dispatcher
+ self.context = self.dispatcher.context
+
+ def tearDown(self):
+ self.backend.stop().get()
+ self.mixer.stop().get()
+ settings.runtime.clear()
+
+ def sendRequest(self, request):
+ self.connection.response = []
+ request = '%s\n' % request.encode('utf-8')
+ self.session.on_receive({'received': request})
+ return self.connection.response
+
+ def assertNoResponse(self):
+ self.assertEqual([], self.connection.response)
+
+ def assertInResponse(self, value):
+ self.assert_(value in self.connection.response, u'Did not find %s '
+ 'in %s' % (repr(value), repr(self.connection.response)))
+
+ def assertOnceInResponse(self, value):
+ matched = len([r for r in self.connection.response if r == value])
+ self.assertEqual(1, matched, 'Expected to find %s once in %s' %
+ (repr(value), repr(self.connection.response)))
+
+ def assertNotInResponse(self, value):
+ self.assert_(value not in self.connection.response, u'Found %s in %s' %
+ (repr(value), repr(self.connection.response)))
+
+ def assertEqualResponse(self, value):
+ self.assertEqual(1, len(self.connection.response))
+ self.assertEqual(value, self.connection.response[0])
diff --git a/tests/frontends/mpd/protocol/audio_output_test.py b/tests/frontends/mpd/protocol/audio_output_test.py
new file mode 100644
index 00000000..3bb8dce8
--- /dev/null
+++ b/tests/frontends/mpd/protocol/audio_output_test.py
@@ -0,0 +1,18 @@
+from tests.frontends.mpd import protocol
+
+
+class AudioOutputHandlerTest(protocol.BaseTestCase):
+ def test_enableoutput(self):
+ self.sendRequest(u'enableoutput "0"')
+ self.assertInResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_disableoutput(self):
+ self.sendRequest(u'disableoutput "0"')
+ self.assertInResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_outputs(self):
+ self.sendRequest(u'outputs')
+ self.assertInResponse(u'outputid: 0')
+ self.assertInResponse(u'outputname: None')
+ self.assertInResponse(u'outputenabled: 1')
+ self.assertInResponse(u'OK')
diff --git a/tests/frontends/mpd/authentication_test.py b/tests/frontends/mpd/protocol/authentication_test.py
similarity index 53%
rename from tests/frontends/mpd/authentication_test.py
rename to tests/frontends/mpd/protocol/authentication_test.py
index d795d726..20422f5b 100644
--- a/tests/frontends/mpd/authentication_test.py
+++ b/tests/frontends/mpd/protocol/authentication_test.py
@@ -1,63 +1,62 @@
-import mock
-import unittest
-
from mopidy import settings
-from mopidy.frontends.mpd.dispatcher import MpdDispatcher
-from mopidy.frontends.mpd.session import MpdSession
-class AuthenticationTest(unittest.TestCase):
- def setUp(self):
- self.session = mock.Mock(spec=MpdSession)
- self.dispatcher = MpdDispatcher(session=self.session)
+from tests.frontends.mpd import protocol
- def tearDown(self):
- settings.runtime.clear()
+class AuthenticationTest(protocol.BaseTestCase):
def test_authentication_with_valid_password_is_accepted(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
- response = self.dispatcher.handle_request(u'password "topsecret"')
+
+ self.sendRequest(u'password "topsecret"')
self.assertTrue(self.dispatcher.authenticated)
- self.assert_(u'OK' in response)
+ self.assertInResponse(u'OK')
def test_authentication_with_invalid_password_is_not_accepted(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
- response = self.dispatcher.handle_request(u'password "secret"')
+
+ self.sendRequest(u'password "secret"')
self.assertFalse(self.dispatcher.authenticated)
- self.assert_(u'ACK [3@0] {password} incorrect password' in response)
+ self.assertEqualResponse(u'ACK [3@0] {password} incorrect password')
def test_authentication_with_anything_when_password_check_turned_off(self):
settings.MPD_SERVER_PASSWORD = None
- response = self.dispatcher.handle_request(u'any request at all')
+
+ self.sendRequest(u'any request at all')
self.assertTrue(self.dispatcher.authenticated)
- self.assert_('ACK [5@0] {} unknown command "any"' in response)
+ self.assertEqualResponse('ACK [5@0] {} unknown command "any"')
def test_anything_when_not_authenticated_should_fail(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
- response = self.dispatcher.handle_request(u'any request at all')
+
+ self.sendRequest(u'any request at all')
self.assertFalse(self.dispatcher.authenticated)
- self.assert_(
- u'ACK [4@0] {any} you don\'t have permission for "any"' in response)
+ self.assertEqualResponse(
+ u'ACK [4@0] {any} you don\'t have permission for "any"')
def test_close_is_allowed_without_authentication(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
- response = self.dispatcher.handle_request(u'close')
+
+ self.sendRequest(u'close')
self.assertFalse(self.dispatcher.authenticated)
- self.assert_(u'OK' in response)
+ self.assertInResponse(u'OK')
def test_commands_is_allowed_without_authentication(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
- response = self.dispatcher.handle_request(u'commands')
+
+ self.sendRequest(u'commands')
self.assertFalse(self.dispatcher.authenticated)
- self.assert_(u'OK' in response)
+ self.assertInResponse(u'OK')
def test_notcommands_is_allowed_without_authentication(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
- response = self.dispatcher.handle_request(u'notcommands')
+
+ self.sendRequest(u'notcommands')
self.assertFalse(self.dispatcher.authenticated)
- self.assert_(u'OK' in response)
+ self.assertInResponse(u'OK')
def test_ping_is_allowed_without_authentication(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
- response = self.dispatcher.handle_request(u'ping')
+
+ self.sendRequest(u'ping')
self.assertFalse(self.dispatcher.authenticated)
- self.assert_(u'OK' in response)
+ self.assertInResponse(u'OK')
diff --git a/tests/frontends/mpd/protocol/command_list_test.py b/tests/frontends/mpd/protocol/command_list_test.py
new file mode 100644
index 00000000..a81725ad
--- /dev/null
+++ b/tests/frontends/mpd/protocol/command_list_test.py
@@ -0,0 +1,54 @@
+from tests.frontends.mpd import protocol
+
+
+class CommandListsTest(protocol.BaseTestCase):
+ def test_command_list_begin(self):
+ response = self.sendRequest(u'command_list_begin')
+ self.assertEquals([], response)
+
+ def test_command_list_end(self):
+ self.sendRequest(u'command_list_begin')
+ self.sendRequest(u'command_list_end')
+ self.assertInResponse(u'OK')
+
+ def test_command_list_end_without_start_first_is_an_unknown_command(self):
+ self.sendRequest(u'command_list_end')
+ self.assertEqualResponse(
+ u'ACK [5@0] {} unknown command "command_list_end"')
+
+ def test_command_list_with_ping(self):
+ self.sendRequest(u'command_list_begin')
+ self.assertEqual([], self.dispatcher.command_list)
+ self.assertEqual(False, self.dispatcher.command_list_ok)
+ self.sendRequest(u'ping')
+ self.assert_(u'ping' in self.dispatcher.command_list)
+ self.sendRequest(u'command_list_end')
+ self.assertInResponse(u'OK')
+ self.assertEqual(False, self.dispatcher.command_list)
+
+ def test_command_list_with_error_returns_ack_with_correct_index(self):
+ self.sendRequest(u'command_list_begin')
+ self.sendRequest(u'play') # Known command
+ self.sendRequest(u'paly') # Unknown command
+ self.sendRequest(u'command_list_end')
+ self.assertEqualResponse(u'ACK [5@1] {} unknown command "paly"')
+
+ def test_command_list_ok_begin(self):
+ response = self.sendRequest(u'command_list_ok_begin')
+ self.assertEquals([], response)
+
+ def test_command_list_ok_with_ping(self):
+ self.sendRequest(u'command_list_ok_begin')
+ self.assertEqual([], self.dispatcher.command_list)
+ self.assertEqual(True, self.dispatcher.command_list_ok)
+ self.sendRequest(u'ping')
+ self.assert_(u'ping' in self.dispatcher.command_list)
+ self.sendRequest(u'command_list_end')
+ self.assertInResponse(u'list_OK')
+ self.assertInResponse(u'OK')
+ self.assertEqual(False, self.dispatcher.command_list)
+ self.assertEqual(False, self.dispatcher.command_list_ok)
+
+ # FIXME this should also include the special handling of idle within a
+ # command list. That is that once a idle/noidle command is found inside a
+ # commad list, the rest of the list seems to be ignored.
diff --git a/tests/frontends/mpd/protocol/connection_test.py b/tests/frontends/mpd/protocol/connection_test.py
new file mode 100644
index 00000000..cd08313f
--- /dev/null
+++ b/tests/frontends/mpd/protocol/connection_test.py
@@ -0,0 +1,44 @@
+from mock import patch
+
+from mopidy import settings
+
+from tests.frontends.mpd import protocol
+
+
+class ConnectionHandlerTest(protocol.BaseTestCase):
+ def test_close_closes_the_client_connection(self):
+ with patch.object(self.session, 'close') as close_mock:
+ response = self.sendRequest(u'close')
+ close_mock.assertEqualResponsecalled_once_with()
+ self.assertEqualResponse(u'OK')
+
+ def test_empty_request(self):
+ self.sendRequest(u'')
+ self.assertEqualResponse(u'OK')
+
+ self.sendRequest(u' ')
+ self.assertEqualResponse(u'OK')
+
+ def test_kill(self):
+ self.sendRequest(u'kill')
+ self.assertEqualResponse(
+ u'ACK [4@0] {kill} you don\'t have permission for "kill"')
+
+ def test_valid_password_is_accepted(self):
+ settings.MPD_SERVER_PASSWORD = u'topsecret'
+ self.sendRequest(u'password "topsecret"')
+ self.assertEqualResponse(u'OK')
+
+ def test_invalid_password_is_not_accepted(self):
+ settings.MPD_SERVER_PASSWORD = u'topsecret'
+ self.sendRequest(u'password "secret"')
+ self.assertEqualResponse(u'ACK [3@0] {password} incorrect password')
+
+ def test_any_password_is_not_accepted_when_password_check_turned_off(self):
+ settings.MPD_SERVER_PASSWORD = None
+ self.sendRequest(u'password "secret"')
+ self.assertEqualResponse(u'ACK [3@0] {password} incorrect password')
+
+ def test_ping(self):
+ self.sendRequest(u'ping')
+ self.assertEqualResponse(u'OK')
diff --git a/tests/frontends/mpd/current_playlist_test.py b/tests/frontends/mpd/protocol/current_playlist_test.py
similarity index 59%
rename from tests/frontends/mpd/current_playlist_test.py
rename to tests/frontends/mpd/protocol/current_playlist_test.py
index c7f47429..321fc6ee 100644
--- a/tests/frontends/mpd/current_playlist_test.py
+++ b/tests/frontends/mpd/protocol/current_playlist_test.py
@@ -1,20 +1,9 @@
-import unittest
-
-from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd.dispatcher import MpdDispatcher
-from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Track
-class CurrentPlaylistHandlerTest(unittest.TestCase):
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.dispatcher = MpdDispatcher()
+from tests.frontends.mpd import protocol
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
+class CurrentPlaylistHandlerTest(protocol.BaseTestCase):
def test_add(self):
needle = Track(uri='dummy://foo')
self.backend.library.provider.dummy_library = [
@@ -22,21 +11,21 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
- result = self.dispatcher.handle_request(u'add "dummy://foo"')
- self.assertEqual(len(result), 1)
- self.assertEqual(result[0], u'OK')
+
+ self.sendRequest(u'add "dummy://foo"')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 6)
self.assertEqual(self.backend.current_playlist.tracks.get()[5], needle)
+ self.assertEqualResponse(u'OK')
def test_add_with_uri_not_found_in_library_should_ack(self):
- result = self.dispatcher.handle_request(u'add "dummy://foo"')
- self.assertEqual(result[0],
+ self.sendRequest(u'add "dummy://foo"')
+ self.assertEqualResponse(
u'ACK [50@0] {add} directory or file not found')
def test_add_with_empty_uri_should_add_all_known_tracks_and_ok(self):
- result = self.dispatcher.handle_request(u'add ""')
+ self.sendRequest(u'add ""')
# TODO check that we add all tracks (we currently don't)
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_addid_without_songpos(self):
needle = Track(uri='dummy://foo')
@@ -45,16 +34,17 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
- result = self.dispatcher.handle_request(u'addid "dummy://foo"')
+
+ self.sendRequest(u'addid "dummy://foo"')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 6)
self.assertEqual(self.backend.current_playlist.tracks.get()[5], needle)
- self.assert_(u'Id: %d' %
- self.backend.current_playlist.cp_tracks.get()[5][0] in result)
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'Id: %d' %
+ self.backend.current_playlist.cp_tracks.get()[5][0])
+ self.assertInResponse(u'OK')
def test_addid_with_empty_uri_acks(self):
- result = self.dispatcher.handle_request(u'addid ""')
- self.assertEqual(result[0], u'ACK [50@0] {addid} No such song')
+ self.sendRequest(u'addid ""')
+ self.assertEqualResponse(u'ACK [50@0] {addid} No such song')
def test_addid_with_songpos(self):
needle = Track(uri='dummy://foo')
@@ -63,12 +53,13 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
- result = self.dispatcher.handle_request(u'addid "dummy://foo" "3"')
+
+ self.sendRequest(u'addid "dummy://foo" "3"')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 6)
self.assertEqual(self.backend.current_playlist.tracks.get()[3], needle)
- self.assert_(u'Id: %d' %
- self.backend.current_playlist.cp_tracks.get()[3][0] in result)
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'Id: %d' %
+ self.backend.current_playlist.cp_tracks.get()[3][0])
+ self.assertInResponse(u'OK')
def test_addid_with_songpos_out_of_bounds_should_ack(self):
needle = Track(uri='dummy://foo')
@@ -77,83 +68,93 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
- result = self.dispatcher.handle_request(u'addid "dummy://foo" "6"')
- self.assertEqual(result[0], u'ACK [2@0] {addid} Bad song index')
+
+ self.sendRequest(u'addid "dummy://foo" "6"')
+ self.assertEqualResponse(u'ACK [2@0] {addid} Bad song index')
def test_addid_with_uri_not_found_in_library_should_ack(self):
- result = self.dispatcher.handle_request(u'addid "dummy://foo"')
- self.assertEqual(result[0], u'ACK [50@0] {addid} No such song')
+ self.sendRequest(u'addid "dummy://foo"')
+ self.assertEqualResponse(u'ACK [50@0] {addid} No such song')
def test_clear(self):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
- result = self.dispatcher.handle_request(u'clear')
+
+ self.sendRequest(u'clear')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 0)
self.assertEqual(self.backend.playback.current_track.get(), None)
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_delete_songpos(self):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
- result = self.dispatcher.handle_request(u'delete "%d"' %
+
+ self.sendRequest(u'delete "%d"' %
self.backend.current_playlist.cp_tracks.get()[2][0])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 4)
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_delete_songpos_out_of_bounds(self):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
- result = self.dispatcher.handle_request(u'delete "5"')
+
+ self.sendRequest(u'delete "5"')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
- self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index')
+ self.assertEqualResponse(u'ACK [2@0] {delete} Bad song index')
def test_delete_open_range(self):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
- result = self.dispatcher.handle_request(u'delete "1:"')
+
+ self.sendRequest(u'delete "1:"')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 1)
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_delete_closed_range(self):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
- result = self.dispatcher.handle_request(u'delete "1:3"')
+
+ self.sendRequest(u'delete "1:3"')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 3)
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_delete_range_out_of_bounds(self):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
- result = self.dispatcher.handle_request(u'delete "5:7"')
+
+ self.sendRequest(u'delete "5:7"')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
- self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index')
+ self.assertEqualResponse(u'ACK [2@0] {delete} Bad song index')
def test_deleteid(self):
self.backend.current_playlist.append([Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
- result = self.dispatcher.handle_request(u'deleteid "1"')
+
+ self.sendRequest(u'deleteid "1"')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 1)
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_deleteid_does_not_exist(self):
self.backend.current_playlist.append([Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
- result = self.dispatcher.handle_request(u'deleteid "12345"')
+
+ self.sendRequest(u'deleteid "12345"')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
- self.assertEqual(result[0], u'ACK [50@0] {deleteid} No such song')
+ self.assertEqualResponse(u'ACK [50@0] {deleteid} No such song')
def test_move_songpos(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.dispatcher.handle_request(u'move "1" "0"')
+
+ self.sendRequest(u'move "1" "0"')
tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'b')
self.assertEqual(tracks[1].name, 'a')
@@ -161,14 +162,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assertEqual(tracks[3].name, 'd')
self.assertEqual(tracks[4].name, 'e')
self.assertEqual(tracks[5].name, 'f')
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_move_open_range(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.dispatcher.handle_request(u'move "2:" "0"')
+
+ self.sendRequest(u'move "2:" "0"')
tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'c')
self.assertEqual(tracks[1].name, 'd')
@@ -176,14 +178,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assertEqual(tracks[3].name, 'f')
self.assertEqual(tracks[4].name, 'a')
self.assertEqual(tracks[5].name, 'b')
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_move_closed_range(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.dispatcher.handle_request(u'move "1:3" "0"')
+
+ self.sendRequest(u'move "1:3" "0"')
tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'b')
self.assertEqual(tracks[1].name, 'c')
@@ -191,14 +194,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assertEqual(tracks[3].name, 'd')
self.assertEqual(tracks[4].name, 'e')
self.assertEqual(tracks[5].name, 'f')
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_moveid(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.dispatcher.handle_request(u'moveid "4" "2"')
+
+ self.sendRequest(u'moveid "4" "2"')
tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'a')
self.assertEqual(tracks[1].name, 'b')
@@ -206,179 +210,200 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assertEqual(tracks[3].name, 'c')
self.assertEqual(tracks[4].name, 'd')
self.assertEqual(tracks[5].name, 'f')
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_playlist_returns_same_as_playlistinfo(self):
- playlist_result = self.dispatcher.handle_request(u'playlist')
- playlistinfo_result = self.dispatcher.handle_request(u'playlistinfo')
- self.assertEqual(playlist_result, playlistinfo_result)
+ playlist_response = self.sendRequest(u'playlist')
+ playlistinfo_response = self.sendRequest(u'playlistinfo')
+ self.assertEqual(playlist_response, playlistinfo_response)
def test_playlistfind(self):
- result = self.dispatcher.handle_request(u'playlistfind "tag" "needle"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
+ self.sendRequest(u'playlistfind "tag" "needle"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
def test_playlistfind_by_filename_not_in_current_playlist(self):
- result = self.dispatcher.handle_request(
- u'playlistfind "filename" "file:///dev/null"')
- self.assertEqual(len(result), 1)
- self.assert_(u'OK' in result)
+ self.sendRequest(u'playlistfind "filename" "file:///dev/null"')
+ self.assertEqualResponse(u'OK')
def test_playlistfind_by_filename_without_quotes(self):
- result = self.dispatcher.handle_request(
- u'playlistfind filename "file:///dev/null"')
- self.assertEqual(len(result), 1)
- self.assert_(u'OK' in result)
+ self.sendRequest(u'playlistfind filename "file:///dev/null"')
+ self.assertEqualResponse(u'OK')
def test_playlistfind_by_filename_in_current_playlist(self):
self.backend.current_playlist.append([
Track(uri='file:///exists')])
- result = self.dispatcher.handle_request(
- u'playlistfind filename "file:///exists"')
- self.assert_(u'file: file:///exists' in result)
- self.assert_(u'Id: 0' in result)
- self.assert_(u'Pos: 0' in result)
- self.assert_(u'OK' in result)
+
+ self.sendRequest( u'playlistfind filename "file:///exists"')
+ self.assertInResponse(u'file: file:///exists')
+ self.assertInResponse(u'Id: 0')
+ self.assertInResponse(u'Pos: 0')
+ self.assertInResponse(u'OK')
def test_playlistid_without_songid(self):
self.backend.current_playlist.append([Track(name='a'), Track(name='b')])
- result = self.dispatcher.handle_request(u'playlistid')
- self.assert_(u'Title: a' in result)
- self.assert_(u'Title: b' in result)
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'playlistid')
+ self.assertInResponse(u'Title: a')
+ self.assertInResponse(u'Title: b')
+ self.assertInResponse(u'OK')
def test_playlistid_with_songid(self):
self.backend.current_playlist.append([Track(name='a'), Track(name='b')])
- result = self.dispatcher.handle_request(u'playlistid "1"')
- self.assert_(u'Title: a' not in result)
- self.assert_(u'Id: 0' not in result)
- self.assert_(u'Title: b' in result)
- self.assert_(u'Id: 1' in result)
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'playlistid "1"')
+ self.assertNotInResponse(u'Title: a')
+ self.assertNotInResponse(u'Id: 0')
+ self.assertInResponse(u'Title: b')
+ self.assertInResponse(u'Id: 1')
+ self.assertInResponse(u'OK')
def test_playlistid_with_not_existing_songid_fails(self):
self.backend.current_playlist.append([Track(name='a'), Track(name='b')])
- result = self.dispatcher.handle_request(u'playlistid "25"')
- self.assertEqual(result[0], u'ACK [50@0] {playlistid} No such song')
+
+ self.sendRequest(u'playlistid "25"')
+ self.assertEqualResponse(u'ACK [50@0] {playlistid} No such song')
def test_playlistinfo_without_songpos_or_range(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.dispatcher.handle_request(u'playlistinfo')
- self.assert_(u'Title: a' in result)
- self.assert_(u'Title: b' in result)
- self.assert_(u'Title: c' in result)
- self.assert_(u'Title: d' in result)
- self.assert_(u'Title: e' in result)
- self.assert_(u'Title: f' in result)
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'playlistinfo')
+ self.assertInResponse(u'Title: a')
+ self.assertInResponse(u'Pos: 0')
+ self.assertInResponse(u'Title: b')
+ self.assertInResponse(u'Pos: 1')
+ self.assertInResponse(u'Title: c')
+ self.assertInResponse(u'Pos: 2')
+ self.assertInResponse(u'Title: d')
+ self.assertInResponse(u'Pos: 3')
+ self.assertInResponse(u'Title: e')
+ self.assertInResponse(u'Pos: 4')
+ self.assertInResponse(u'Title: f')
+ self.assertInResponse(u'Pos: 5')
+ self.assertInResponse(u'OK')
def test_playlistinfo_with_songpos(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.dispatcher.handle_request(u'playlistinfo "4"')
- self.assert_(u'Title: a' not in result)
- self.assert_(u'Title: b' not in result)
- self.assert_(u'Title: c' not in result)
- self.assert_(u'Title: d' not in result)
- self.assert_(u'Title: e' in result)
- self.assert_(u'Title: f' not in result)
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'playlistinfo "4"')
+ self.assertNotInResponse(u'Title: a')
+ self.assertNotInResponse(u'Pos: 0')
+ self.assertNotInResponse(u'Title: b')
+ self.assertNotInResponse(u'Pos: 1')
+ self.assertNotInResponse(u'Title: c')
+ self.assertNotInResponse(u'Pos: 2')
+ self.assertNotInResponse(u'Title: d')
+ self.assertNotInResponse(u'Pos: 3')
+ self.assertInResponse(u'Title: e')
+ self.assertInResponse(u'Pos: 4')
+ self.assertNotInResponse(u'Title: f')
+ self.assertNotInResponse(u'Pos: 5')
+ self.assertInResponse(u'OK')
def test_playlistinfo_with_negative_songpos_same_as_playlistinfo(self):
- result1 = self.dispatcher.handle_request(u'playlistinfo "-1"')
- result2 = self.dispatcher.handle_request(u'playlistinfo')
- self.assertEqual(result1, result2)
+ response1 = self.sendRequest(u'playlistinfo "-1"')
+ response2 = self.sendRequest(u'playlistinfo')
+ self.assertEqual(response1, response2)
def test_playlistinfo_with_open_range(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.dispatcher.handle_request(u'playlistinfo "2:"')
- self.assert_(u'Title: a' not in result)
- self.assert_(u'Title: b' not in result)
- self.assert_(u'Title: c' in result)
- self.assert_(u'Title: d' in result)
- self.assert_(u'Title: e' in result)
- self.assert_(u'Title: f' in result)
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'playlistinfo "2:"')
+ self.assertNotInResponse(u'Title: a')
+ self.assertNotInResponse(u'Pos: 0')
+ self.assertNotInResponse(u'Title: b')
+ self.assertNotInResponse(u'Pos: 1')
+ self.assertInResponse(u'Title: c')
+ self.assertInResponse(u'Pos: 2')
+ self.assertInResponse(u'Title: d')
+ self.assertInResponse(u'Pos: 3')
+ self.assertInResponse(u'Title: e')
+ self.assertInResponse(u'Pos: 4')
+ self.assertInResponse(u'Title: f')
+ self.assertInResponse(u'Pos: 5')
+ self.assertInResponse(u'OK')
def test_playlistinfo_with_closed_range(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.dispatcher.handle_request(u'playlistinfo "2:4"')
- self.assert_(u'Title: a' not in result)
- self.assert_(u'Title: b' not in result)
- self.assert_(u'Title: c' in result)
- self.assert_(u'Title: d' in result)
- self.assert_(u'Title: e' not in result)
- self.assert_(u'Title: f' not in result)
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'playlistinfo "2:4"')
+ self.assertNotInResponse(u'Title: a')
+ self.assertNotInResponse(u'Title: b')
+ self.assertInResponse(u'Title: c')
+ self.assertInResponse(u'Title: d')
+ self.assertNotInResponse(u'Title: e')
+ self.assertNotInResponse(u'Title: f')
+ self.assertInResponse(u'OK')
def test_playlistinfo_with_too_high_start_of_range_returns_arg_error(self):
- result = self.dispatcher.handle_request(u'playlistinfo "10:20"')
- self.assert_(u'ACK [2@0] {playlistinfo} Bad song index' in result)
+ self.sendRequest(u'playlistinfo "10:20"')
+ self.assertEqualResponse(u'ACK [2@0] {playlistinfo} Bad song index')
def test_playlistinfo_with_too_high_end_of_range_returns_ok(self):
- result = self.dispatcher.handle_request(u'playlistinfo "0:20"')
- self.assert_(u'OK' in result)
+ self.sendRequest(u'playlistinfo "0:20"')
+ self.assertInResponse(u'OK')
def test_playlistsearch(self):
- result = self.dispatcher.handle_request(
- u'playlistsearch "any" "needle"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
+ self.sendRequest( u'playlistsearch "any" "needle"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
def test_playlistsearch_without_quotes(self):
- result = self.dispatcher.handle_request(u'playlistsearch any "needle"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
+ self.sendRequest(u'playlistsearch any "needle"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
def test_plchanges(self):
self.backend.current_playlist.append(
[Track(name='a'), Track(name='b'), Track(name='c')])
- result = self.dispatcher.handle_request(u'plchanges "0"')
- self.assert_(u'Title: a' in result)
- self.assert_(u'Title: b' in result)
- self.assert_(u'Title: c' in result)
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'plchanges "0"')
+ self.assertInResponse(u'Title: a')
+ self.assertInResponse(u'Title: b')
+ self.assertInResponse(u'Title: c')
+ self.assertInResponse(u'OK')
def test_plchanges_with_minus_one_returns_entire_playlist(self):
self.backend.current_playlist.append(
[Track(name='a'), Track(name='b'), Track(name='c')])
- result = self.dispatcher.handle_request(u'plchanges "-1"')
- self.assert_(u'Title: a' in result)
- self.assert_(u'Title: b' in result)
- self.assert_(u'Title: c' in result)
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'plchanges "-1"')
+ self.assertInResponse(u'Title: a')
+ self.assertInResponse(u'Title: b')
+ self.assertInResponse(u'Title: c')
+ self.assertInResponse(u'OK')
def test_plchanges_without_quotes_works(self):
self.backend.current_playlist.append(
[Track(name='a'), Track(name='b'), Track(name='c')])
- result = self.dispatcher.handle_request(u'plchanges 0')
- self.assert_(u'Title: a' in result)
- self.assert_(u'Title: b' in result)
- self.assert_(u'Title: c' in result)
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'plchanges 0')
+ self.assertInResponse(u'Title: a')
+ self.assertInResponse(u'Title: b')
+ self.assertInResponse(u'Title: c')
+ self.assertInResponse(u'OK')
def test_plchangesposid(self):
self.backend.current_playlist.append([Track(), Track(), Track()])
- result = self.dispatcher.handle_request(u'plchangesposid "0"')
+
+ self.sendRequest(u'plchangesposid "0"')
cp_tracks = self.backend.current_playlist.cp_tracks.get()
- self.assert_(u'cpos: 0' in result)
- self.assert_(u'Id: %d' % cp_tracks[0][0]
- in result)
- self.assert_(u'cpos: 2' in result)
- self.assert_(u'Id: %d' % cp_tracks[1][0]
- in result)
- self.assert_(u'cpos: 2' in result)
- self.assert_(u'Id: %d' % cp_tracks[2][0]
- in result)
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'cpos: 0')
+ self.assertInResponse(u'Id: %d' % cp_tracks[0][0])
+ self.assertInResponse(u'cpos: 2')
+ self.assertInResponse(u'Id: %d' % cp_tracks[1][0])
+ self.assertInResponse(u'cpos: 2')
+ self.assertInResponse(u'Id: %d' % cp_tracks[2][0])
+ self.assertInResponse(u'OK')
def test_shuffle_without_range(self):
self.backend.current_playlist.append([
@@ -386,9 +411,10 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
Track(name='d'), Track(name='e'), Track(name='f'),
])
version = self.backend.current_playlist.version.get()
- result = self.dispatcher.handle_request(u'shuffle')
+
+ self.sendRequest(u'shuffle')
self.assert_(version < self.backend.current_playlist.version.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_shuffle_with_open_range(self):
self.backend.current_playlist.append([
@@ -396,14 +422,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
Track(name='d'), Track(name='e'), Track(name='f'),
])
version = self.backend.current_playlist.version.get()
- result = self.dispatcher.handle_request(u'shuffle "4:"')
+
+ self.sendRequest(u'shuffle "4:"')
self.assert_(version < self.backend.current_playlist.version.get())
tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'a')
self.assertEqual(tracks[1].name, 'b')
self.assertEqual(tracks[2].name, 'c')
self.assertEqual(tracks[3].name, 'd')
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_shuffle_with_closed_range(self):
self.backend.current_playlist.append([
@@ -411,21 +438,23 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
Track(name='d'), Track(name='e'), Track(name='f'),
])
version = self.backend.current_playlist.version.get()
- result = self.dispatcher.handle_request(u'shuffle "1:3"')
+
+ self.sendRequest(u'shuffle "1:3"')
self.assert_(version < self.backend.current_playlist.version.get())
tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'a')
self.assertEqual(tracks[3].name, 'd')
self.assertEqual(tracks[4].name, 'e')
self.assertEqual(tracks[5].name, 'f')
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_swap(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.dispatcher.handle_request(u'swap "1" "4"')
+
+ self.sendRequest(u'swap "1" "4"')
tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'a')
self.assertEqual(tracks[1].name, 'e')
@@ -433,14 +462,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assertEqual(tracks[3].name, 'd')
self.assertEqual(tracks[4].name, 'b')
self.assertEqual(tracks[5].name, 'f')
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_swapid(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.dispatcher.handle_request(u'swapid "1" "4"')
+
+ self.sendRequest(u'swapid "1" "4"')
tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'a')
self.assertEqual(tracks[1].name, 'e')
@@ -448,4 +478,4 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assertEqual(tracks[3].name, 'd')
self.assertEqual(tracks[4].name, 'b')
self.assertEqual(tracks[5].name, 'f')
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
diff --git a/tests/frontends/mpd/protocol/idle_test.py b/tests/frontends/mpd/protocol/idle_test.py
new file mode 100644
index 00000000..ae23c88e
--- /dev/null
+++ b/tests/frontends/mpd/protocol/idle_test.py
@@ -0,0 +1,206 @@
+from mock import patch
+
+from mopidy.frontends.mpd.protocol.status import SUBSYSTEMS
+
+from tests.frontends.mpd import protocol
+
+
+class IdleHandlerTest(protocol.BaseTestCase):
+ def idleEvent(self, subsystem):
+ self.session.on_idle(subsystem)
+
+ def assertEqualEvents(self, events):
+ self.assertEqual(set(events), self.context.events)
+
+ def assertEqualSubscriptions(self, events):
+ self.assertEqual(set(events), self.context.subscriptions)
+
+ def assertNoEvents(self):
+ self.assertEqualEvents([])
+
+ def assertNoSubscriptions(self):
+ self.assertEqualSubscriptions([])
+
+ def test_base_state(self):
+ self.assertNoSubscriptions()
+ self.assertNoEvents()
+ self.assertNoResponse()
+
+ def test_idle(self):
+ self.sendRequest(u'idle')
+ self.assertEqualSubscriptions(SUBSYSTEMS)
+ self.assertNoEvents()
+ self.assertNoResponse()
+
+ def test_idle_disables_timeout(self):
+ self.sendRequest(u'idle')
+ self.connection.disable_timeout.assert_called_once_with()
+
+ def test_noidle(self):
+ self.sendRequest(u'noidle')
+ self.assertNoSubscriptions()
+ self.assertNoEvents()
+ self.assertNoResponse()
+
+ def test_idle_player(self):
+ self.sendRequest(u'idle player')
+ self.assertEqualSubscriptions(['player'])
+ self.assertNoEvents()
+ self.assertNoResponse()
+
+ def test_idle_player_playlist(self):
+ self.sendRequest(u'idle player playlist')
+ self.assertEqualSubscriptions(['player', 'playlist'])
+ self.assertNoEvents()
+ self.assertNoResponse()
+
+ def test_idle_then_noidle(self):
+ self.sendRequest(u'idle')
+ self.sendRequest(u'noidle')
+ self.assertNoSubscriptions()
+ self.assertNoEvents()
+ self.assertOnceInResponse(u'OK')
+
+ def test_idle_then_noidle_enables_timeout(self):
+ self.sendRequest(u'idle')
+ self.sendRequest(u'noidle')
+ self.connection.enable_timeout.assert_called_once_with()
+
+ def test_idle_then_play(self):
+ with patch.object(self.session, 'stop') as stop_mock:
+ self.sendRequest(u'idle')
+ self.sendRequest(u'play')
+ stop_mock.assert_called_once_with()
+
+ def test_idle_then_idle(self):
+ with patch.object(self.session, 'stop') as stop_mock:
+ self.sendRequest(u'idle')
+ self.sendRequest(u'idle')
+ stop_mock.assert_called_once_with()
+
+ def test_idle_player_then_play(self):
+ with patch.object(self.session, 'stop') as stop_mock:
+ self.sendRequest(u'idle player')
+ self.sendRequest(u'play')
+ stop_mock.assert_called_once_with()
+
+ def test_idle_then_player(self):
+ self.sendRequest(u'idle')
+ self.idleEvent(u'player')
+ self.assertNoSubscriptions()
+ self.assertNoEvents()
+ self.assertOnceInResponse(u'changed: player')
+ self.assertOnceInResponse(u'OK')
+
+ def test_idle_player_then_event_player(self):
+ self.sendRequest(u'idle player')
+ self.idleEvent(u'player')
+ self.assertNoSubscriptions()
+ self.assertNoEvents()
+ self.assertOnceInResponse(u'changed: player')
+ self.assertOnceInResponse(u'OK')
+
+ def test_idle_player_then_noidle(self):
+ self.sendRequest(u'idle player')
+ self.sendRequest(u'noidle')
+ self.assertNoSubscriptions()
+ self.assertNoEvents()
+ self.assertOnceInResponse(u'OK')
+
+ def test_idle_player_playlist_then_noidle(self):
+ self.sendRequest(u'idle player playlist')
+ self.sendRequest(u'noidle')
+ self.assertNoEvents()
+ self.assertNoSubscriptions()
+ self.assertOnceInResponse(u'OK')
+
+ def test_idle_player_playlist_then_player(self):
+ self.sendRequest(u'idle player playlist')
+ self.idleEvent(u'player')
+ self.assertNoEvents()
+ self.assertNoSubscriptions()
+ self.assertOnceInResponse(u'changed: player')
+ self.assertNotInResponse(u'changed: playlist')
+ self.assertOnceInResponse(u'OK')
+
+ def test_idle_playlist_then_player(self):
+ self.sendRequest(u'idle playlist')
+ self.idleEvent(u'player')
+ self.assertEqualEvents(['player'])
+ self.assertEqualSubscriptions(['playlist'])
+ self.assertNoResponse()
+
+ def test_idle_playlist_then_player_then_playlist(self):
+ self.sendRequest(u'idle playlist')
+ self.idleEvent(u'player')
+ self.idleEvent(u'playlist')
+ self.assertNoEvents()
+ self.assertNoSubscriptions()
+ self.assertNotInResponse(u'changed: player')
+ self.assertOnceInResponse(u'changed: playlist')
+ self.assertOnceInResponse(u'OK')
+
+ def test_player(self):
+ self.idleEvent(u'player')
+ self.assertEqualEvents(['player'])
+ self.assertNoSubscriptions()
+ self.assertNoResponse()
+
+ def test_player_then_idle_player(self):
+ self.idleEvent(u'player')
+ self.sendRequest(u'idle player')
+ self.assertNoEvents()
+ self.assertNoSubscriptions()
+ self.assertOnceInResponse(u'changed: player')
+ self.assertNotInResponse(u'changed: playlist')
+ self.assertOnceInResponse(u'OK')
+
+ def test_player_then_playlist(self):
+ self.idleEvent(u'player')
+ self.idleEvent(u'playlist')
+ self.assertEqualEvents(['player', 'playlist'])
+ self.assertNoSubscriptions()
+ self.assertNoResponse()
+
+ def test_player_then_idle(self):
+ self.idleEvent(u'player')
+ self.sendRequest(u'idle')
+ self.assertNoEvents()
+ self.assertNoSubscriptions()
+ self.assertOnceInResponse(u'changed: player')
+ self.assertOnceInResponse(u'OK')
+
+ def test_player_then_playlist_then_idle(self):
+ self.idleEvent(u'player')
+ self.idleEvent(u'playlist')
+ self.sendRequest(u'idle')
+ self.assertNoEvents()
+ self.assertNoSubscriptions()
+ self.assertOnceInResponse(u'changed: player')
+ self.assertOnceInResponse(u'changed: playlist')
+ self.assertOnceInResponse(u'OK')
+
+ def test_player_then_idle_playlist(self):
+ self.idleEvent(u'player')
+ self.sendRequest(u'idle playlist')
+ self.assertEqualEvents(['player'])
+ self.assertEqualSubscriptions(['playlist'])
+ self.assertNoResponse()
+
+ def test_player_then_idle_playlist_then_noidle(self):
+ self.idleEvent(u'player')
+ self.sendRequest(u'idle playlist')
+ self.sendRequest(u'noidle')
+ self.assertNoEvents()
+ self.assertNoSubscriptions()
+ self.assertOnceInResponse(u'OK')
+
+ def test_player_then_playlist_then_idle_playlist(self):
+ self.idleEvent(u'player')
+ self.idleEvent(u'playlist')
+ self.sendRequest(u'idle playlist')
+ self.assertNoEvents()
+ self.assertNoSubscriptions()
+ self.assertNotInResponse(u'changed: player')
+ self.assertOnceInResponse(u'changed: playlist')
+ self.assertOnceInResponse(u'OK')
diff --git a/tests/frontends/mpd/protocol/music_db_test.py b/tests/frontends/mpd/protocol/music_db_test.py
new file mode 100644
index 00000000..088502c4
--- /dev/null
+++ b/tests/frontends/mpd/protocol/music_db_test.py
@@ -0,0 +1,344 @@
+from tests.frontends.mpd import protocol
+
+
+class MusicDatabaseHandlerTest(protocol.BaseTestCase):
+ def test_count(self):
+ self.sendRequest(u'count "tag" "needle"')
+ self.assertInResponse(u'songs: 0')
+ self.assertInResponse(u'playtime: 0')
+ self.assertInResponse(u'OK')
+
+ def test_findadd(self):
+ self.sendRequest(u'findadd "album" "what"')
+ self.assertInResponse(u'OK')
+
+ def test_listall(self):
+ self.sendRequest(u'listall "file:///dev/urandom"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_listallinfo(self):
+ self.sendRequest(u'listallinfo "file:///dev/urandom"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_lsinfo_without_path_returns_same_as_listplaylists(self):
+ lsinfo_response = self.sendRequest(u'lsinfo')
+ listplaylists_response = self.sendRequest(u'listplaylists')
+ self.assertEqual(lsinfo_response, listplaylists_response)
+
+ def test_lsinfo_with_empty_path_returns_same_as_listplaylists(self):
+ lsinfo_response = self.sendRequest(u'lsinfo ""')
+ listplaylists_response = self.sendRequest(u'listplaylists')
+ self.assertEqual(lsinfo_response, listplaylists_response)
+
+ def test_lsinfo_for_root_returns_same_as_listplaylists(self):
+ lsinfo_response = self.sendRequest(u'lsinfo "/"')
+ listplaylists_response = self.sendRequest(u'listplaylists')
+ self.assertEqual(lsinfo_response, listplaylists_response)
+
+ def test_update_without_uri(self):
+ self.sendRequest(u'update')
+ self.assertInResponse(u'updating_db: 0')
+ self.assertInResponse(u'OK')
+
+ def test_update_with_uri(self):
+ self.sendRequest(u'update "file:///dev/urandom"')
+ self.assertInResponse(u'updating_db: 0')
+ self.assertInResponse(u'OK')
+
+ def test_rescan_without_uri(self):
+ self.sendRequest(u'rescan')
+ self.assertInResponse(u'updating_db: 0')
+ self.assertInResponse(u'OK')
+
+ def test_rescan_with_uri(self):
+ self.sendRequest(u'rescan "file:///dev/urandom"')
+ self.assertInResponse(u'updating_db: 0')
+ self.assertInResponse(u'OK')
+
+
+class MusicDatabaseFindTest(protocol.BaseTestCase):
+ def test_find_album(self):
+ self.sendRequest(u'find "album" "what"')
+ self.assertInResponse(u'OK')
+
+ def test_find_album_without_quotes(self):
+ self.sendRequest(u'find album "what"')
+ self.assertInResponse(u'OK')
+
+ def test_find_artist(self):
+ self.sendRequest(u'find "artist" "what"')
+ self.assertInResponse(u'OK')
+
+ def test_find_artist_without_quotes(self):
+ self.sendRequest(u'find artist "what"')
+ self.assertInResponse(u'OK')
+
+ def test_find_title(self):
+ self.sendRequest(u'find "title" "what"')
+ self.assertInResponse(u'OK')
+
+ def test_find_title_without_quotes(self):
+ self.sendRequest(u'find title "what"')
+ self.assertInResponse(u'OK')
+
+ def test_find_date(self):
+ self.sendRequest(u'find "date" "2002-01-01"')
+ self.assertInResponse(u'OK')
+
+ def test_find_date_without_quotes(self):
+ self.sendRequest(u'find date "2002-01-01"')
+ self.assertInResponse(u'OK')
+
+ def test_find_date_with_capital_d_and_incomplete_date(self):
+ self.sendRequest(u'find Date "2005"')
+ self.assertInResponse(u'OK')
+
+ def test_find_else_should_fail(self):
+ self.sendRequest(u'find "somethingelse" "what"')
+ self.assertEqualResponse(u'ACK [2@0] {find} incorrect arguments')
+
+ def test_find_album_and_artist(self):
+ self.sendRequest(u'find album "album_what" artist "artist_what"')
+ self.assertInResponse(u'OK')
+
+
+class MusicDatabaseListTest(protocol.BaseTestCase):
+ def test_list_foo_returns_ack(self):
+ self.sendRequest(u'list "foo"')
+ self.assertEqualResponse(u'ACK [2@0] {list} incorrect arguments')
+
+ ### Artist
+
+ def test_list_artist_with_quotes(self):
+ self.sendRequest(u'list "artist"')
+ self.assertInResponse(u'OK')
+
+ def test_list_artist_without_quotes(self):
+ self.sendRequest(u'list artist')
+ self.assertInResponse(u'OK')
+
+ def test_list_artist_without_quotes_and_capitalized(self):
+ self.sendRequest(u'list Artist')
+ self.assertInResponse(u'OK')
+
+ def test_list_artist_with_query_of_one_token(self):
+ self.sendRequest(u'list "artist" "anartist"')
+ self.assertEqualResponse(
+ u'ACK [2@0] {list} should be "Album" for 3 arguments')
+
+ def test_list_artist_with_unknown_field_in_query_returns_ack(self):
+ self.sendRequest(u'list "artist" "foo" "bar"')
+ self.assertEqualResponse(u'ACK [2@0] {list} not able to parse args')
+
+ def test_list_artist_by_artist(self):
+ self.sendRequest(u'list "artist" "artist" "anartist"')
+ self.assertInResponse(u'OK')
+
+ def test_list_artist_by_album(self):
+ self.sendRequest(u'list "artist" "album" "analbum"')
+ self.assertInResponse(u'OK')
+
+ def test_list_artist_by_full_date(self):
+ self.sendRequest(u'list "artist" "date" "2001-01-01"')
+ self.assertInResponse(u'OK')
+
+ def test_list_artist_by_year(self):
+ self.sendRequest(u'list "artist" "date" "2001"')
+ self.assertInResponse(u'OK')
+
+ def test_list_artist_by_genre(self):
+ self.sendRequest(u'list "artist" "genre" "agenre"')
+ self.assertInResponse(u'OK')
+
+ def test_list_artist_by_artist_and_album(self):
+ self.sendRequest(
+ u'list "artist" "artist" "anartist" "album" "analbum"')
+ self.assertInResponse(u'OK')
+
+ ### Album
+
+ def test_list_album_with_quotes(self):
+ self.sendRequest(u'list "album"')
+ self.assertInResponse(u'OK')
+
+ def test_list_album_without_quotes(self):
+ self.sendRequest(u'list album')
+ self.assertInResponse(u'OK')
+
+ def test_list_album_without_quotes_and_capitalized(self):
+ self.sendRequest(u'list Album')
+ self.assertInResponse(u'OK')
+
+ def test_list_album_with_artist_name(self):
+ self.sendRequest(u'list "album" "anartist"')
+ self.assertInResponse(u'OK')
+
+ def test_list_album_by_artist(self):
+ self.sendRequest(u'list "album" "artist" "anartist"')
+ self.assertInResponse(u'OK')
+
+ def test_list_album_by_album(self):
+ self.sendRequest(u'list "album" "album" "analbum"')
+ self.assertInResponse(u'OK')
+
+ def test_list_album_by_full_date(self):
+ self.sendRequest(u'list "album" "date" "2001-01-01"')
+ self.assertInResponse(u'OK')
+
+ def test_list_album_by_year(self):
+ self.sendRequest(u'list "album" "date" "2001"')
+ self.assertInResponse(u'OK')
+
+ def test_list_album_by_genre(self):
+ self.sendRequest(u'list "album" "genre" "agenre"')
+ self.assertInResponse(u'OK')
+
+ def test_list_album_by_artist_and_album(self):
+ self.sendRequest(
+ u'list "album" "artist" "anartist" "album" "analbum"')
+ self.assertInResponse(u'OK')
+
+ ### Date
+
+ def test_list_date_with_quotes(self):
+ self.sendRequest(u'list "date"')
+ self.assertInResponse(u'OK')
+
+ def test_list_date_without_quotes(self):
+ self.sendRequest(u'list date')
+ self.assertInResponse(u'OK')
+
+ def test_list_date_without_quotes_and_capitalized(self):
+ self.sendRequest(u'list Date')
+ self.assertInResponse(u'OK')
+
+ def test_list_date_with_query_of_one_token(self):
+ self.sendRequest(u'list "date" "anartist"')
+ self.assertEqualResponse(
+ u'ACK [2@0] {list} should be "Album" for 3 arguments')
+
+ def test_list_date_by_artist(self):
+ self.sendRequest(u'list "date" "artist" "anartist"')
+ self.assertInResponse(u'OK')
+
+ def test_list_date_by_album(self):
+ self.sendRequest(u'list "date" "album" "analbum"')
+ self.assertInResponse(u'OK')
+
+ def test_list_date_by_full_date(self):
+ self.sendRequest(u'list "date" "date" "2001-01-01"')
+ self.assertInResponse(u'OK')
+
+ def test_list_date_by_year(self):
+ self.sendRequest(u'list "date" "date" "2001"')
+ self.assertInResponse(u'OK')
+
+ def test_list_date_by_genre(self):
+ self.sendRequest(u'list "date" "genre" "agenre"')
+ self.assertInResponse(u'OK')
+
+ def test_list_date_by_artist_and_album(self):
+ self.sendRequest(u'list "date" "artist" "anartist" "album" "analbum"')
+ self.assertInResponse(u'OK')
+
+ ### Genre
+
+ def test_list_genre_with_quotes(self):
+ self.sendRequest(u'list "genre"')
+ self.assertInResponse(u'OK')
+
+ def test_list_genre_without_quotes(self):
+ self.sendRequest(u'list genre')
+ self.assertInResponse(u'OK')
+
+ def test_list_genre_without_quotes_and_capitalized(self):
+ self.sendRequest(u'list Genre')
+ self.assertInResponse(u'OK')
+
+ def test_list_genre_with_query_of_one_token(self):
+ self.sendRequest(u'list "genre" "anartist"')
+ self.assertEqualResponse(
+ u'ACK [2@0] {list} should be "Album" for 3 arguments')
+
+ def test_list_genre_by_artist(self):
+ self.sendRequest(u'list "genre" "artist" "anartist"')
+ self.assertInResponse(u'OK')
+
+ def test_list_genre_by_album(self):
+ self.sendRequest(u'list "genre" "album" "analbum"')
+ self.assertInResponse(u'OK')
+
+ def test_list_genre_by_full_date(self):
+ self.sendRequest(u'list "genre" "date" "2001-01-01"')
+ self.assertInResponse(u'OK')
+
+ def test_list_genre_by_year(self):
+ self.sendRequest(u'list "genre" "date" "2001"')
+ self.assertInResponse(u'OK')
+
+ def test_list_genre_by_genre(self):
+ self.sendRequest(u'list "genre" "genre" "agenre"')
+ self.assertInResponse(u'OK')
+
+ def test_list_genre_by_artist_and_album(self):
+ self.sendRequest(
+ u'list "genre" "artist" "anartist" "album" "analbum"')
+ self.assertInResponse(u'OK')
+
+
+class MusicDatabaseSearchTest(protocol.BaseTestCase):
+ def test_search_album(self):
+ self.sendRequest(u'search "album" "analbum"')
+ self.assertInResponse(u'OK')
+
+ def test_search_album_without_quotes(self):
+ self.sendRequest(u'search album "analbum"')
+ self.assertInResponse(u'OK')
+
+ def test_search_artist(self):
+ self.sendRequest(u'search "artist" "anartist"')
+ self.assertInResponse(u'OK')
+
+ def test_search_artist_without_quotes(self):
+ self.sendRequest(u'search artist "anartist"')
+ self.assertInResponse(u'OK')
+
+ def test_search_filename(self):
+ self.sendRequest(u'search "filename" "afilename"')
+ self.assertInResponse(u'OK')
+
+ def test_search_filename_without_quotes(self):
+ self.sendRequest(u'search filename "afilename"')
+ self.assertInResponse(u'OK')
+
+ def test_search_title(self):
+ self.sendRequest(u'search "title" "atitle"')
+ self.assertInResponse(u'OK')
+
+ def test_search_title_without_quotes(self):
+ self.sendRequest(u'search title "atitle"')
+ self.assertInResponse(u'OK')
+
+ def test_search_any(self):
+ self.sendRequest(u'search "any" "anything"')
+ self.assertInResponse(u'OK')
+
+ def test_search_any_without_quotes(self):
+ self.sendRequest(u'search any "anything"')
+ self.assertInResponse(u'OK')
+
+ def test_search_date(self):
+ self.sendRequest(u'search "date" "2002-01-01"')
+ self.assertInResponse(u'OK')
+
+ def test_search_date_without_quotes(self):
+ self.sendRequest(u'search date "2002-01-01"')
+ self.assertInResponse(u'OK')
+
+ def test_search_date_with_capital_d_and_incomplete_date(self):
+ self.sendRequest(u'search Date "2005"')
+ self.assertInResponse(u'OK')
+
+ def test_search_else_should_fail(self):
+ self.sendRequest(u'search "sometype" "something"')
+ self.assertEqualResponse(u'ACK [2@0] {search} incorrect arguments')
diff --git a/tests/frontends/mpd/playback_test.py b/tests/frontends/mpd/protocol/playback_test.py
similarity index 53%
rename from tests/frontends/mpd/playback_test.py
rename to tests/frontends/mpd/protocol/playback_test.py
index e80943d6..01658f6d 100644
--- a/tests/frontends/mpd/playback_test.py
+++ b/tests/frontends/mpd/protocol/playback_test.py
@@ -1,247 +1,238 @@
-import unittest
-
-from mopidy.backends.base import PlaybackController
-from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd.dispatcher import MpdDispatcher
-from mopidy.mixers.dummy import DummyMixer
+from mopidy.backends import base as backend
from mopidy.models import Track
-from tests import SkipTest
+from tests import unittest
+from tests.frontends.mpd import protocol
-PAUSED = PlaybackController.PAUSED
-PLAYING = PlaybackController.PLAYING
-STOPPED = PlaybackController.STOPPED
+PAUSED = backend.PlaybackController.PAUSED
+PLAYING = backend.PlaybackController.PLAYING
+STOPPED = backend.PlaybackController.STOPPED
-class PlaybackOptionsHandlerTest(unittest.TestCase):
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.dispatcher = MpdDispatcher()
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
+class PlaybackOptionsHandlerTest(protocol.BaseTestCase):
def test_consume_off(self):
- result = self.dispatcher.handle_request(u'consume "0"')
+ self.sendRequest(u'consume "0"')
self.assertFalse(self.backend.playback.consume.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_consume_off_without_quotes(self):
- result = self.dispatcher.handle_request(u'consume 0')
+ self.sendRequest(u'consume 0')
self.assertFalse(self.backend.playback.consume.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_consume_on(self):
- result = self.dispatcher.handle_request(u'consume "1"')
+ self.sendRequest(u'consume "1"')
self.assertTrue(self.backend.playback.consume.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_consume_on_without_quotes(self):
- result = self.dispatcher.handle_request(u'consume 1')
+ self.sendRequest(u'consume 1')
self.assertTrue(self.backend.playback.consume.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_crossfade(self):
- result = self.dispatcher.handle_request(u'crossfade "10"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
+ self.sendRequest(u'crossfade "10"')
+ self.assertInResponse(u'ACK [0@0] {} Not implemented')
def test_random_off(self):
- result = self.dispatcher.handle_request(u'random "0"')
+ self.sendRequest(u'random "0"')
self.assertFalse(self.backend.playback.random.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_random_off_without_quotes(self):
- result = self.dispatcher.handle_request(u'random 0')
+ self.sendRequest(u'random 0')
self.assertFalse(self.backend.playback.random.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_random_on(self):
- result = self.dispatcher.handle_request(u'random "1"')
+ self.sendRequest(u'random "1"')
self.assertTrue(self.backend.playback.random.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_random_on_without_quotes(self):
- result = self.dispatcher.handle_request(u'random 1')
+ self.sendRequest(u'random 1')
self.assertTrue(self.backend.playback.random.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_repeat_off(self):
- result = self.dispatcher.handle_request(u'repeat "0"')
+ self.sendRequest(u'repeat "0"')
self.assertFalse(self.backend.playback.repeat.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_repeat_off_without_quotes(self):
- result = self.dispatcher.handle_request(u'repeat 0')
+ self.sendRequest(u'repeat 0')
self.assertFalse(self.backend.playback.repeat.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_repeat_on(self):
- result = self.dispatcher.handle_request(u'repeat "1"')
+ self.sendRequest(u'repeat "1"')
self.assertTrue(self.backend.playback.repeat.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_repeat_on_without_quotes(self):
- result = self.dispatcher.handle_request(u'repeat 1')
+ self.sendRequest(u'repeat 1')
self.assertTrue(self.backend.playback.repeat.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_setvol_below_min(self):
- result = self.dispatcher.handle_request(u'setvol "-10"')
- self.assert_(u'OK' in result)
+ self.sendRequest(u'setvol "-10"')
self.assertEqual(0, self.mixer.volume.get())
+ self.assertInResponse(u'OK')
def test_setvol_min(self):
- result = self.dispatcher.handle_request(u'setvol "0"')
- self.assert_(u'OK' in result)
+ self.sendRequest(u'setvol "0"')
self.assertEqual(0, self.mixer.volume.get())
+ self.assertInResponse(u'OK')
def test_setvol_middle(self):
- result = self.dispatcher.handle_request(u'setvol "50"')
- self.assert_(u'OK' in result)
+ self.sendRequest(u'setvol "50"')
self.assertEqual(50, self.mixer.volume.get())
+ self.assertInResponse(u'OK')
def test_setvol_max(self):
- result = self.dispatcher.handle_request(u'setvol "100"')
- self.assert_(u'OK' in result)
+ self.sendRequest(u'setvol "100"')
self.assertEqual(100, self.mixer.volume.get())
+ self.assertInResponse(u'OK')
def test_setvol_above_max(self):
- result = self.dispatcher.handle_request(u'setvol "110"')
- self.assert_(u'OK' in result)
+ self.sendRequest(u'setvol "110"')
self.assertEqual(100, self.mixer.volume.get())
+ self.assertInResponse(u'OK')
def test_setvol_plus_is_ignored(self):
- result = self.dispatcher.handle_request(u'setvol "+10"')
- self.assert_(u'OK' in result)
+ self.sendRequest(u'setvol "+10"')
self.assertEqual(10, self.mixer.volume.get())
+ self.assertInResponse(u'OK')
def test_setvol_without_quotes(self):
- result = self.dispatcher.handle_request(u'setvol 50')
- self.assert_(u'OK' in result)
+ self.sendRequest(u'setvol 50')
self.assertEqual(50, self.mixer.volume.get())
+ self.assertInResponse(u'OK')
def test_single_off(self):
- result = self.dispatcher.handle_request(u'single "0"')
+ self.sendRequest(u'single "0"')
self.assertFalse(self.backend.playback.single.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_single_off_without_quotes(self):
- result = self.dispatcher.handle_request(u'single 0')
+ self.sendRequest(u'single 0')
self.assertFalse(self.backend.playback.single.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_single_on(self):
- result = self.dispatcher.handle_request(u'single "1"')
+ self.sendRequest(u'single "1"')
self.assertTrue(self.backend.playback.single.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_single_on_without_quotes(self):
- result = self.dispatcher.handle_request(u'single 1')
+ self.sendRequest(u'single 1')
self.assertTrue(self.backend.playback.single.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_replay_gain_mode_off(self):
- result = self.dispatcher.handle_request(u'replay_gain_mode "off"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
+ self.sendRequest(u'replay_gain_mode "off"')
+ self.assertInResponse(u'ACK [0@0] {} Not implemented')
def test_replay_gain_mode_track(self):
- result = self.dispatcher.handle_request(u'replay_gain_mode "track"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
+ self.sendRequest(u'replay_gain_mode "track"')
+ self.assertInResponse(u'ACK [0@0] {} Not implemented')
def test_replay_gain_mode_album(self):
- result = self.dispatcher.handle_request(u'replay_gain_mode "album"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
+ self.sendRequest(u'replay_gain_mode "album"')
+ self.assertInResponse(u'ACK [0@0] {} Not implemented')
def test_replay_gain_status_default(self):
- expected = u'off'
- result = self.dispatcher.handle_request(u'replay_gain_status')
- self.assert_(u'OK' in result)
- self.assert_(expected in result)
+ self.sendRequest(u'replay_gain_status')
+ self.assertInResponse(u'OK')
+ self.assertInResponse(u'off')
+ @unittest.SkipTest
def test_replay_gain_status_off(self):
- raise SkipTest # TODO
+ pass
+ @unittest.SkipTest
def test_replay_gain_status_track(self):
- raise SkipTest # TODO
+ pass
+ @unittest.SkipTest
def test_replay_gain_status_album(self):
- raise SkipTest # TODO
+ pass
-class PlaybackControlHandlerTest(unittest.TestCase):
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.dispatcher = MpdDispatcher()
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
-
+class PlaybackControlHandlerTest(protocol.BaseTestCase):
def test_next(self):
- result = self.dispatcher.handle_request(u'next')
- self.assert_(u'OK' in result)
+ self.sendRequest(u'next')
+ self.assertInResponse(u'OK')
def test_pause_off(self):
self.backend.current_playlist.append([Track()])
- self.dispatcher.handle_request(u'play "0"')
- self.dispatcher.handle_request(u'pause "1"')
- result = self.dispatcher.handle_request(u'pause "0"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'play "0"')
+ self.sendRequest(u'pause "1"')
+ self.sendRequest(u'pause "0"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
+ self.assertInResponse(u'OK')
def test_pause_on(self):
self.backend.current_playlist.append([Track()])
- self.dispatcher.handle_request(u'play "0"')
- result = self.dispatcher.handle_request(u'pause "1"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'play "0"')
+ self.sendRequest(u'pause "1"')
self.assertEqual(PAUSED, self.backend.playback.state.get())
+ self.assertInResponse(u'OK')
def test_pause_toggle(self):
self.backend.current_playlist.append([Track()])
- result = self.dispatcher.handle_request(u'play "0"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'play "0"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
- result = self.dispatcher.handle_request(u'pause')
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
+
+ self.sendRequest(u'pause')
self.assertEqual(PAUSED, self.backend.playback.state.get())
- result = self.dispatcher.handle_request(u'pause')
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
+
+ self.sendRequest(u'pause')
self.assertEqual(PLAYING, self.backend.playback.state.get())
+ self.assertInResponse(u'OK')
def test_play_without_pos(self):
self.backend.current_playlist.append([Track()])
self.backend.playback.state = PAUSED
- result = self.dispatcher.handle_request(u'play')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'play')
self.assertEqual(PLAYING, self.backend.playback.state.get())
+ self.assertInResponse(u'OK')
def test_play_with_pos(self):
self.backend.current_playlist.append([Track()])
- result = self.dispatcher.handle_request(u'play "0"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'play "0"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
+ self.assertInResponse(u'OK')
def test_play_with_pos_without_quotes(self):
self.backend.current_playlist.append([Track()])
- result = self.dispatcher.handle_request(u'play 0')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'play 0')
self.assertEqual(PLAYING, self.backend.playback.state.get())
+ self.assertInResponse(u'OK')
def test_play_with_pos_out_of_bounds(self):
self.backend.current_playlist.append([])
- result = self.dispatcher.handle_request(u'play "0"')
- self.assertEqual(result[0], u'ACK [2@0] {play} Bad song index')
+
+ self.sendRequest(u'play "0"')
self.assertEqual(STOPPED, self.backend.playback.state.get())
+ self.assertInResponse(u'ACK [2@0] {play} Bad song index')
def test_play_minus_one_plays_first_in_playlist_if_no_current_track(self):
self.assertEqual(self.backend.playback.current_track.get(), None)
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- result = self.dispatcher.handle_request(u'play "-1"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'play "-1"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
- self.assertEqual(self.backend.playback.current_track.get().uri, 'a')
+ self.assertEqual('a', self.backend.playback.current_track.get().uri)
+ self.assertInResponse(u'OK')
def test_play_minus_one_plays_current_track_if_current_track_is_set(self):
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
@@ -250,27 +241,30 @@ class PlaybackControlHandlerTest(unittest.TestCase):
self.backend.playback.next()
self.backend.playback.stop()
self.assertNotEqual(self.backend.playback.current_track.get(), None)
- result = self.dispatcher.handle_request(u'play "-1"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'play "-1"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
- self.assertEqual(self.backend.playback.current_track.get().uri, 'b')
+ self.assertEqual('b', self.backend.playback.current_track.get().uri)
+ self.assertInResponse(u'OK')
def test_play_minus_one_on_empty_playlist_does_not_ack(self):
self.backend.current_playlist.clear()
- result = self.dispatcher.handle_request(u'play "-1"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'play "-1"')
self.assertEqual(STOPPED, self.backend.playback.state.get())
- self.assertEqual(self.backend.playback.current_track.get(), None)
+ self.assertEqual(None, self.backend.playback.current_track.get())
+ self.assertInResponse(u'OK')
def test_play_minus_is_ignored_if_playing(self):
self.backend.current_playlist.append([Track(length=40000)])
self.backend.playback.seek(30000)
self.assert_(self.backend.playback.time_position.get() >= 30000)
self.assertEquals(PLAYING, self.backend.playback.state.get())
- result = self.dispatcher.handle_request(u'play "-1"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'play "-1"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assert_(self.backend.playback.time_position.get() >= 30000)
+ self.assertInResponse(u'OK')
def test_play_minus_one_resumes_if_paused(self):
self.backend.current_playlist.append([Track(length=40000)])
@@ -279,24 +273,27 @@ class PlaybackControlHandlerTest(unittest.TestCase):
self.assertEquals(PLAYING, self.backend.playback.state.get())
self.backend.playback.pause()
self.assertEquals(PAUSED, self.backend.playback.state.get())
- result = self.dispatcher.handle_request(u'play "-1"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'play "-1"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assert_(self.backend.playback.time_position.get() >= 30000)
+ self.assertInResponse(u'OK')
def test_playid(self):
self.backend.current_playlist.append([Track()])
- result = self.dispatcher.handle_request(u'playid "0"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'playid "0"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
+ self.assertInResponse(u'OK')
def test_playid_minus_one_plays_first_in_playlist_if_no_current_track(self):
self.assertEqual(self.backend.playback.current_track.get(), None)
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- result = self.dispatcher.handle_request(u'playid "-1"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'playid "-1"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
- self.assertEqual(self.backend.playback.current_track.get().uri, 'a')
+ self.assertEqual('a', self.backend.playback.current_track.get().uri)
+ self.assertInResponse(u'OK')
def test_playid_minus_one_plays_current_track_if_current_track_is_set(self):
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
@@ -304,28 +301,31 @@ class PlaybackControlHandlerTest(unittest.TestCase):
self.backend.playback.play()
self.backend.playback.next()
self.backend.playback.stop()
- self.assertNotEqual(self.backend.playback.current_track.get(), None)
- result = self.dispatcher.handle_request(u'playid "-1"')
- self.assert_(u'OK' in result)
+ self.assertNotEqual(None, self.backend.playback.current_track.get())
+
+ self.sendRequest(u'playid "-1"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
- self.assertEqual(self.backend.playback.current_track.get().uri, 'b')
+ self.assertEqual('b', self.backend.playback.current_track.get().uri)
+ self.assertInResponse(u'OK')
def test_playid_minus_one_on_empty_playlist_does_not_ack(self):
self.backend.current_playlist.clear()
- result = self.dispatcher.handle_request(u'playid "-1"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'playid "-1"')
self.assertEqual(STOPPED, self.backend.playback.state.get())
- self.assertEqual(self.backend.playback.current_track.get(), None)
+ self.assertEqual(None, self.backend.playback.current_track.get())
+ self.assertInResponse(u'OK')
def test_playid_minus_is_ignored_if_playing(self):
self.backend.current_playlist.append([Track(length=40000)])
self.backend.playback.seek(30000)
self.assert_(self.backend.playback.time_position.get() >= 30000)
self.assertEquals(PLAYING, self.backend.playback.state.get())
- result = self.dispatcher.handle_request(u'playid "-1"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'playid "-1"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assert_(self.backend.playback.time_position.get() >= 30000)
+ self.assertInResponse(u'OK')
def test_playid_minus_one_resumes_if_paused(self):
self.backend.current_playlist.append([Track(length=40000)])
@@ -334,58 +334,64 @@ class PlaybackControlHandlerTest(unittest.TestCase):
self.assertEquals(PLAYING, self.backend.playback.state.get())
self.backend.playback.pause()
self.assertEquals(PAUSED, self.backend.playback.state.get())
- result = self.dispatcher.handle_request(u'playid "-1"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'playid "-1"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assert_(self.backend.playback.time_position.get() >= 30000)
+ self.assertInResponse(u'OK')
def test_playid_which_does_not_exist(self):
self.backend.current_playlist.append([Track()])
- result = self.dispatcher.handle_request(u'playid "12345"')
- self.assertEqual(result[0], u'ACK [50@0] {playid} No such song')
+
+ self.sendRequest(u'playid "12345"')
+ self.assertInResponse(u'ACK [50@0] {playid} No such song')
def test_previous(self):
- result = self.dispatcher.handle_request(u'previous')
- self.assert_(u'OK' in result)
+ self.sendRequest(u'previous')
+ self.assertInResponse(u'OK')
def test_seek(self):
self.backend.current_playlist.append([Track(length=40000)])
- self.dispatcher.handle_request(u'seek "0"')
- result = self.dispatcher.handle_request(u'seek "0" "30"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'seek "0"')
+ self.sendRequest(u'seek "0" "30"')
self.assert_(self.backend.playback.time_position >= 30000)
+ self.assertInResponse(u'OK')
def test_seek_with_songpos(self):
seek_track = Track(uri='2', length=40000)
self.backend.current_playlist.append(
[Track(uri='1', length=40000), seek_track])
- result = self.dispatcher.handle_request(u'seek "1" "30"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'seek "1" "30"')
self.assertEqual(self.backend.playback.current_track.get(), seek_track)
+ self.assertInResponse(u'OK')
def test_seek_without_quotes(self):
self.backend.current_playlist.append([Track(length=40000)])
- self.dispatcher.handle_request(u'seek 0')
- result = self.dispatcher.handle_request(u'seek 0 30')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'seek 0')
+ self.sendRequest(u'seek 0 30')
self.assert_(self.backend.playback.time_position.get() >= 30000)
+ self.assertInResponse(u'OK')
def test_seekid(self):
self.backend.current_playlist.append([Track(length=40000)])
- result = self.dispatcher.handle_request(u'seekid "0" "30"')
- self.assert_(u'OK' in result)
+ self.sendRequest(u'seekid "0" "30"')
self.assert_(self.backend.playback.time_position.get() >= 30000)
+ self.assertInResponse(u'OK')
def test_seekid_with_cpid(self):
seek_track = Track(uri='2', length=40000)
self.backend.current_playlist.append(
[Track(length=40000), seek_track])
- result = self.dispatcher.handle_request(u'seekid "1" "30"')
- self.assert_(u'OK' in result)
- self.assertEqual(self.backend.playback.current_cpid.get(), 1)
- self.assertEqual(self.backend.playback.current_track.get(), seek_track)
+
+ self.sendRequest(u'seekid "1" "30"')
+ self.assertEqual(1, self.backend.playback.current_cpid.get())
+ self.assertEqual(seek_track, self.backend.playback.current_track.get())
+ self.assertInResponse(u'OK')
def test_stop(self):
- result = self.dispatcher.handle_request(u'stop')
- self.assert_(u'OK' in result)
+ self.sendRequest(u'stop')
self.assertEqual(STOPPED, self.backend.playback.state.get())
+ self.assertInResponse(u'OK')
diff --git a/tests/frontends/mpd/protocol/reflection_test.py b/tests/frontends/mpd/protocol/reflection_test.py
new file mode 100644
index 00000000..8bd9b7e0
--- /dev/null
+++ b/tests/frontends/mpd/protocol/reflection_test.py
@@ -0,0 +1,67 @@
+from mopidy import settings
+
+from tests.frontends.mpd import protocol
+
+
+class ReflectionHandlerTest(protocol.BaseTestCase):
+ def test_commands_returns_list_of_all_commands(self):
+ self.sendRequest(u'commands')
+ # Check if some random commands are included
+ self.assertInResponse(u'command: commands')
+ self.assertInResponse(u'command: play')
+ self.assertInResponse(u'command: status')
+ # Check if commands you do not have access to are not present
+ self.assertNotInResponse(u'command: kill')
+ # Check if the blacklisted commands are not present
+ self.assertNotInResponse(u'command: command_list_begin')
+ self.assertNotInResponse(u'command: command_list_ok_begin')
+ self.assertNotInResponse(u'command: command_list_end')
+ self.assertNotInResponse(u'command: idle')
+ self.assertNotInResponse(u'command: noidle')
+ self.assertNotInResponse(u'command: sticker')
+ self.assertInResponse(u'OK')
+
+ def test_commands_show_less_if_auth_required_and_not_authed(self):
+ settings.MPD_SERVER_PASSWORD = u'secret'
+ self.sendRequest(u'commands')
+ # Not requiring auth
+ self.assertInResponse(u'command: close')
+ self.assertInResponse(u'command: commands')
+ self.assertInResponse(u'command: notcommands')
+ self.assertInResponse(u'command: password')
+ self.assertInResponse(u'command: ping')
+ # Requiring auth
+ self.assertNotInResponse(u'command: play')
+ self.assertNotInResponse(u'command: status')
+
+ def test_decoders(self):
+ self.sendRequest(u'decoders')
+ self.assertInResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_notcommands_returns_only_kill_and_ok(self):
+ response = self.sendRequest(u'notcommands')
+ self.assertEqual(2, len(response))
+ self.assertInResponse(u'command: kill')
+ self.assertInResponse(u'OK')
+
+ def test_notcommands_returns_more_if_auth_required_and_not_authed(self):
+ settings.MPD_SERVER_PASSWORD = u'secret'
+ self.sendRequest(u'notcommands')
+ # Not requiring auth
+ self.assertNotInResponse(u'command: close')
+ self.assertNotInResponse(u'command: commands')
+ self.assertNotInResponse(u'command: notcommands')
+ self.assertNotInResponse(u'command: password')
+ self.assertNotInResponse(u'command: ping')
+ # Requiring auth
+ self.assertInResponse(u'command: play')
+ self.assertInResponse(u'command: status')
+
+ def test_tagtypes(self):
+ self.sendRequest(u'tagtypes')
+ self.assertInResponse(u'OK')
+
+ def test_urlhandlers(self):
+ self.sendRequest(u'urlhandlers')
+ self.assertInResponse(u'OK')
+ self.assertInResponse(u'handler: dummy')
diff --git a/tests/frontends/mpd/protocol/regression_test.py b/tests/frontends/mpd/protocol/regression_test.py
new file mode 100644
index 00000000..7f214efa
--- /dev/null
+++ b/tests/frontends/mpd/protocol/regression_test.py
@@ -0,0 +1,164 @@
+import random
+
+from mopidy.models import Track
+
+from tests.frontends.mpd import protocol
+
+
+class IssueGH17RegressionTest(protocol.BaseTestCase):
+ """
+ The issue: http://github.com/mopidy/mopidy/issues/17
+
+ How to reproduce:
+
+ - Play a playlist where one track cannot be played
+ - Turn on random mode
+ - Press next until you get to the unplayable track
+ """
+ def test(self):
+ self.backend.current_playlist.append([
+ Track(uri='a'), Track(uri='b'), None,
+ Track(uri='d'), Track(uri='e'), Track(uri='f')])
+ random.seed(1) # Playlist order: abcfde
+
+ self.sendRequest(u'play')
+ self.assertEquals('a', self.backend.playback.current_track.get().uri)
+ self.sendRequest(u'random "1"')
+ self.sendRequest(u'next')
+ self.assertEquals('b', self.backend.playback.current_track.get().uri)
+ self.sendRequest(u'next')
+ # Should now be at track 'c', but playback fails and it skips ahead
+ self.assertEquals('f', self.backend.playback.current_track.get().uri)
+ self.sendRequest(u'next')
+ self.assertEquals('d', self.backend.playback.current_track.get().uri)
+ self.sendRequest(u'next')
+ self.assertEquals('e', self.backend.playback.current_track.get().uri)
+
+
+class IssueGH18RegressionTest(protocol.BaseTestCase):
+ """
+ The issue: http://github.com/mopidy/mopidy/issues/18
+
+ How to reproduce:
+
+ Play, random on, next, random off, next, next.
+
+ At this point it gives the same song over and over.
+ """
+
+ def test(self):
+ self.backend.current_playlist.append([
+ Track(uri='a'), Track(uri='b'), Track(uri='c'),
+ Track(uri='d'), Track(uri='e'), Track(uri='f')])
+ random.seed(1)
+
+ self.sendRequest(u'play')
+ self.sendRequest(u'random "1"')
+ self.sendRequest(u'next')
+ self.sendRequest(u'random "0"')
+ self.sendRequest(u'next')
+
+ self.sendRequest(u'next')
+ cp_track_1 = self.backend.playback.current_cp_track.get()
+ self.sendRequest(u'next')
+ cp_track_2 = self.backend.playback.current_cp_track.get()
+ self.sendRequest(u'next')
+ cp_track_3 = self.backend.playback.current_cp_track.get()
+
+ self.assertNotEqual(cp_track_1, cp_track_2)
+ self.assertNotEqual(cp_track_2, cp_track_3)
+
+
+class IssueGH22RegressionTest(protocol.BaseTestCase):
+ """
+ The issue: http://github.com/mopidy/mopidy/issues/22
+
+ How to reproduce:
+
+ Play, random on, remove all tracks from the current playlist (as in
+ "delete" each one, not "clear").
+
+ Alternatively: Play, random on, remove a random track from the current
+ playlist, press next until it crashes.
+ """
+
+ def test(self):
+ self.backend.current_playlist.append([
+ Track(uri='a'), Track(uri='b'), Track(uri='c'),
+ Track(uri='d'), Track(uri='e'), Track(uri='f')])
+ random.seed(1)
+
+ self.sendRequest(u'play')
+ self.sendRequest(u'random "1"')
+ self.sendRequest(u'deleteid "1"')
+ self.sendRequest(u'deleteid "2"')
+ self.sendRequest(u'deleteid "3"')
+ self.sendRequest(u'deleteid "4"')
+ self.sendRequest(u'deleteid "5"')
+ self.sendRequest(u'deleteid "6"')
+ self.sendRequest(u'status')
+
+
+class IssueGH69RegressionTest(protocol.BaseTestCase):
+ """
+ The issue: https://github.com/mopidy/mopidy/issues/69
+
+ How to reproduce:
+
+ Play track, stop, clear current playlist, load a new playlist, status.
+
+ The status response now contains "song: None".
+ """
+
+ def test(self):
+ self.backend.stored_playlists.create('foo')
+ self.backend.current_playlist.append([
+ Track(uri='a'), Track(uri='b'), Track(uri='c'),
+ Track(uri='d'), Track(uri='e'), Track(uri='f')])
+
+ self.sendRequest(u'play')
+ self.sendRequest(u'stop')
+ self.sendRequest(u'clear')
+ self.sendRequest(u'load "foo"')
+ self.assertNotInResponse('song: None')
+
+
+class IssueGH113RegressionTest(protocol.BaseTestCase):
+ """
+ The issue: https://github.com/mopidy/mopidy/issues/113
+
+ How to reproduce:
+
+ - Have a playlist with a name contining backslashes, like
+ "all lart spotify:track:\w\{22\} pastes".
+ - Try to load the playlist with the backslashes in the playlist name
+ escaped.
+ """
+
+ def test(self):
+ self.backend.stored_playlists.create(
+ u'all lart spotify:track:\w\{22\} pastes')
+
+ self.sendRequest(u'lsinfo "/"')
+ self.assertInResponse(
+ u'playlist: all lart spotify:track:\w\{22\} pastes')
+
+ self.sendRequest(
+ r'listplaylistinfo "all lart spotify:track:\\w\\{22\\} pastes"')
+ self.assertInResponse('OK')
+
+
+class IssueGH137RegressionTest(protocol.BaseTestCase):
+ """
+ The issue: https://github.com/mopidy/mopidy/issues/137
+
+ How to reproduce:
+
+ - Send "list" query with mismatching quotes
+ """
+
+ def test(self):
+ self.sendRequest(u'list Date Artist "Anita Ward" '
+ u'Album "This Is Remixed Hits - Mashups & Rare 12" Mixes"')
+
+ self.assertInResponse('ACK [2@0] {list} Invalid unquoted character')
diff --git a/tests/frontends/mpd/protocol/status_test.py b/tests/frontends/mpd/protocol/status_test.py
new file mode 100644
index 00000000..e6572eab
--- /dev/null
+++ b/tests/frontends/mpd/protocol/status_test.py
@@ -0,0 +1,37 @@
+from mopidy.models import Track
+
+from tests.frontends.mpd import protocol
+
+
+class StatusHandlerTest(protocol.BaseTestCase):
+ def test_clearerror(self):
+ self.sendRequest(u'clearerror')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_currentsong(self):
+ track = Track()
+ self.backend.current_playlist.append([track])
+ self.backend.playback.play()
+ self.sendRequest(u'currentsong')
+ self.assertInResponse(u'file: ')
+ self.assertInResponse(u'Time: 0')
+ self.assertInResponse(u'Artist: ')
+ self.assertInResponse(u'Title: ')
+ self.assertInResponse(u'Album: ')
+ self.assertInResponse(u'Track: 0')
+ self.assertInResponse(u'Date: ')
+ self.assertInResponse(u'Pos: 0')
+ self.assertInResponse(u'Id: 0')
+ self.assertInResponse(u'OK')
+
+ def test_currentsong_without_song(self):
+ self.sendRequest(u'currentsong')
+ self.assertInResponse(u'OK')
+
+ def test_stats_command(self):
+ self.sendRequest(u'stats')
+ self.assertInResponse(u'OK')
+
+ def test_status_command(self):
+ self.sendRequest(u'status')
+ self.assertInResponse(u'OK')
diff --git a/tests/frontends/mpd/protocol/stickers_test.py b/tests/frontends/mpd/protocol/stickers_test.py
new file mode 100644
index 00000000..3e8b687f
--- /dev/null
+++ b/tests/frontends/mpd/protocol/stickers_test.py
@@ -0,0 +1,33 @@
+from tests.frontends.mpd import protocol
+
+
+class StickersHandlerTest(protocol.BaseTestCase):
+ def test_sticker_get(self):
+ self.sendRequest(
+ u'sticker get "song" "file:///dev/urandom" "a_name"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_sticker_set(self):
+ self.sendRequest(
+ u'sticker set "song" "file:///dev/urandom" "a_name" "a_value"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_sticker_delete_with_name(self):
+ self.sendRequest(
+ u'sticker delete "song" "file:///dev/urandom" "a_name"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_sticker_delete_without_name(self):
+ self.sendRequest(
+ u'sticker delete "song" "file:///dev/urandom"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_sticker_list(self):
+ self.sendRequest(
+ u'sticker list "song" "file:///dev/urandom"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_sticker_find(self):
+ self.sendRequest(
+ u'sticker find "song" "file:///dev/urandom" "a_name"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
diff --git a/tests/frontends/mpd/protocol/stored_playlists_test.py b/tests/frontends/mpd/protocol/stored_playlists_test.py
new file mode 100644
index 00000000..45d6a09a
--- /dev/null
+++ b/tests/frontends/mpd/protocol/stored_playlists_test.py
@@ -0,0 +1,94 @@
+import datetime
+
+from mopidy.models import Track, Playlist
+
+from tests.frontends.mpd import protocol
+
+
+class StoredPlaylistsHandlerTest(protocol.BaseTestCase):
+ def test_listplaylist(self):
+ self.backend.stored_playlists.playlists = [
+ Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])]
+
+ self.sendRequest(u'listplaylist "name"')
+ self.assertInResponse(u'file: file:///dev/urandom')
+ self.assertInResponse(u'OK')
+
+ def test_listplaylist_fails_if_no_playlist_is_found(self):
+ self.sendRequest(u'listplaylist "name"')
+ self.assertEqualResponse(u'ACK [50@0] {listplaylist} No such playlist')
+
+ def test_listplaylistinfo(self):
+ self.backend.stored_playlists.playlists = [
+ Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])]
+
+ self.sendRequest(u'listplaylistinfo "name"')
+ self.assertInResponse(u'file: file:///dev/urandom')
+ self.assertInResponse(u'Track: 0')
+ self.assertNotInResponse(u'Pos: 0')
+ self.assertInResponse(u'OK')
+
+ def test_listplaylistinfo_fails_if_no_playlist_is_found(self):
+ self.sendRequest(u'listplaylistinfo "name"')
+ self.assertEqualResponse(
+ u'ACK [50@0] {listplaylistinfo} No such playlist')
+
+ def test_listplaylists(self):
+ last_modified = datetime.datetime(2001, 3, 17, 13, 41, 17, 12345)
+ self.backend.stored_playlists.playlists = [Playlist(name='a',
+ last_modified=last_modified)]
+
+ self.sendRequest(u'listplaylists')
+ self.assertInResponse(u'playlist: a')
+ # Date without microseconds and with time zone information
+ self.assertInResponse(u'Last-Modified: 2001-03-17T13:41:17Z')
+ self.assertInResponse(u'OK')
+
+ def test_load_known_playlist_appends_to_current_playlist(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
+ self.backend.stored_playlists.playlists = [Playlist(name='A-list',
+ tracks=[Track(uri='c'), Track(uri='d'), Track(uri='e')])]
+
+ self.sendRequest(u'load "A-list"')
+ tracks = self.backend.current_playlist.tracks.get()
+ self.assertEqual(5, len(tracks))
+ self.assertEqual('a', tracks[0].uri)
+ self.assertEqual('b', tracks[1].uri)
+ self.assertEqual('c', tracks[2].uri)
+ self.assertEqual('d', tracks[3].uri)
+ self.assertEqual('e', tracks[4].uri)
+ self.assertInResponse(u'OK')
+
+ def test_load_unknown_playlist_acks(self):
+ self.sendRequest(u'load "unknown playlist"')
+ self.assertEqual(0, len(self.backend.current_playlist.tracks.get()))
+ self.assertEqualResponse(u'ACK [50@0] {load} No such playlist')
+
+ def test_playlistadd(self):
+ self.sendRequest(u'playlistadd "name" "file:///dev/urandom"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_playlistclear(self):
+ self.sendRequest(u'playlistclear "name"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_playlistdelete(self):
+ self.sendRequest(u'playlistdelete "name" "5"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_playlistmove(self):
+ self.sendRequest(u'playlistmove "name" "5" "10"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_rename(self):
+ self.sendRequest(u'rename "old_name" "new_name"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_rm(self):
+ self.sendRequest(u'rm "name"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_save(self):
+ self.sendRequest(u'save "name"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
diff --git a/tests/frontends/mpd/reflection_test.py b/tests/frontends/mpd/reflection_test.py
deleted file mode 100644
index c4fd632a..00000000
--- a/tests/frontends/mpd/reflection_test.py
+++ /dev/null
@@ -1,79 +0,0 @@
-import unittest
-
-from mopidy import settings
-from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd.dispatcher import MpdDispatcher
-from mopidy.mixers.dummy import DummyMixer
-
-class ReflectionHandlerTest(unittest.TestCase):
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.dispatcher = MpdDispatcher()
-
- def tearDown(self):
- settings.runtime.clear()
- self.backend.stop().get()
- self.mixer.stop().get()
-
- def test_commands_returns_list_of_all_commands(self):
- result = self.dispatcher.handle_request(u'commands')
- # Check if some random commands are included
- self.assert_(u'command: commands' in result)
- self.assert_(u'command: play' in result)
- self.assert_(u'command: status' in result)
- # Check if commands you do not have access to are not present
- self.assert_(u'command: kill' not in result)
- # Check if the blacklisted commands are not present
- self.assert_(u'command: command_list_begin' not in result)
- self.assert_(u'command: command_list_ok_begin' not in result)
- self.assert_(u'command: command_list_end' not in result)
- self.assert_(u'command: idle' not in result)
- self.assert_(u'command: noidle' not in result)
- self.assert_(u'command: sticker' not in result)
- self.assert_(u'OK' in result)
-
- def test_commands_show_less_if_auth_required_and_not_authed(self):
- settings.MPD_SERVER_PASSWORD = u'secret'
- result = self.dispatcher.handle_request(u'commands')
- # Not requiring auth
- self.assert_(u'command: close' in result, result)
- self.assert_(u'command: commands' in result, result)
- self.assert_(u'command: notcommands' in result, result)
- self.assert_(u'command: password' in result, result)
- self.assert_(u'command: ping' in result, result)
- # Requiring auth
- self.assert_(u'command: play' not in result, result)
- self.assert_(u'command: status' not in result, result)
-
- def test_decoders(self):
- result = self.dispatcher.handle_request(u'decoders')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_notcommands_returns_only_kill_and_ok(self):
- result = self.dispatcher.handle_request(u'notcommands')
- self.assertEqual(2, len(result))
- self.assert_(u'command: kill' in result)
- self.assert_(u'OK' in result)
-
- def test_notcommands_returns_more_if_auth_required_and_not_authed(self):
- settings.MPD_SERVER_PASSWORD = u'secret'
- result = self.dispatcher.handle_request(u'notcommands')
- # Not requiring auth
- self.assert_(u'command: close' not in result, result)
- self.assert_(u'command: commands' not in result, result)
- self.assert_(u'command: notcommands' not in result, result)
- self.assert_(u'command: password' not in result, result)
- self.assert_(u'command: ping' not in result, result)
- # Requiring auth
- self.assert_(u'command: play' in result, result)
- self.assert_(u'command: status' in result, result)
-
- def test_tagtypes(self):
- result = self.dispatcher.handle_request(u'tagtypes')
- self.assert_(u'OK' in result)
-
- def test_urlhandlers(self):
- result = self.dispatcher.handle_request(u'urlhandlers')
- self.assert_(u'OK' in result)
- self.assert_(u'handler: dummy' in result)
diff --git a/tests/frontends/mpd/regression_test.py b/tests/frontends/mpd/regression_test.py
deleted file mode 100644
index f786cf0a..00000000
--- a/tests/frontends/mpd/regression_test.py
+++ /dev/null
@@ -1,158 +0,0 @@
-import random
-import unittest
-
-from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd import dispatcher
-from mopidy.mixers.dummy import DummyMixer
-from mopidy.models import Track
-
-class IssueGH17RegressionTest(unittest.TestCase):
- """
- The issue: http://github.com/mopidy/mopidy/issues#issue/17
-
- How to reproduce:
-
- - Play a playlist where one track cannot be played
- - Turn on random mode
- - Press next until you get to the unplayable track
- """
-
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.backend.current_playlist.append([
- Track(uri='a'), Track(uri='b'), None,
- Track(uri='d'), Track(uri='e'), Track(uri='f')])
- self.mixer = DummyMixer.start().proxy()
- self.mpd = dispatcher.MpdDispatcher()
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
-
- def test(self):
- random.seed(1) # Playlist order: abcfde
- self.mpd.handle_request(u'play')
- self.assertEquals('a', self.backend.playback.current_track.get().uri)
- self.mpd.handle_request(u'random "1"')
- self.mpd.handle_request(u'next')
- self.assertEquals('b', self.backend.playback.current_track.get().uri)
- self.mpd.handle_request(u'next')
- # Should now be at track 'c', but playback fails and it skips ahead
- self.assertEquals('f', self.backend.playback.current_track.get().uri)
- self.mpd.handle_request(u'next')
- self.assertEquals('d', self.backend.playback.current_track.get().uri)
- self.mpd.handle_request(u'next')
- self.assertEquals('e', self.backend.playback.current_track.get().uri)
-
-
-class IssueGH18RegressionTest(unittest.TestCase):
- """
- The issue: http://github.com/mopidy/mopidy/issues#issue/18
-
- How to reproduce:
-
- Play, random on, next, random off, next, next.
-
- At this point it gives the same song over and over.
- """
-
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.backend.current_playlist.append([
- Track(uri='a'), Track(uri='b'), Track(uri='c'),
- Track(uri='d'), Track(uri='e'), Track(uri='f')])
- self.mixer = DummyMixer.start().proxy()
- self.mpd = dispatcher.MpdDispatcher()
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
-
- def test(self):
- random.seed(1)
- self.mpd.handle_request(u'play')
- self.mpd.handle_request(u'random "1"')
- self.mpd.handle_request(u'next')
- self.mpd.handle_request(u'random "0"')
- self.mpd.handle_request(u'next')
-
- self.mpd.handle_request(u'next')
- cp_track_1 = self.backend.playback.current_cp_track.get()
- self.mpd.handle_request(u'next')
- cp_track_2 = self.backend.playback.current_cp_track.get()
- self.mpd.handle_request(u'next')
- cp_track_3 = self.backend.playback.current_cp_track.get()
-
- self.assertNotEqual(cp_track_1, cp_track_2)
- self.assertNotEqual(cp_track_2, cp_track_3)
-
-
-class IssueGH22RegressionTest(unittest.TestCase):
- """
- The issue: http://github.com/mopidy/mopidy/issues/#issue/22
-
- How to reproduce:
-
- Play, random on, remove all tracks from the current playlist (as in
- "delete" each one, not "clear").
-
- Alternatively: Play, random on, remove a random track from the current
- playlist, press next until it crashes.
- """
-
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.backend.current_playlist.append([
- Track(uri='a'), Track(uri='b'), Track(uri='c'),
- Track(uri='d'), Track(uri='e'), Track(uri='f')])
- self.mixer = DummyMixer.start().proxy()
- self.mpd = dispatcher.MpdDispatcher()
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
-
- def test(self):
- random.seed(1)
- self.mpd.handle_request(u'play')
- self.mpd.handle_request(u'random "1"')
- self.mpd.handle_request(u'deleteid "1"')
- self.mpd.handle_request(u'deleteid "2"')
- self.mpd.handle_request(u'deleteid "3"')
- self.mpd.handle_request(u'deleteid "4"')
- self.mpd.handle_request(u'deleteid "5"')
- self.mpd.handle_request(u'deleteid "6"')
- self.mpd.handle_request(u'status')
-
-
-class IssueGH69RegressionTest(unittest.TestCase):
- """
- The issue: https://github.com/mopidy/mopidy/issues#issue/69
-
- How to reproduce:
-
- Play track, stop, clear current playlist, load a new playlist, status.
-
- The status response now contains "song: None".
- """
-
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.backend.current_playlist.append([
- Track(uri='a'), Track(uri='b'), Track(uri='c'),
- Track(uri='d'), Track(uri='e'), Track(uri='f')])
- self.backend.stored_playlists.create('foo')
- self.mixer = DummyMixer.start().proxy()
- self.mpd = dispatcher.MpdDispatcher()
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
-
- def test(self):
- self.mpd.handle_request(u'play')
- self.mpd.handle_request(u'stop')
- self.mpd.handle_request(u'clear')
- self.mpd.handle_request(u'load "foo"')
- response = self.mpd.handle_request(u'status')
- self.assert_('song: None' not in response)
diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py
index b0c57588..a20abaed 100644
--- a/tests/frontends/mpd/serializer_test.py
+++ b/tests/frontends/mpd/serializer_test.py
@@ -1,11 +1,13 @@
-import datetime as dt
+import datetime
import os
-import unittest
from mopidy import settings
from mopidy.utils.path import mtime, uri_to_path
from mopidy.frontends.mpd import translator, protocol
-from mopidy.models import Album, Artist, Playlist, Track
+from mopidy.models import Album, Artist, CpTrack, Playlist, Track
+
+from tests import unittest
+
class TrackMpdFormatTest(unittest.TestCase):
track = Track(
@@ -15,7 +17,7 @@ class TrackMpdFormatTest(unittest.TestCase):
album=Album(name=u'an album', num_tracks=13,
artists=[Artist(name=u'an other artist')]),
track_no=7,
- date=dt.date(1977, 1, 1),
+ date=datetime.date(1977, 1, 1),
length=137000,
)
@@ -43,17 +45,17 @@ class TrackMpdFormatTest(unittest.TestCase):
self.assert_(('Pos', 1) not in result)
def test_track_to_mpd_format_with_cpid(self):
- result = translator.track_to_mpd_format(Track(), cpid=1)
+ result = translator.track_to_mpd_format(CpTrack(1, Track()))
self.assert_(('Id', 1) not in result)
def test_track_to_mpd_format_with_position_and_cpid(self):
- result = translator.track_to_mpd_format(Track(), position=1, cpid=2)
+ result = translator.track_to_mpd_format(CpTrack(2, Track()), position=1)
self.assert_(('Pos', 1) in result)
self.assert_(('Id', 2) in result)
def test_track_to_mpd_format_for_nonempty_track(self):
result = translator.track_to_mpd_format(
- self.track, position=9, cpid=122)
+ CpTrack(122, self.track), position=9)
self.assert_(('file', 'a uri') in result)
self.assert_(('Time', 137) in result)
self.assert_(('Artist', 'an artist') in result)
@@ -61,7 +63,7 @@ class TrackMpdFormatTest(unittest.TestCase):
self.assert_(('Album', 'an album') in result)
self.assert_(('AlbumArtist', 'an other artist') in result)
self.assert_(('Track', '7/13') in result)
- self.assert_(('Date', dt.date(1977, 1, 1)) in result)
+ self.assert_(('Date', datetime.date(1977, 1, 1)) in result)
self.assert_(('Pos', 9) in result)
self.assert_(('Id', 122) in result)
self.assertEqual(len(result), 10)
diff --git a/tests/frontends/mpd/server_test.py b/tests/frontends/mpd/server_test.py
deleted file mode 100644
index b2e27559..00000000
--- a/tests/frontends/mpd/server_test.py
+++ /dev/null
@@ -1,23 +0,0 @@
-import unittest
-
-from mopidy import settings
-from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd import server
-from mopidy.mixers.dummy import DummyMixer
-
-class MpdSessionTest(unittest.TestCase):
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.session = server.MpdSession(None, None, (None, None))
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
- settings.runtime.clear()
-
- def test_found_terminator_catches_decode_error(self):
- # Pressing Ctrl+C in a telnet session sends a 0xff byte to the server.
- self.session.input_buffer = ['\xff']
- self.session.found_terminator()
- self.assertEqual(len(self.session.input_buffer), 0)
diff --git a/tests/frontends/mpd/status_test.py b/tests/frontends/mpd/status_test.py
index a7ed921f..bdd2dab8 100644
--- a/tests/frontends/mpd/status_test.py
+++ b/tests/frontends/mpd/status_test.py
@@ -1,67 +1,30 @@
-import unittest
-
-from mopidy.backends.base import PlaybackController
-from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd.dispatcher import MpdDispatcher
+from mopidy.backends import dummy as backend
+from mopidy.frontends.mpd import dispatcher
from mopidy.frontends.mpd.protocol import status
-from mopidy.mixers.dummy import DummyMixer
+from mopidy.mixers import dummy as mixer
from mopidy.models import Track
-PAUSED = PlaybackController.PAUSED
-PLAYING = PlaybackController.PLAYING
-STOPPED = PlaybackController.STOPPED
+from tests import unittest
+
+PAUSED = backend.PlaybackController.PAUSED
+PLAYING = backend.PlaybackController.PLAYING
+STOPPED = backend.PlaybackController.STOPPED
+
+# FIXME migrate to using protocol.BaseTestCase instead of status.stats
+# directly?
+
class StatusHandlerTest(unittest.TestCase):
def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.dispatcher = MpdDispatcher()
+ self.backend = backend.DummyBackend.start().proxy()
+ self.mixer = mixer.DummyMixer.start().proxy()
+ self.dispatcher = dispatcher.MpdDispatcher()
self.context = self.dispatcher.context
def tearDown(self):
self.backend.stop().get()
self.mixer.stop().get()
- def test_clearerror(self):
- result = self.dispatcher.handle_request(u'clearerror')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_currentsong(self):
- track = Track()
- self.backend.current_playlist.append([track])
- self.backend.playback.play()
- result = self.dispatcher.handle_request(u'currentsong')
- self.assert_(u'file: ' in result)
- self.assert_(u'Time: 0' in result)
- self.assert_(u'Artist: ' in result)
- self.assert_(u'Title: ' in result)
- self.assert_(u'Album: ' in result)
- self.assert_(u'Track: 0' in result)
- self.assert_(u'Date: ' in result)
- self.assert_(u'Pos: 0' in result)
- self.assert_(u'Id: 0' in result)
- self.assert_(u'OK' in result)
-
- def test_currentsong_without_song(self):
- result = self.dispatcher.handle_request(u'currentsong')
- self.assert_(u'OK' in result)
-
- def test_idle_without_subsystems(self):
- result = self.dispatcher.handle_request(u'idle')
- self.assert_(u'OK' in result)
-
- def test_idle_with_subsystems(self):
- result = self.dispatcher.handle_request(u'idle database playlist')
- self.assert_(u'OK' in result)
-
- def test_noidle(self):
- result = self.dispatcher.handle_request(u'noidle')
- self.assert_(u'OK' in result)
-
- def test_stats_command(self):
- result = self.dispatcher.handle_request(u'stats')
- self.assert_(u'OK' in result)
-
def test_stats_method(self):
result = status.stats(self.context)
self.assert_('artists' in result)
@@ -79,10 +42,6 @@ class StatusHandlerTest(unittest.TestCase):
self.assert_('playtime' in result)
self.assert_(int(result['playtime']) >= 0)
- def test_status_command(self):
- result = self.dispatcher.handle_request(u'status')
- self.assert_(u'OK' in result)
-
def test_status_method_contains_volume_which_defaults_to_0(self):
result = dict(status.status(self.context))
self.assert_('volume' in result)
@@ -205,7 +164,14 @@ class StatusHandlerTest(unittest.TestCase):
self.backend.playback.play_time_accumulated = 59123
result = dict(status.status(self.context))
self.assert_('elapsed' in result)
- self.assertEqual(int(result['elapsed']), 59123)
+ self.assertEqual(result['elapsed'], '59.123')
+
+ def test_status_method_when_starting_playing_contains_elapsed_zero(self):
+ self.backend.playback.state = PAUSED
+ self.backend.playback.play_time_accumulated = 123 # Less than 1000ms
+ result = dict(status.status(self.context))
+ self.assert_('elapsed' in result)
+ self.assertEqual(result['elapsed'], '0.123')
def test_status_method_when_playing_contains_bitrate(self):
self.backend.current_playlist.append([Track(bitrate=320)])
diff --git a/tests/frontends/mpd/stickers_test.py b/tests/frontends/mpd/stickers_test.py
deleted file mode 100644
index 86ac8aec..00000000
--- a/tests/frontends/mpd/stickers_test.py
+++ /dev/null
@@ -1,45 +0,0 @@
-import unittest
-
-from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd.dispatcher import MpdDispatcher
-from mopidy.mixers.dummy import DummyMixer
-
-class StickersHandlerTest(unittest.TestCase):
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.dispatcher = MpdDispatcher()
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
-
- def test_sticker_get(self):
- result = self.dispatcher.handle_request(
- u'sticker get "song" "file:///dev/urandom" "a_name"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_sticker_set(self):
- result = self.dispatcher.handle_request(
- u'sticker set "song" "file:///dev/urandom" "a_name" "a_value"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_sticker_delete_with_name(self):
- result = self.dispatcher.handle_request(
- u'sticker delete "song" "file:///dev/urandom" "a_name"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_sticker_delete_without_name(self):
- result = self.dispatcher.handle_request(
- u'sticker delete "song" "file:///dev/urandom"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_sticker_list(self):
- result = self.dispatcher.handle_request(
- u'sticker list "song" "file:///dev/urandom"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_sticker_find(self):
- result = self.dispatcher.handle_request(
- u'sticker find "song" "file:///dev/urandom" "a_name"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
diff --git a/tests/frontends/mpd/stored_playlists_test.py b/tests/frontends/mpd/stored_playlists_test.py
deleted file mode 100644
index 04bab6f1..00000000
--- a/tests/frontends/mpd/stored_playlists_test.py
+++ /dev/null
@@ -1,102 +0,0 @@
-import datetime as dt
-import unittest
-
-from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd.dispatcher import MpdDispatcher
-from mopidy.mixers.dummy import DummyMixer
-from mopidy.models import Track, Playlist
-
-class StoredPlaylistsHandlerTest(unittest.TestCase):
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.dispatcher = MpdDispatcher()
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
-
- def test_listplaylist(self):
- self.backend.stored_playlists.playlists = [
- Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])]
- result = self.dispatcher.handle_request(u'listplaylist "name"')
- self.assert_(u'file: file:///dev/urandom' in result)
- self.assert_(u'OK' in result)
-
- def test_listplaylist_fails_if_no_playlist_is_found(self):
- result = self.dispatcher.handle_request(u'listplaylist "name"')
- self.assertEqual(result[0],
- u'ACK [50@0] {listplaylist} No such playlist')
-
- def test_listplaylistinfo(self):
- self.backend.stored_playlists.playlists = [
- Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])]
- result = self.dispatcher.handle_request(u'listplaylistinfo "name"')
- self.assert_(u'file: file:///dev/urandom' in result)
- self.assert_(u'Track: 0' in result)
- self.assert_(u'Pos: 0' not in result)
- self.assert_(u'OK' in result)
-
- def test_listplaylistinfo_fails_if_no_playlist_is_found(self):
- result = self.dispatcher.handle_request(u'listplaylistinfo "name"')
- self.assertEqual(result[0],
- u'ACK [50@0] {listplaylistinfo} No such playlist')
-
- def test_listplaylists(self):
- last_modified = dt.datetime(2001, 3, 17, 13, 41, 17, 12345)
- self.backend.stored_playlists.playlists = [Playlist(name='a',
- last_modified=last_modified)]
- result = self.dispatcher.handle_request(u'listplaylists')
- self.assert_(u'playlist: a' in result)
- # Date without microseconds and with time zone information
- self.assert_(u'Last-Modified: 2001-03-17T13:41:17Z' in result)
- self.assert_(u'OK' in result)
-
- def test_load_known_playlist_appends_to_current_playlist(self):
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
- self.backend.stored_playlists.playlists = [Playlist(name='A-list',
- tracks=[Track(uri='c'), Track(uri='d'), Track(uri='e')])]
- result = self.dispatcher.handle_request(u'load "A-list"')
- self.assert_(u'OK' in result)
- tracks = self.backend.current_playlist.tracks.get()
- self.assertEqual(len(tracks), 5)
- self.assertEqual(tracks[0].uri, 'a')
- self.assertEqual(tracks[1].uri, 'b')
- self.assertEqual(tracks[2].uri, 'c')
- self.assertEqual(tracks[3].uri, 'd')
- self.assertEqual(tracks[4].uri, 'e')
-
- def test_load_unknown_playlist_acks(self):
- result = self.dispatcher.handle_request(u'load "unknown playlist"')
- self.assert_(u'ACK [50@0] {load} No such playlist' in result)
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 0)
-
- def test_playlistadd(self):
- result = self.dispatcher.handle_request(
- u'playlistadd "name" "file:///dev/urandom"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_playlistclear(self):
- result = self.dispatcher.handle_request(u'playlistclear "name"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_playlistdelete(self):
- result = self.dispatcher.handle_request(u'playlistdelete "name" "5"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_playlistmove(self):
- result = self.dispatcher.handle_request(u'playlistmove "name" "5" "10"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_rename(self):
- result = self.dispatcher.handle_request(u'rename "old_name" "new_name"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_rm(self):
- result = self.dispatcher.handle_request(u'rm "name"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_save(self):
- result = self.dispatcher.handle_request(u'save "name"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
diff --git a/tests/frontends/mpris/__init__.py b/tests/frontends/mpris/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/frontends/mpris/events_test.py b/tests/frontends/mpris/events_test.py
new file mode 100644
index 00000000..49e56226
--- /dev/null
+++ b/tests/frontends/mpris/events_test.py
@@ -0,0 +1,78 @@
+import sys
+
+import mock
+
+from mopidy import OptionalDependencyError
+from mopidy.models import Track
+
+try:
+ from mopidy.frontends.mpris import MprisFrontend, objects
+except OptionalDependencyError:
+ pass
+
+from tests import unittest
+
+
+@unittest.skipUnless(sys.platform.startswith('linux'), 'requires Linux')
+class BackendEventsTest(unittest.TestCase):
+ def setUp(self):
+ self.mpris_frontend = MprisFrontend() # As a plain class, not an actor
+ 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(Track(), 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(Track(), 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_event_changes_playback_status_and_metadata(self):
+ self.mpris_object.Get.return_value = '...'
+ self.mpris_frontend.track_playback_started(Track())
+ 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_event_changes_playback_status_and_metadata(self):
+ self.mpris_object.Get.return_value = '...'
+ self.mpris_frontend.track_playback_ended(Track(), 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()
+ 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_object.Get.return_value = 31000000
+ self.mpris_frontend.seeked()
+ self.assertListEqual(self.mpris_object.Get.call_args_list, [
+ ((objects.PLAYER_IFACE, 'Position'), {}),
+ ])
+ self.mpris_object.Seeked.assert_called_with(31000000)
diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py
new file mode 100644
index 00000000..24c426fb
--- /dev/null
+++ b/tests/frontends/mpris/player_interface_test.py
@@ -0,0 +1,834 @@
+import sys
+
+import mock
+
+from mopidy import OptionalDependencyError
+from mopidy.backends.dummy import DummyBackend
+from mopidy.backends.base.playback import PlaybackController
+from mopidy.mixers.dummy import DummyMixer
+from mopidy.models import Album, Artist, Track
+
+try:
+ from mopidy.frontends.mpris import objects
+except OptionalDependencyError:
+ pass
+
+from tests import unittest
+
+PLAYING = PlaybackController.PLAYING
+PAUSED = PlaybackController.PAUSED
+STOPPED = PlaybackController.STOPPED
+
+
+@unittest.skipUnless(sys.platform.startswith('linux'), 'requires Linux')
+class PlayerInterfaceTest(unittest.TestCase):
+ def setUp(self):
+ objects.MprisObject._connect_to_dbus = mock.Mock()
+ self.mixer = DummyMixer.start().proxy()
+ self.backend = DummyBackend.start().proxy()
+ self.mpris = objects.MprisObject()
+ self.mpris._backend = self.backend
+
+ def tearDown(self):
+ self.backend.stop()
+ self.mixer.stop()
+
+ def test_get_playback_status_is_playing_when_playing(self):
+ self.backend.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.backend.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.backend.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.backend.playback.repeat = False
+ self.backend.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.backend.playback.repeat = True
+ self.backend.playback.single = True
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'LoopStatus')
+ self.assertEqual('Track', result)
+
+ def test_get_loop_status_is_playlist_when_looping_the_current_playlist(self):
+ self.backend.playback.repeat = True
+ self.backend.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.backend.playback.repeat = True
+ self.backend.playback.single = True
+ self.mpris.Set(objects.PLAYER_IFACE, 'LoopStatus', 'None')
+ self.assertEquals(self.backend.playback.repeat.get(), True)
+ self.assertEquals(self.backend.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.assertEquals(self.backend.playback.repeat.get(), False)
+ self.assertEquals(self.backend.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.assertEquals(self.backend.playback.repeat.get(), True)
+ self.assertEquals(self.backend.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.assertEquals(self.backend.playback.repeat.get(), True)
+ self.assertEquals(self.backend.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.assert_(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.assert_(rate >= maximum_rate)
+
+ def test_set_rate_is_ignored_if_can_control_is_false(self):
+ self.mpris.get_CanControl = lambda *_: False
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.mpris.Set(objects.PLAYER_IFACE, 'Rate', 0)
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+
+ def test_set_rate_to_zero_pauses_playback(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.mpris.Set(objects.PLAYER_IFACE, 'Rate', 0)
+ self.assertEquals(self.backend.playback.state.get(), PAUSED)
+
+ def test_get_shuffle_returns_true_if_random_is_active(self):
+ self.backend.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.backend.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.backend.playback.random = False
+ result = self.mpris.Set(objects.PLAYER_IFACE, 'Shuffle', True)
+ self.assertFalse(self.backend.playback.random.get())
+
+ def test_set_shuffle_to_true_activates_random_mode(self):
+ self.backend.playback.random = False
+ self.assertFalse(self.backend.playback.random.get())
+ result = self.mpris.Set(objects.PLAYER_IFACE, 'Shuffle', True)
+ self.assertTrue(self.backend.playback.random.get())
+
+ def test_set_shuffle_to_false_deactivates_random_mode(self):
+ self.backend.playback.random = True
+ self.assertTrue(self.backend.playback.random.get())
+ result = self.mpris.Set(objects.PLAYER_IFACE, 'Shuffle', False)
+ self.assertFalse(self.backend.playback.random.get())
+
+ def test_get_metadata_has_trackid_even_when_no_current_track(self):
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
+ self.assert_('mpris:trackid' in result.keys())
+ self.assertEquals(result['mpris:trackid'], '')
+
+ def test_get_metadata_has_trackid_based_on_cpid(self):
+ self.backend.current_playlist.append([Track(uri='a')])
+ self.backend.playback.play()
+ (cpid, track) = self.backend.playback.current_cp_track.get()
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
+ self.assertIn('mpris:trackid', result.keys())
+ self.assertEquals(result['mpris:trackid'],
+ '/com/mopidy/track/%d' % cpid)
+
+ def test_get_metadata_has_track_length(self):
+ self.backend.current_playlist.append([Track(uri='a', length=40000)])
+ self.backend.playback.play()
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
+ self.assertIn('mpris:length', result.keys())
+ self.assertEquals(result['mpris:length'], 40000000)
+
+ def test_get_metadata_has_track_uri(self):
+ self.backend.current_playlist.append([Track(uri='a')])
+ self.backend.playback.play()
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
+ self.assertIn('xesam:url', result.keys())
+ self.assertEquals(result['xesam:url'], 'a')
+
+ def test_get_metadata_has_track_title(self):
+ self.backend.current_playlist.append([Track(name='a')])
+ self.backend.playback.play()
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
+ self.assertIn('xesam:title', result.keys())
+ self.assertEquals(result['xesam:title'], 'a')
+
+ def test_get_metadata_has_track_artists(self):
+ self.backend.current_playlist.append([Track(artists=[
+ Artist(name='a'), Artist(name='b'), Artist(name=None)])])
+ self.backend.playback.play()
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
+ self.assertIn('xesam:artist', result.keys())
+ self.assertEquals(result['xesam:artist'], ['a', 'b'])
+
+ def test_get_metadata_has_track_album(self):
+ self.backend.current_playlist.append([Track(album=Album(name='a'))])
+ self.backend.playback.play()
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
+ self.assertIn('xesam:album', result.keys())
+ self.assertEquals(result['xesam:album'], 'a')
+
+ def test_get_metadata_has_track_album_artists(self):
+ self.backend.current_playlist.append([Track(album=Album(artists=[
+ Artist(name='a'), Artist(name='b'), Artist(name=None)]))])
+ self.backend.playback.play()
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
+ self.assertIn('xesam:albumArtist', result.keys())
+ self.assertEquals(result['xesam:albumArtist'], ['a', 'b'])
+
+ def test_get_metadata_has_track_number_in_album(self):
+ self.backend.current_playlist.append([Track(track_no=7)])
+ self.backend.playback.play()
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
+ self.assertIn('xesam:trackNumber', result.keys())
+ self.assertEquals(result['xesam:trackNumber'], 7)
+
+ def test_get_volume_should_return_volume_between_zero_and_one(self):
+ self.mixer.volume = 0
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume')
+ self.assertEquals(result, 0)
+
+ self.mixer.volume = 50
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume')
+ self.assertEquals(result, 0.5)
+
+ self.mixer.volume = 100
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume')
+ self.assertEquals(result, 1)
+
+ def test_set_volume_is_ignored_if_can_control_is_false(self):
+ self.mpris.get_CanControl = lambda *_: False
+ self.mixer.volume = 0
+ self.mpris.Set(objects.PLAYER_IFACE, 'Volume', 1.0)
+ self.assertEquals(self.mixer.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.assertEquals(self.mixer.volume.get(), 100)
+
+ def test_set_volume_to_anything_above_one_should_set_mixer_volume_to_100(self):
+ self.mpris.Set(objects.PLAYER_IFACE, 'Volume', 2.0)
+ self.assertEquals(self.mixer.volume.get(), 100)
+
+ def test_set_volume_to_anything_not_a_number_does_not_change_volume(self):
+ self.mixer.volume = 10
+ self.mpris.Set(objects.PLAYER_IFACE, 'Volume', None)
+ self.assertEquals(self.mixer.volume.get(), 10)
+
+ def test_get_position_returns_time_position_in_microseconds(self):
+ self.backend.current_playlist.append([Track(uri='a', length=40000)])
+ self.backend.playback.play()
+ self.backend.playback.seek(10000)
+ result_in_microseconds = self.mpris.Get(objects.PLAYER_IFACE, 'Position')
+ result_in_milliseconds = result_in_microseconds // 1000
+ self.assert_(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.assertEquals(result_in_milliseconds, 0)
+
+ def test_get_minimum_rate_is_one_or_less(self):
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'MinimumRate')
+ self.assert_(result <= 1.0)
+
+ def test_get_maximum_rate_is_one_or_more(self):
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'MaximumRate')
+ self.assert_(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.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.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.backend.current_playlist.append([Track(uri='a')])
+ self.backend.playback.repeat = True
+ self.backend.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.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoNext')
+ self.assertFalse(result)
+
+ def test_can_go_previous_is_true_if_can_control_and_other_previous_track(self):
+ self.mpris.get_CanControl = lambda *_: True
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.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.backend.current_playlist.append([Track(uri='a')])
+ self.backend.playback.repeat = True
+ self.backend.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.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.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.backend.current_playlist.append([Track(uri='a')])
+ self.backend.playback.play()
+ self.assertTrue(self.backend.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.backend.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.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+ self.mpris.Next()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+
+ def test_next_when_playing_should_skip_to_next_track_and_keep_playing(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.mpris.Next()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+
+ def test_next_when_at_end_of_list_should_stop_playback(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.playback.next()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.mpris.Next()
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+
+ def test_next_when_paused_should_skip_to_next_track_and_stay_paused(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.playback.pause()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+ self.assertEquals(self.backend.playback.state.get(), PAUSED)
+ self.mpris.Next()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
+ self.assertEquals(self.backend.playback.state.get(), PAUSED)
+
+ def test_next_when_stopped_should_skip_to_next_track_and_stay_stopped(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.playback.stop()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+ self.mpris.Next()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+
+ def test_previous_is_ignored_if_can_go_previous_is_false(self):
+ self.mpris.get_CanGoPrevious = lambda *_: False
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.playback.next()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
+ self.mpris.Previous()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
+
+ def test_previous_when_playing_should_skip_to_prev_track_and_keep_playing(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.playback.next()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.mpris.Previous()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+
+ def test_previous_when_at_start_of_list_should_stop_playback(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.mpris.Previous()
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+
+ def test_previous_when_paused_should_skip_to_previous_track_and_stay_paused(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.playback.next()
+ self.backend.playback.pause()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
+ self.assertEquals(self.backend.playback.state.get(), PAUSED)
+ self.mpris.Previous()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+ self.assertEquals(self.backend.playback.state.get(), PAUSED)
+
+ def test_previous_when_stopped_should_skip_to_previous_track_and_stay_stopped(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.playback.next()
+ self.backend.playback.stop()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+ self.mpris.Previous()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+
+ def test_pause_is_ignored_if_can_pause_is_false(self):
+ self.mpris.get_CanPause = lambda *_: False
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.mpris.Pause()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+
+ def test_pause_when_playing_should_pause_playback(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.mpris.Pause()
+ self.assertEquals(self.backend.playback.state.get(), PAUSED)
+
+ def test_pause_when_paused_has_no_effect(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.playback.pause()
+ self.assertEquals(self.backend.playback.state.get(), PAUSED)
+ self.mpris.Pause()
+ self.assertEquals(self.backend.playback.state.get(), PAUSED)
+
+ def test_playpause_is_ignored_if_can_pause_is_false(self):
+ self.mpris.get_CanPause = lambda *_: False
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.mpris.PlayPause()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+
+ def test_playpause_when_playing_should_pause_playback(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.mpris.PlayPause()
+ self.assertEquals(self.backend.playback.state.get(), PAUSED)
+
+ def test_playpause_when_paused_should_resume_playback(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.playback.pause()
+
+ self.assertEquals(self.backend.playback.state.get(), PAUSED)
+ at_pause = self.backend.playback.time_position.get()
+ self.assert_(at_pause >= 0)
+
+ self.mpris.PlayPause()
+
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ after_pause = self.backend.playback.time_position.get()
+ self.assert_(after_pause >= at_pause)
+
+ def test_playpause_when_stopped_should_start_playback(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+ self.mpris.PlayPause()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+
+ def test_stop_is_ignored_if_can_control_is_false(self):
+ self.mpris.get_CanControl = lambda *_: False
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.mpris.Stop()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+
+ def test_stop_when_playing_should_stop_playback(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.mpris.Stop()
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+
+ def test_stop_when_paused_should_stop_playback(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.playback.pause()
+ self.assertEquals(self.backend.playback.state.get(), PAUSED)
+ self.mpris.Stop()
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+
+ def test_play_is_ignored_if_can_play_is_false(self):
+ self.mpris.get_CanPlay = lambda *_: False
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+ self.mpris.Play()
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+
+ def test_play_when_stopped_starts_playback(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+ self.mpris.Play()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+
+ def test_play_after_pause_resumes_from_same_position(self):
+ self.backend.current_playlist.append([Track(uri='a', length=40000)])
+ self.backend.playback.play()
+
+ before_pause = self.backend.playback.time_position.get()
+ self.assert_(before_pause >= 0)
+
+ self.mpris.Pause()
+ self.assertEquals(self.backend.playback.state.get(), PAUSED)
+ at_pause = self.backend.playback.time_position.get()
+ self.assert_(at_pause >= before_pause)
+
+ self.mpris.Play()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ after_pause = self.backend.playback.time_position.get()
+ self.assert_(after_pause >= at_pause)
+
+ def test_play_when_there_is_no_track_has_no_effect(self):
+ self.backend.current_playlist.clear()
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+ self.mpris.Play()
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+
+ def test_seek_is_ignored_if_can_seek_is_false(self):
+ self.mpris.get_CanSeek = lambda *_: False
+ self.backend.current_playlist.append([Track(uri='a', length=40000)])
+ self.backend.playback.play()
+
+ before_seek = self.backend.playback.time_position.get()
+ self.assert_(before_seek >= 0)
+
+ milliseconds_to_seek = 10000
+ microseconds_to_seek = milliseconds_to_seek * 1000
+
+ self.mpris.Seek(microseconds_to_seek)
+
+ after_seek = self.backend.playback.time_position.get()
+ self.assert_(before_seek <= after_seek < (
+ before_seek + milliseconds_to_seek))
+
+ def test_seek_seeks_given_microseconds_forward_in_the_current_track(self):
+ self.backend.current_playlist.append([Track(uri='a', length=40000)])
+ self.backend.playback.play()
+
+ before_seek = self.backend.playback.time_position.get()
+ self.assert_(before_seek >= 0)
+
+ milliseconds_to_seek = 10000
+ microseconds_to_seek = milliseconds_to_seek * 1000
+
+ self.mpris.Seek(microseconds_to_seek)
+
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+
+ after_seek = self.backend.playback.time_position.get()
+ self.assert_(after_seek >= (before_seek + milliseconds_to_seek))
+
+ def test_seek_seeks_given_microseconds_backward_if_negative(self):
+ self.backend.current_playlist.append([Track(uri='a', length=40000)])
+ self.backend.playback.play()
+ self.backend.playback.seek(20000)
+
+ before_seek = self.backend.playback.time_position.get()
+ self.assert_(before_seek >= 20000)
+
+ milliseconds_to_seek = -10000
+ microseconds_to_seek = milliseconds_to_seek * 1000
+
+ self.mpris.Seek(microseconds_to_seek)
+
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+
+ after_seek = self.backend.playback.time_position.get()
+ self.assert_(after_seek >= (before_seek + milliseconds_to_seek))
+ self.assert_(after_seek < before_seek)
+
+ def test_seek_seeks_to_start_of_track_if_new_position_is_negative(self):
+ self.backend.current_playlist.append([Track(uri='a', length=40000)])
+ self.backend.playback.play()
+ self.backend.playback.seek(20000)
+
+ before_seek = self.backend.playback.time_position.get()
+ self.assert_(before_seek >= 20000)
+
+ milliseconds_to_seek = -30000
+ microseconds_to_seek = milliseconds_to_seek * 1000
+
+ self.mpris.Seek(microseconds_to_seek)
+
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+
+ after_seek = self.backend.playback.time_position.get()
+ self.assert_(after_seek >= (before_seek + milliseconds_to_seek))
+ self.assert_(after_seek < before_seek)
+ self.assert_(after_seek >= 0)
+
+ def test_seek_skips_to_next_track_if_new_position_larger_than_track_length(self):
+ self.backend.current_playlist.append([Track(uri='a', length=40000),
+ Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.playback.seek(20000)
+
+ before_seek = self.backend.playback.time_position.get()
+ self.assert_(before_seek >= 20000)
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+
+ milliseconds_to_seek = 50000
+ microseconds_to_seek = milliseconds_to_seek * 1000
+
+ self.mpris.Seek(microseconds_to_seek)
+
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
+
+ after_seek = self.backend.playback.time_position.get()
+ self.assert_(after_seek >= 0)
+ self.assert_(after_seek < before_seek)
+
+ def test_set_position_is_ignored_if_can_seek_is_false(self):
+ self.mpris.get_CanSeek = lambda *_: False
+ self.backend.current_playlist.append([Track(uri='a', length=40000)])
+ self.backend.playback.play()
+
+ before_set_position = self.backend.playback.time_position.get()
+ self.assert_(before_set_position <= 5000)
+
+ track_id = 'a'
+
+ position_to_set_in_milliseconds = 20000
+ position_to_set_in_microseconds = position_to_set_in_milliseconds * 1000
+
+ self.mpris.SetPosition(track_id, position_to_set_in_microseconds)
+
+ after_set_position = self.backend.playback.time_position.get()
+ self.assert_(before_set_position <= after_set_position <
+ position_to_set_in_milliseconds)
+
+ def test_set_position_sets_the_current_track_position_in_microsecs(self):
+ self.backend.current_playlist.append([Track(uri='a', length=40000)])
+ self.backend.playback.play()
+
+ before_set_position = self.backend.playback.time_position.get()
+ self.assert_(before_set_position <= 5000)
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+
+ track_id = '/com/mopidy/track/0'
+
+ position_to_set_in_milliseconds = 20000
+ position_to_set_in_microseconds = position_to_set_in_milliseconds * 1000
+
+ self.mpris.SetPosition(track_id, position_to_set_in_microseconds)
+
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+
+ after_set_position = self.backend.playback.time_position.get()
+ self.assert_(after_set_position >= position_to_set_in_milliseconds)
+
+ def test_set_position_does_nothing_if_the_position_is_negative(self):
+ self.backend.current_playlist.append([Track(uri='a', length=40000)])
+ self.backend.playback.play()
+ self.backend.playback.seek(20000)
+
+ before_set_position = self.backend.playback.time_position.get()
+ self.assert_(before_set_position >= 20000)
+ self.assert_(before_set_position <= 25000)
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+
+ track_id = '/com/mopidy/track/0'
+
+ position_to_set_in_milliseconds = -1000
+ position_to_set_in_microseconds = position_to_set_in_milliseconds * 1000
+
+ self.mpris.SetPosition(track_id, position_to_set_in_microseconds)
+
+ after_set_position = self.backend.playback.time_position.get()
+ self.assert_(after_set_position >= before_set_position)
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+
+ def test_set_position_does_nothing_if_position_is_larger_than_track_length(self):
+ self.backend.current_playlist.append([Track(uri='a', length=40000)])
+ self.backend.playback.play()
+ self.backend.playback.seek(20000)
+
+ before_set_position = self.backend.playback.time_position.get()
+ self.assert_(before_set_position >= 20000)
+ self.assert_(before_set_position <= 25000)
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+
+ track_id = 'a'
+
+ position_to_set_in_milliseconds = 50000
+ position_to_set_in_microseconds = position_to_set_in_milliseconds * 1000
+
+ self.mpris.SetPosition(track_id, position_to_set_in_microseconds)
+
+ after_set_position = self.backend.playback.time_position.get()
+ self.assert_(after_set_position >= before_set_position)
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+
+ def test_set_position_does_nothing_if_track_id_does_not_match_current_track(self):
+ self.backend.current_playlist.append([Track(uri='a', length=40000)])
+ self.backend.playback.play()
+ self.backend.playback.seek(20000)
+
+ before_set_position = self.backend.playback.time_position.get()
+ self.assert_(before_set_position >= 20000)
+ self.assert_(before_set_position <= 25000)
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+
+ track_id = 'b'
+
+ position_to_set_in_milliseconds = 0
+ position_to_set_in_microseconds = position_to_set_in_milliseconds * 1000
+
+ self.mpris.SetPosition(track_id, position_to_set_in_microseconds)
+
+ after_set_position = self.backend.playback.time_position.get()
+ self.assert_(after_set_position >= before_set_position)
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+
+ def test_open_uri_is_ignored_if_can_play_is_false(self):
+ self.mpris.get_CanPlay = lambda *_: False
+ self.backend.library.provider.dummy_library = [
+ Track(uri='dummy:/test/uri')]
+ self.mpris.OpenUri('dummy:/test/uri')
+ self.assertEquals(len(self.backend.current_playlist.tracks.get()), 0)
+
+ def test_open_uri_ignores_uris_with_unknown_uri_scheme(self):
+ self.assertListEqual(self.backend.uri_schemes.get(), ['dummy'])
+ self.mpris.get_CanPlay = lambda *_: True
+ self.backend.library.provider.dummy_library = [
+ Track(uri='notdummy:/test/uri')]
+ self.mpris.OpenUri('notdummy:/test/uri')
+ self.assertEquals(len(self.backend.current_playlist.tracks.get()), 0)
+
+ def test_open_uri_adds_uri_to_current_playlist(self):
+ self.mpris.get_CanPlay = lambda *_: True
+ self.backend.library.provider.dummy_library = [
+ Track(uri='dummy:/test/uri')]
+ self.mpris.OpenUri('dummy:/test/uri')
+ self.assertEquals(self.backend.current_playlist.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.provider.dummy_library = [
+ Track(uri='dummy:/test/uri')]
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+
+ self.mpris.OpenUri('dummy:/test/uri')
+
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEquals(self.backend.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.provider.dummy_library = [
+ Track(uri='dummy:/test/uri')]
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.playback.pause()
+ self.assertEquals(self.backend.playback.state.get(), PAUSED)
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+
+ self.mpris.OpenUri('dummy:/test/uri')
+
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEquals(self.backend.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.provider.dummy_library = [
+ Track(uri='dummy:/test/uri')]
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+
+ self.mpris.OpenUri('dummy:/test/uri')
+
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEquals(self.backend.playback.current_track.get().uri,
+ 'dummy:/test/uri')
diff --git a/tests/frontends/mpris/root_interface_test.py b/tests/frontends/mpris/root_interface_test.py
new file mode 100644
index 00000000..1e54fc15
--- /dev/null
+++ b/tests/frontends/mpris/root_interface_test.py
@@ -0,0 +1,70 @@
+import sys
+
+import mock
+
+from mopidy import OptionalDependencyError, settings
+from mopidy.backends.dummy import DummyBackend
+
+try:
+ from mopidy.frontends.mpris import objects
+except OptionalDependencyError:
+ pass
+
+from tests import unittest
+
+
+@unittest.skipUnless(sys.platform.startswith('linux'), 'requires Linux')
+class RootInterfaceTest(unittest.TestCase):
+ def setUp(self):
+ objects.exit_process = mock.Mock()
+ objects.MprisObject._connect_to_dbus = mock.Mock()
+ self.backend = DummyBackend.start().proxy()
+ self.mpris = objects.MprisObject()
+
+ def tearDown(self):
+ self.backend.stop()
+
+ def test_constructor_connects_to_dbus(self):
+ self.assert_(self.mpris._connect_to_dbus.called)
+
+ 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_mopidy(self):
+ result = self.mpris.Get(objects.ROOT_IFACE, 'DesktopEntry')
+ self.assertEquals(result, 'mopidy')
+
+ def test_desktop_entry_is_based_on_DESKTOP_FILE_setting(self):
+ settings.runtime['DESKTOP_FILE'] = '/tmp/foo.desktop'
+ result = self.mpris.Get(objects.ROOT_IFACE, 'DesktopEntry')
+ self.assertEquals(result, 'foo')
+ settings.runtime.clear()
+
+ def test_supported_uri_schemes_is_empty(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)
diff --git a/tests/gstreamer_test.py b/tests/gstreamer_test.py
index 0b9a559e..012c9002 100644
--- a/tests/gstreamer_test.py
+++ b/tests/gstreamer_test.py
@@ -1,21 +1,14 @@
-import multiprocessing
-import unittest
-
-from tests import SkipTest
-
-# FIXME Our Windows build server does not support GStreamer yet
import sys
-if sys.platform == 'win32':
- raise SkipTest
from mopidy import settings
from mopidy.gstreamer import GStreamer
from mopidy.utils.path import path_to_uri
-from tests import path_to_data_dir
+from tests import unittest, path_to_data_dir
-# TODO BaseOutputTest?
+@unittest.skipIf(sys.platform == 'win32',
+ 'Our Windows build server does not support GStreamer yet')
class GStreamerTest(unittest.TestCase):
def setUp(self):
settings.BACKENDS = ('mopidy.backends.local.LocalBackend',)
@@ -48,11 +41,11 @@ class GStreamerTest(unittest.TestCase):
self.gstreamer.start_playback()
self.assertTrue(self.gstreamer.stop_playback())
- @SkipTest
+ @unittest.SkipTest
def test_deliver_data(self):
pass # TODO
- @SkipTest
+ @unittest.SkipTest
def test_end_of_data_stream(self):
pass # TODO
@@ -71,10 +64,10 @@ class GStreamerTest(unittest.TestCase):
self.assertTrue(self.gstreamer.set_volume(100))
self.assertEqual(100, self.gstreamer.get_volume())
- @SkipTest
+ @unittest.SkipTest
def test_set_state_encapsulation(self):
pass # TODO
- @SkipTest
+ @unittest.SkipTest
def test_set_position(self):
pass # TODO
diff --git a/tests/help_test.py b/tests/help_test.py
index 25f534c2..1fa22c2f 100644
--- a/tests/help_test.py
+++ b/tests/help_test.py
@@ -1,10 +1,12 @@
import os
import subprocess
import sys
-import unittest
import mopidy
+from tests import unittest
+
+
class HelpTest(unittest.TestCase):
def test_help_has_mopidy_options(self):
mopidy_dir = os.path.dirname(mopidy.__file__)
diff --git a/tests/listeners_test.py b/tests/listeners_test.py
index 761aff4f..486dcf9c 100644
--- a/tests/listeners_test.py
+++ b/tests/listeners_test.py
@@ -1,14 +1,36 @@
-import unittest
-
from mopidy.listeners import BackendListener
from mopidy.models import Track
+from tests import unittest
+
+
class BackendListenerTest(unittest.TestCase):
def setUp(self):
self.listener = BackendListener()
- def test_listener_has_default_impl_for_the_started_playing_event(self):
- self.listener.started_playing(Track())
+ def test_listener_has_default_impl_for_track_playback_paused(self):
+ self.listener.track_playback_paused(Track(), 0)
- def test_listener_has_default_impl_for_the_stopped_playing_event(self):
- self.listener.stopped_playing(Track(), 0)
+ def test_listener_has_default_impl_for_track_playback_resumed(self):
+ self.listener.track_playback_resumed(Track(), 0)
+
+ def test_listener_has_default_impl_for_track_playback_started(self):
+ self.listener.track_playback_started(Track())
+
+ def test_listener_has_default_impl_for_track_playback_ended(self):
+ self.listener.track_playback_ended(Track(), 0)
+
+ def test_listener_has_default_impl_for_playback_state_changed(self):
+ self.listener.playback_state_changed()
+
+ def test_listener_has_default_impl_for_playlist_changed(self):
+ self.listener.playlist_changed()
+
+ def test_listener_has_default_impl_for_options_changed(self):
+ self.listener.options_changed()
+
+ def test_listener_has_default_impl_for_volume_changed(self):
+ self.listener.volume_changed()
+
+ def test_listener_has_default_impl_for_seeked(self):
+ self.listener.seeked()
diff --git a/tests/mixers/denon_test.py b/tests/mixers/denon_test.py
index 5370f155..cdfe0772 100644
--- a/tests/mixers/denon_test.py
+++ b/tests/mixers/denon_test.py
@@ -1,8 +1,9 @@
-import unittest
-
from mopidy.mixers.denon import DenonMixer
from tests.mixers.base_test import BaseMixerTest
+from tests import unittest
+
+
class DenonMixerDeviceMock(object):
def __init__(self):
self._open = True
@@ -24,6 +25,7 @@ class DenonMixerDeviceMock(object):
def open(self):
self._open = True
+
class DenonMixerTest(BaseMixerTest, unittest.TestCase):
ACTUAL_MAX = 99
INITIAL = 1
@@ -32,7 +34,7 @@ class DenonMixerTest(BaseMixerTest, unittest.TestCase):
def setUp(self):
self.device = DenonMixerDeviceMock()
- self.mixer = DenonMixer(None, device=self.device)
+ self.mixer = DenonMixer(device=self.device)
def test_reopen_device(self):
self.device._open = False
diff --git a/tests/mixers/dummy_test.py b/tests/mixers/dummy_test.py
index 334dc8a1..f9418d7a 100644
--- a/tests/mixers/dummy_test.py
+++ b/tests/mixers/dummy_test.py
@@ -1,9 +1,10 @@
-import unittest
-
from mopidy.mixers.dummy import DummyMixer
+
+from tests import unittest
from tests.mixers.base_test import BaseMixerTest
-class DenonMixerTest(BaseMixerTest, unittest.TestCase):
+
+class DummyMixerTest(BaseMixerTest, unittest.TestCase):
mixer_class = DummyMixer
def test_set_volume_is_capped(self):
@@ -15,3 +16,8 @@ class DenonMixerTest(BaseMixerTest, unittest.TestCase):
self.mixer.amplification_factor = 0.5
self.mixer._volume = 50
self.assertEquals(self.mixer.volume, 100)
+
+ def test_get_volume_get_the_same_number_as_was_set(self):
+ self.mixer.amplification_factor = 0.5
+ self.mixer.volume = 13
+ self.assertEquals(self.mixer.volume, 13)
diff --git a/tests/models_test.py b/tests/models_test.py
index 637a8209..978f35b6 100644
--- a/tests/models_test.py
+++ b/tests/models_test.py
@@ -1,9 +1,9 @@
-import datetime as dt
-import unittest
+import datetime
from mopidy.models import Artist, Album, CpTrack, Track, Playlist
-from tests import SkipTest
+from tests import unittest
+
class GenericCopyTets(unittest.TestCase):
def compare(self, orig, other):
@@ -49,6 +49,7 @@ class GenericCopyTets(unittest.TestCase):
test = lambda: Track().copy(invalid_key=True)
self.assertRaises(TypeError, test)
+
class ArtistTest(unittest.TestCase):
def test_uri(self):
uri = u'an_uri'
@@ -321,7 +322,7 @@ class TrackTest(unittest.TestCase):
self.assertRaises(AttributeError, setattr, track, 'track_no', None)
def test_date(self):
- date = dt.date(1977, 1, 1)
+ date = datetime.date(1977, 1, 1)
track = Track(date=date)
self.assertEqual(track.date, date)
self.assertRaises(AttributeError, setattr, track, 'date', None)
@@ -400,7 +401,7 @@ class TrackTest(unittest.TestCase):
self.assertEqual(hash(track1), hash(track2))
def test_eq_date(self):
- date = dt.date.today()
+ date = datetime.date.today()
track1 = Track(date=date)
track2 = Track(date=date)
self.assertEqual(track1, track2)
@@ -425,7 +426,7 @@ class TrackTest(unittest.TestCase):
self.assertEqual(hash(track1), hash(track2))
def test_eq(self):
- date = dt.date.today()
+ date = datetime.date.today()
artists = [Artist()]
album = Album()
track1 = Track(uri=u'uri', name=u'name', artists=artists, album=album,
@@ -474,8 +475,8 @@ class TrackTest(unittest.TestCase):
self.assertNotEqual(hash(track1), hash(track2))
def test_ne_date(self):
- track1 = Track(date=dt.date.today())
- track2 = Track(date=dt.date.today()-dt.timedelta(days=1))
+ track1 = Track(date=datetime.date.today())
+ track2 = Track(date=datetime.date.today()-datetime.timedelta(days=1))
self.assertNotEqual(track1, track2)
self.assertNotEqual(hash(track1), hash(track2))
@@ -500,11 +501,11 @@ class TrackTest(unittest.TestCase):
def test_ne(self):
track1 = Track(uri=u'uri1', name=u'name1',
artists=[Artist(name=u'name1')], album=Album(name=u'name1'),
- track_no=1, date=dt.date.today(), length=100, bitrate=100,
+ track_no=1, date=datetime.date.today(), length=100, bitrate=100,
musicbrainz_id='id1')
track2 = Track(uri=u'uri2', name=u'name2',
artists=[Artist(name=u'name2')], album=Album(name=u'name2'),
- track_no=2, date=dt.date.today()-dt.timedelta(days=1),
+ track_no=2, date=datetime.date.today()-datetime.timedelta(days=1),
length=200, bitrate=200, musicbrainz_id='id2')
self.assertNotEqual(track1, track2)
self.assertNotEqual(hash(track1), hash(track2))
@@ -535,7 +536,7 @@ class PlaylistTest(unittest.TestCase):
self.assertEqual(playlist.length, 3)
def test_last_modified(self):
- last_modified = dt.datetime.now()
+ last_modified = datetime.datetime.now()
playlist = Playlist(last_modified=last_modified)
self.assertEqual(playlist.last_modified, last_modified)
self.assertRaises(AttributeError, setattr, playlist, 'last_modified',
@@ -543,7 +544,7 @@ class PlaylistTest(unittest.TestCase):
def test_with_new_uri(self):
tracks = [Track()]
- last_modified = dt.datetime.now()
+ last_modified = datetime.datetime.now()
playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks,
last_modified=last_modified)
new_playlist = playlist.copy(uri=u'another uri')
@@ -554,7 +555,7 @@ class PlaylistTest(unittest.TestCase):
def test_with_new_name(self):
tracks = [Track()]
- last_modified = dt.datetime.now()
+ last_modified = datetime.datetime.now()
playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks,
last_modified=last_modified)
new_playlist = playlist.copy(name=u'another name')
@@ -565,7 +566,7 @@ class PlaylistTest(unittest.TestCase):
def test_with_new_tracks(self):
tracks = [Track()]
- last_modified = dt.datetime.now()
+ last_modified = datetime.datetime.now()
playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks,
last_modified=last_modified)
new_tracks = [Track(), Track()]
@@ -577,8 +578,8 @@ class PlaylistTest(unittest.TestCase):
def test_with_new_last_modified(self):
tracks = [Track()]
- last_modified = dt.datetime.now()
- new_last_modified = last_modified + dt.timedelta(1)
+ last_modified = datetime.datetime.now()
+ new_last_modified = last_modified + datetime.timedelta(1)
playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks,
last_modified=last_modified)
new_playlist = playlist.copy(last_modified=new_last_modified)
diff --git a/tests/scanner_test.py b/tests/scanner_test.py
index f403a221..91e67e11 100644
--- a/tests/scanner_test.py
+++ b/tests/scanner_test.py
@@ -1,10 +1,10 @@
-import unittest
from datetime import date
from mopidy.scanner import Scanner, translator
from mopidy.models import Track, Artist, Album
-from tests import path_to_data_dir, SkipTest
+from tests import unittest, path_to_data_dir
+
class FakeGstDate(object):
def __init__(self, year, month, day):
@@ -12,6 +12,7 @@ class FakeGstDate(object):
self.month = month
self.day = day
+
class TranslatorTest(unittest.TestCase):
def setUp(self):
self.data = {
@@ -126,6 +127,7 @@ class TranslatorTest(unittest.TestCase):
del self.track['date']
self.check()
+
class ScannerTest(unittest.TestCase):
def setUp(self):
self.errors = {}
@@ -185,6 +187,6 @@ class ScannerTest(unittest.TestCase):
self.scan('scanner/image')
self.assert_(self.errors)
- @SkipTest
+ @unittest.SkipTest
def test_song_without_time_is_handeled(self):
pass
diff --git a/tests/utils/decode_test.py b/tests/utils/decode_test.py
new file mode 100644
index 00000000..edbfe651
--- /dev/null
+++ b/tests/utils/decode_test.py
@@ -0,0 +1,38 @@
+import mock
+
+from mopidy.utils import locale_decode
+
+from tests import unittest
+
+
+@mock.patch('mopidy.utils.locale.getpreferredencoding')
+class LocaleDecodeTest(unittest.TestCase):
+ def test_can_decode_utf8_strings_with_french_content(self, mock):
+ mock.return_value = 'UTF-8'
+
+ result = locale_decode(
+ '[Errno 98] Adresse d\xc3\xa9j\xc3\xa0 utilis\xc3\xa9e')
+
+ self.assertEquals(u'[Errno 98] Adresse d\xe9j\xe0 utilis\xe9e', result)
+
+ def test_can_decode_an_ioerror_with_french_content(self, mock):
+ mock.return_value = 'UTF-8'
+
+ error = IOError(98, 'Adresse d\xc3\xa9j\xc3\xa0 utilis\xc3\xa9e')
+ result = locale_decode(error)
+
+ self.assertEquals(u'[Errno 98] Adresse d\xe9j\xe0 utilis\xe9e', result)
+
+ def test_does_not_use_locale_to_decode_unicode_strings(self, mock):
+ mock.return_value = 'UTF-8'
+
+ locale_decode(u'abc')
+
+ self.assertFalse(mock.called)
+
+ def test_does_not_use_locale_to_decode_ascii_bytestrings(self, mock):
+ mock.return_value = 'UTF-8'
+
+ locale_decode('abc')
+
+ self.assertFalse(mock.called)
diff --git a/tests/utils/init_test.py b/tests/utils/init_test.py
index 70dd7e36..2097e3e6 100644
--- a/tests/utils/init_test.py
+++ b/tests/utils/init_test.py
@@ -1,7 +1,8 @@
-import unittest
-
from mopidy.utils import get_class
+from tests import unittest
+
+
class GetClassTest(unittest.TestCase):
def test_loading_module_that_does_not_exist(self):
self.assertRaises(ImportError, get_class, 'foo.bar.Baz')
diff --git a/tests/utils/network/__init__.py b/tests/utils/network/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/utils/network/connection_test.py b/tests/utils/network/connection_test.py
new file mode 100644
index 00000000..aa1be2b6
--- /dev/null
+++ b/tests/utils/network/connection_test.py
@@ -0,0 +1,539 @@
+import errno
+import gobject
+import logging
+import pykka
+import socket
+from mock import patch, sentinel, Mock
+
+from mopidy.utils import network
+
+from tests import unittest, any_int, any_unicode
+
+
+class ConnectionTest(unittest.TestCase):
+ def setUp(self):
+ self.mock = Mock(spec=network.Connection)
+
+ def test_init_ensure_nonblocking_io(self):
+ sock = Mock(spec=socket.SocketType)
+
+ network.Connection.__init__(self.mock, Mock(), sock,
+ (sentinel.host, sentinel.port), sentinel.timeout)
+ sock.setblocking.assert_called_once_with(False)
+
+ def test_init_starts_actor(self):
+ protocol = Mock(spec=network.LineProtocol)
+
+ network.Connection.__init__(self.mock, protocol, Mock(),
+ (sentinel.host, sentinel.port), sentinel.timeout)
+ protocol.start.assert_called_once_with(self.mock)
+
+ def test_init_enables_recv_and_timeout(self):
+ network.Connection.__init__(self.mock, Mock(), Mock(),
+ (sentinel.host, sentinel.port), sentinel.timeout)
+ self.mock.enable_recv.assert_called_once_with()
+ self.mock.enable_timeout.assert_called_once_with()
+
+ def test_init_stores_values_in_attributes(self):
+ addr = (sentinel.host, sentinel.port)
+ protocol = Mock(spec=network.LineProtocol)
+ sock = Mock(spec=socket.SocketType)
+
+ network.Connection.__init__(
+ self.mock, protocol, sock, addr, sentinel.timeout)
+ self.assertEqual(sock, self.mock.sock)
+ self.assertEqual(protocol, self.mock.protocol)
+ self.assertEqual(sentinel.timeout, self.mock.timeout)
+ self.assertEqual(sentinel.host, self.mock.host)
+ self.assertEqual(sentinel.port, self.mock.port)
+
+ def test_init_handles_ipv6_addr(self):
+ addr = (sentinel.host, sentinel.port,
+ sentinel.flowinfo, sentinel.scopeid)
+ protocol = Mock(spec=network.LineProtocol)
+ sock = Mock(spec=socket.SocketType)
+
+ network.Connection.__init__(
+ self.mock, protocol, sock, addr, sentinel.timeout)
+ self.assertEqual(sentinel.host, self.mock.host)
+ self.assertEqual(sentinel.port, self.mock.port)
+
+ def test_stop_disables_recv_send_and_timeout(self):
+ self.mock.stopping = False
+ self.mock.actor_ref = Mock()
+ self.mock.sock = Mock(spec=socket.SocketType)
+
+ network.Connection.stop(self.mock, sentinel.reason)
+ self.mock.disable_timeout.assert_called_once_with()
+ self.mock.disable_recv.assert_called_once_with()
+ self.mock.disable_send.assert_called_once_with()
+
+ def test_stop_closes_socket(self):
+ self.mock.stopping = False
+ self.mock.actor_ref = Mock()
+ self.mock.sock = Mock(spec=socket.SocketType)
+
+ network.Connection.stop(self.mock, sentinel.reason)
+ self.mock.sock.close.assert_called_once_with()
+
+ def test_stop_closes_socket_error(self):
+ self.mock.stopping = False
+ self.mock.actor_ref = Mock()
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.close.side_effect = socket.error
+
+ network.Connection.stop(self.mock, sentinel.reason)
+ self.mock.sock.close.assert_called_once_with()
+
+ def test_stop_stops_actor(self):
+ self.mock.stopping = False
+ self.mock.actor_ref = Mock()
+ self.mock.sock = Mock(spec=socket.SocketType)
+
+ network.Connection.stop(self.mock, sentinel.reason)
+ self.mock.actor_ref.stop.assert_called_once_with()
+
+ def test_stop_handles_actor_already_being_stopped(self):
+ self.mock.stopping = False
+ self.mock.actor_ref = Mock()
+ self.mock.actor_ref.stop.side_effect = pykka.ActorDeadError()
+ self.mock.sock = Mock(spec=socket.SocketType)
+
+ network.Connection.stop(self.mock, sentinel.reason)
+ self.mock.actor_ref.stop.assert_called_once_with()
+
+ def test_stop_sets_stopping_to_true(self):
+ self.mock.stopping = False
+ self.mock.actor_ref = Mock()
+ self.mock.sock = Mock(spec=socket.SocketType)
+
+ network.Connection.stop(self.mock, sentinel.reason)
+ self.assertEqual(True, self.mock.stopping)
+
+ def test_stop_does_not_proceed_when_already_stopping(self):
+ self.mock.stopping = True
+ self.mock.actor_ref = Mock()
+ self.mock.sock = Mock(spec=socket.SocketType)
+
+ network.Connection.stop(self.mock, sentinel.reason)
+ self.assertEqual(0, self.mock.actor_ref.stop.call_count)
+ self.assertEqual(0, self.mock.sock.close.call_count)
+
+ @patch.object(network.logger, 'log', new=Mock())
+ def test_stop_logs_reason(self):
+ self.mock.stopping = False
+ self.mock.actor_ref = Mock()
+ self.mock.sock = Mock(spec=socket.SocketType)
+
+ network.Connection.stop(self.mock, sentinel.reason)
+ network.logger.log.assert_called_once_with(
+ logging.DEBUG, sentinel.reason)
+
+ @patch.object(network.logger, 'log', new=Mock())
+ def test_stop_logs_reason_with_level(self):
+ self.mock.stopping = False
+ self.mock.actor_ref = Mock()
+ self.mock.sock = Mock(spec=socket.SocketType)
+
+ network.Connection.stop(self.mock, sentinel.reason,
+ level=sentinel.level)
+ network.logger.log.assert_called_once_with(
+ sentinel.level, sentinel.reason)
+
+ @patch.object(network.logger, 'log', new=Mock())
+ def test_stop_logs_that_it_is_calling_itself(self):
+ self.mock.stopping = True
+ self.mock.actor_ref = Mock()
+ self.mock.sock = Mock(spec=socket.SocketType)
+
+ network.Connection.stop(self.mock, sentinel.reason)
+ network.logger.log(any_int, any_unicode)
+
+ @patch.object(gobject, 'io_add_watch', new=Mock())
+ def test_enable_recv_registers_with_gobject(self):
+ self.mock.recv_id = None
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.fileno.return_value = sentinel.fileno
+ gobject.io_add_watch.return_value = sentinel.tag
+
+ network.Connection.enable_recv(self.mock)
+ gobject.io_add_watch.assert_called_once_with(sentinel.fileno,
+ gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP,
+ self.mock.recv_callback)
+ self.assertEqual(sentinel.tag, self.mock.recv_id)
+
+ @patch.object(gobject, 'io_add_watch', new=Mock())
+ def test_enable_recv_already_registered(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.recv_id = sentinel.tag
+
+ network.Connection.enable_recv(self.mock)
+ self.assertEqual(0, gobject.io_add_watch.call_count)
+
+ def test_enable_recv_does_not_change_tag(self):
+ self.mock.recv_id = sentinel.tag
+ self.mock.sock = Mock(spec=socket.SocketType)
+
+ network.Connection.enable_recv(self.mock)
+ self.assertEqual(sentinel.tag, self.mock.recv_id)
+
+ @patch.object(gobject, 'source_remove', new=Mock())
+ def test_disable_recv_deregisters(self):
+ self.mock.recv_id = sentinel.tag
+
+ network.Connection.disable_recv(self.mock)
+ gobject.source_remove.assert_called_once_with(sentinel.tag)
+ self.assertEqual(None, self.mock.recv_id)
+
+ @patch.object(gobject, 'source_remove', new=Mock())
+ def test_disable_recv_already_deregistered(self):
+ self.mock.recv_id = None
+
+ network.Connection.disable_recv(self.mock)
+ self.assertEqual(0, gobject.source_remove.call_count)
+ self.assertEqual(None, self.mock.recv_id)
+
+ def test_enable_recv_on_closed_socket(self):
+ self.mock.recv_id = None
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.fileno.side_effect = socket.error(errno.EBADF, '')
+
+ network.Connection.enable_recv(self.mock)
+ self.mock.stop.assert_called_once_with(any_unicode)
+ self.assertEqual(None, self.mock.recv_id)
+
+ @patch.object(gobject, 'io_add_watch', new=Mock())
+ def test_enable_send_registers_with_gobject(self):
+ self.mock.send_id = None
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.fileno.return_value = sentinel.fileno
+ gobject.io_add_watch.return_value = sentinel.tag
+
+ network.Connection.enable_send(self.mock)
+ gobject.io_add_watch.assert_called_once_with(sentinel.fileno,
+ gobject.IO_OUT | gobject.IO_ERR | gobject.IO_HUP,
+ self.mock.send_callback)
+ self.assertEqual(sentinel.tag, self.mock.send_id)
+
+ @patch.object(gobject, 'io_add_watch', new=Mock())
+ def test_enable_send_already_registered(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.send_id = sentinel.tag
+
+ network.Connection.enable_send(self.mock)
+ self.assertEqual(0, gobject.io_add_watch.call_count)
+
+ def test_enable_send_does_not_change_tag(self):
+ self.mock.send_id = sentinel.tag
+ self.mock.sock = Mock(spec=socket.SocketType)
+
+ network.Connection.enable_send(self.mock)
+ self.assertEqual(sentinel.tag, self.mock.send_id)
+
+ @patch.object(gobject, 'source_remove', new=Mock())
+ def test_disable_send_deregisters(self):
+ self.mock.send_id = sentinel.tag
+
+ network.Connection.disable_send(self.mock)
+ gobject.source_remove.assert_called_once_with(sentinel.tag)
+ self.assertEqual(None, self.mock.send_id)
+
+ @patch.object(gobject, 'source_remove', new=Mock())
+ def test_disable_send_already_deregistered(self):
+ self.mock.send_id = None
+
+ network.Connection.disable_send(self.mock)
+ self.assertEqual(0, gobject.source_remove.call_count)
+ self.assertEqual(None, self.mock.send_id)
+
+ def test_enable_send_on_closed_socket(self):
+ self.mock.send_id = None
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.fileno.side_effect = socket.error(errno.EBADF, '')
+
+ network.Connection.enable_send(self.mock)
+ self.assertEqual(None, self.mock.send_id)
+
+ @patch.object(gobject, 'timeout_add_seconds', new=Mock())
+ def test_enable_timeout_clears_existing_timeouts(self):
+ self.mock.timeout = 10
+
+ network.Connection.enable_timeout(self.mock)
+ self.mock.disable_timeout.assert_called_once_with()
+
+ @patch.object(gobject, 'timeout_add_seconds', new=Mock())
+ def test_enable_timeout_add_gobject_timeout(self):
+ self.mock.timeout = 10
+ gobject.timeout_add_seconds.return_value = sentinel.tag
+
+ network.Connection.enable_timeout(self.mock)
+ gobject.timeout_add_seconds.assert_called_once_with(10,
+ self.mock.timeout_callback)
+ self.assertEqual(sentinel.tag, self.mock.timeout_id)
+
+ @patch.object(gobject, 'timeout_add_seconds', new=Mock())
+ def test_enable_timeout_does_not_add_timeout(self):
+ self.mock.timeout = 0
+ network.Connection.enable_timeout(self.mock)
+ self.assertEqual(0, gobject.timeout_add_seconds.call_count)
+
+ self.mock.timeout = -1
+ network.Connection.enable_timeout(self.mock)
+ self.assertEqual(0, gobject.timeout_add_seconds.call_count)
+
+ self.mock.timeout = None
+ network.Connection.enable_timeout(self.mock)
+ self.assertEqual(0, gobject.timeout_add_seconds.call_count)
+
+ def test_enable_timeout_does_not_call_disable_for_invalid_timeout(self):
+ self.mock.timeout = 0
+ network.Connection.enable_timeout(self.mock)
+ self.assertEqual(0, self.mock.disable_timeout.call_count)
+
+ self.mock.timeout = -1
+ network.Connection.enable_timeout(self.mock)
+ self.assertEqual(0, self.mock.disable_timeout.call_count)
+
+ self.mock.timeout = None
+ network.Connection.enable_timeout(self.mock)
+ self.assertEqual(0, self.mock.disable_timeout.call_count)
+
+ @patch.object(gobject, 'source_remove', new=Mock())
+ def test_disable_timeout_deregisters(self):
+ self.mock.timeout_id = sentinel.tag
+
+ network.Connection.disable_timeout(self.mock)
+ gobject.source_remove.assert_called_once_with(sentinel.tag)
+ self.assertEqual(None, self.mock.timeout_id)
+
+ @patch.object(gobject, 'source_remove', new=Mock())
+ def test_disable_timeout_already_deregistered(self):
+ self.mock.timeout_id = None
+
+ network.Connection.disable_timeout(self.mock)
+ self.assertEqual(0, gobject.source_remove.call_count)
+ self.assertEqual(None, self.mock.timeout_id)
+
+ def test_queue_send_acquires_and_releases_lock(self):
+ self.mock.send_lock = Mock()
+ self.mock.send_buffer = ''
+
+ network.Connection.queue_send(self.mock, 'data')
+ self.mock.send_lock.acquire.assert_called_once_with(True)
+ self.mock.send_lock.release.assert_called_once_with()
+
+ def test_queue_send_calls_send(self):
+ self.mock.send_buffer = ''
+ self.mock.send_lock = Mock()
+ self.mock.send.return_value = ''
+
+ network.Connection.queue_send(self.mock, 'data')
+ self.mock.send.assert_called_once_with('data')
+ self.assertEqual(0, self.mock.enable_send.call_count)
+ self.assertEqual('', self.mock.send_buffer)
+
+ def test_queue_send_calls_enable_send_for_partial_send(self):
+ self.mock.send_buffer = ''
+ self.mock.send_lock = Mock()
+ self.mock.send.return_value = 'ta'
+
+ network.Connection.queue_send(self.mock, 'data')
+ self.mock.send.assert_called_once_with('data')
+ self.mock.enable_send.assert_called_once_with()
+ self.assertEqual('ta', self.mock.send_buffer)
+
+ def test_queue_send_calls_send_with_existing_buffer(self):
+ self.mock.send_buffer = 'foo'
+ self.mock.send_lock = Mock()
+ self.mock.send.return_value = ''
+
+ network.Connection.queue_send(self.mock, 'bar')
+ self.mock.send.assert_called_once_with('foobar')
+ self.assertEqual(0, self.mock.enable_send.call_count)
+ self.assertEqual('', self.mock.send_buffer)
+
+ def test_recv_callback_respects_io_err(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.actor_ref = Mock()
+
+ self.assertTrue(network.Connection.recv_callback(self.mock,
+ sentinel.fd, gobject.IO_IN | gobject.IO_ERR))
+ self.mock.stop.assert_called_once_with(any_unicode)
+
+ def test_recv_callback_respects_io_hup(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.actor_ref = Mock()
+
+ self.assertTrue(network.Connection.recv_callback(self.mock,
+ sentinel.fd, gobject.IO_IN | gobject.IO_HUP))
+ self.mock.stop.assert_called_once_with(any_unicode)
+
+ def test_recv_callback_respects_io_hup_and_io_err(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.actor_ref = Mock()
+
+ self.assertTrue(network.Connection.recv_callback(self.mock,
+ sentinel.fd, gobject.IO_IN | gobject.IO_HUP | gobject.IO_ERR))
+ self.mock.stop.assert_called_once_with(any_unicode)
+
+ def test_recv_callback_sends_data_to_actor(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.recv.return_value = 'data'
+ self.mock.actor_ref = Mock()
+
+ self.assertTrue(network.Connection.recv_callback(
+ self.mock, sentinel.fd, gobject.IO_IN))
+ self.mock.actor_ref.send_one_way.assert_called_once_with(
+ {'received': 'data'})
+
+ def test_recv_callback_handles_dead_actors(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.recv.return_value = 'data'
+ self.mock.actor_ref = Mock()
+ self.mock.actor_ref.send_one_way.side_effect = pykka.ActorDeadError()
+
+ self.assertTrue(network.Connection.recv_callback(
+ self.mock, sentinel.fd, gobject.IO_IN))
+ self.mock.stop.assert_called_once_with(any_unicode)
+
+ def test_recv_callback_gets_no_data(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.recv.return_value = ''
+ self.mock.actor_ref = Mock()
+
+ self.assertTrue(network.Connection.recv_callback(
+ self.mock, sentinel.fd, gobject.IO_IN))
+ self.mock.stop.assert_called_once_with(any_unicode)
+
+ def test_recv_callback_recoverable_error(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+
+ for error in (errno.EWOULDBLOCK, errno.EINTR):
+ self.mock.sock.recv.side_effect = socket.error(error, '')
+ self.assertTrue(network.Connection.recv_callback(
+ self.mock, sentinel.fd, gobject.IO_IN))
+ self.assertEqual(0, self.mock.stop.call_count)
+
+ def test_recv_callback_unrecoverable_error(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.recv.side_effect = socket.error
+
+ self.assertTrue(network.Connection.recv_callback(
+ self.mock, sentinel.fd, gobject.IO_IN))
+ self.mock.stop.assert_called_once_with(any_unicode)
+
+ def test_send_callback_respects_io_err(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.send.return_value = 1
+ self.mock.send_lock = Mock()
+ self.mock.actor_ref = Mock()
+ self.mock.send_buffer = ''
+
+ self.assertTrue(network.Connection.send_callback(self.mock,
+ sentinel.fd, gobject.IO_IN | gobject.IO_ERR))
+ self.mock.stop.assert_called_once_with(any_unicode)
+
+ def test_send_callback_respects_io_hup(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.send.return_value = 1
+ self.mock.send_lock = Mock()
+ self.mock.actor_ref = Mock()
+ self.mock.send_buffer = ''
+
+ self.assertTrue(network.Connection.send_callback(self.mock,
+ sentinel.fd, gobject.IO_IN | gobject.IO_HUP))
+ self.mock.stop.assert_called_once_with(any_unicode)
+
+ def test_send_callback_respects_io_hup_and_io_err(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.send.return_value = 1
+ self.mock.send_lock = Mock()
+ self.mock.actor_ref = Mock()
+ self.mock.send_buffer = ''
+
+ self.assertTrue(network.Connection.send_callback(self.mock,
+ sentinel.fd, gobject.IO_IN | gobject.IO_HUP | gobject.IO_ERR))
+ self.mock.stop.assert_called_once_with(any_unicode)
+
+ def test_send_callback_acquires_and_releases_lock(self):
+ self.mock.send_lock = Mock()
+ self.mock.send_lock.acquire.return_value = True
+ self.mock.send_buffer = ''
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.send.return_value = 0
+
+ self.assertTrue(network.Connection.send_callback(
+ self.mock, sentinel.fd, gobject.IO_IN))
+ self.mock.send_lock.acquire.assert_called_once_with(False)
+ self.mock.send_lock.release.assert_called_once_with()
+
+ def test_send_callback_fails_to_acquire_lock(self):
+ self.mock.send_lock = Mock()
+ self.mock.send_lock.acquire.return_value = False
+ self.mock.send_buffer = ''
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.send.return_value = 0
+
+ self.assertTrue(network.Connection.send_callback(
+ self.mock, sentinel.fd, gobject.IO_IN))
+ self.mock.send_lock.acquire.assert_called_once_with(False)
+ self.assertEqual(0, self.mock.sock.send.call_count)
+
+ def test_send_callback_sends_all_data(self):
+ self.mock.send_lock = Mock()
+ self.mock.send_lock.acquire.return_value = True
+ self.mock.send_buffer = 'data'
+ self.mock.send.return_value = ''
+
+ self.assertTrue(network.Connection.send_callback(
+ self.mock, sentinel.fd, gobject.IO_IN))
+ self.mock.disable_send.assert_called_once_with()
+ self.mock.send.assert_called_once_with('data')
+ self.assertEqual('', self.mock.send_buffer)
+
+ def test_send_callback_sends_partial_data(self):
+ self.mock.send_lock = Mock()
+ self.mock.send_lock.acquire.return_value = True
+ self.mock.send_buffer = 'data'
+ self.mock.send.return_value = 'ta'
+
+ self.assertTrue(network.Connection.send_callback(
+ self.mock, sentinel.fd, gobject.IO_IN))
+ self.mock.send.assert_called_once_with('data')
+ self.assertEqual('ta', self.mock.send_buffer)
+
+ def test_send_recoverable_error(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+
+ for error in (errno.EWOULDBLOCK, errno.EINTR):
+ self.mock.sock.send.side_effect = socket.error(error, '')
+
+ network.Connection.send(self.mock, 'data')
+ self.assertEqual(0, self.mock.stop.call_count)
+
+ def test_send_calls_socket_send(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.send.return_value = 4
+
+ self.assertEqual('', network.Connection.send(self.mock, 'data'))
+ self.mock.sock.send.assert_called_once_with('data')
+
+ def test_send_calls_socket_send_partial_send(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.send.return_value = 2
+
+ self.assertEqual('ta', network.Connection.send(self.mock, 'data'))
+ self.mock.sock.send.assert_called_once_with('data')
+
+ def test_send_unrecoverable_error(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.send.side_effect = socket.error
+
+ self.assertEqual('', network.Connection.send(self.mock, 'data'))
+ self.mock.stop.assert_called_once_with(any_unicode)
+
+ def test_timeout_callback(self):
+ self.mock.timeout = 10
+
+ self.assertFalse(network.Connection.timeout_callback(self.mock))
+ self.mock.stop.assert_called_once_with(any_unicode)
diff --git a/tests/utils/network/lineprotocol_test.py b/tests/utils/network/lineprotocol_test.py
new file mode 100644
index 00000000..b323de09
--- /dev/null
+++ b/tests/utils/network/lineprotocol_test.py
@@ -0,0 +1,290 @@
+#encoding: utf-8
+
+import re
+from mock import sentinel, Mock
+
+from mopidy.utils import network
+
+from tests import unittest
+
+
+class LineProtocolTest(unittest.TestCase):
+ def setUp(self):
+ self.mock = Mock(spec=network.LineProtocol)
+
+ self.mock.terminator = network.LineProtocol.terminator
+ self.mock.encoding = network.LineProtocol.encoding
+ self.mock.delimeter = network.LineProtocol.delimeter
+ self.mock.prevent_timeout = False
+
+ def test_init_stores_values_in_attributes(self):
+ delimeter = re.compile(network.LineProtocol.terminator)
+ network.LineProtocol.__init__(self.mock, sentinel.connection)
+ self.assertEqual(sentinel.connection, self.mock.connection)
+ self.assertEqual('', self.mock.recv_buffer)
+ self.assertEqual(delimeter, self.mock.delimeter)
+ self.assertFalse(self.mock.prevent_timeout)
+
+ def test_init_compiles_delimeter(self):
+ self.mock.delimeter = '\r?\n'
+ delimeter = re.compile('\r?\n')
+
+ network.LineProtocol.__init__(self.mock, sentinel.connection)
+ self.assertEqual(delimeter, self.mock.delimeter)
+
+ def test_on_receive_no_new_lines_adds_to_recv_buffer(self):
+ self.mock.connection = Mock(spec=network.Connection)
+ self.mock.recv_buffer = ''
+ self.mock.parse_lines.return_value = []
+
+ network.LineProtocol.on_receive(self.mock, {'received': 'data'})
+ self.assertEqual('data', self.mock.recv_buffer)
+ self.mock.parse_lines.assert_called_once_with()
+ self.assertEqual(0, self.mock.on_line_received.call_count)
+
+ def test_on_receive_toggles_timeout(self):
+ self.mock.connection = Mock(spec=network.Connection)
+ self.mock.recv_buffer = ''
+ self.mock.parse_lines.return_value = []
+
+ network.LineProtocol.on_receive(self.mock, {'received': 'data'})
+ self.mock.connection.disable_timeout.assert_called_once_with()
+ self.mock.connection.enable_timeout.assert_called_once_with()
+
+ def test_on_receive_toggles_unless_prevent_timeout_is_set(self):
+ self.mock.connection = Mock(spec=network.Connection)
+ self.mock.recv_buffer = ''
+ self.mock.parse_lines.return_value = []
+ self.mock.prevent_timeout = True
+
+ network.LineProtocol.on_receive(self.mock, {'received': 'data'})
+ self.mock.connection.disable_timeout.assert_called_once_with()
+ self.assertEqual(0, self.mock.connection.enable_timeout.call_count)
+
+ def test_on_receive_no_new_lines_calls_parse_lines(self):
+ self.mock.connection = Mock(spec=network.Connection)
+ self.mock.recv_buffer = ''
+ self.mock.parse_lines.return_value = []
+
+ network.LineProtocol.on_receive(self.mock, {'received': 'data'})
+ self.mock.parse_lines.assert_called_once_with()
+ self.assertEqual(0, self.mock.on_line_received.call_count)
+
+ def test_on_receive_with_new_line_calls_decode(self):
+ self.mock.connection = Mock(spec=network.Connection)
+ self.mock.recv_buffer = ''
+ self.mock.parse_lines.return_value = [sentinel.line]
+
+ network.LineProtocol.on_receive(self.mock, {'received': 'data\n'})
+ self.mock.parse_lines.assert_called_once_with()
+ self.mock.decode.assert_called_once_with(sentinel.line)
+
+ def test_on_receive_with_new_line_calls_on_recieve(self):
+ self.mock.connection = Mock(spec=network.Connection)
+ self.mock.recv_buffer = ''
+ self.mock.parse_lines.return_value = [sentinel.line]
+ self.mock.decode.return_value = sentinel.decoded
+
+ network.LineProtocol.on_receive(self.mock, {'received': 'data\n'})
+ self.mock.on_line_received.assert_called_once_with(sentinel.decoded)
+
+ def test_on_receive_with_new_line_with_failed_decode(self):
+ self.mock.connection = Mock(spec=network.Connection)
+ self.mock.recv_buffer = ''
+ self.mock.parse_lines.return_value = [sentinel.line]
+ self.mock.decode.return_value = None
+
+ network.LineProtocol.on_receive(self.mock, {'received': 'data\n'})
+ self.assertEqual(0, self.mock.on_line_received.call_count)
+
+ def test_on_receive_with_new_lines_calls_on_recieve(self):
+ self.mock.connection = Mock(spec=network.Connection)
+ self.mock.recv_buffer = ''
+ self.mock.parse_lines.return_value = ['line1', 'line2']
+ self.mock.decode.return_value = sentinel.decoded
+
+ network.LineProtocol.on_receive(self.mock,
+ {'received': 'line1\nline2\n'})
+ self.assertEqual(2, self.mock.on_line_received.call_count)
+
+ def test_parse_lines_emtpy_buffer(self):
+ self.mock.delimeter = re.compile(r'\n')
+ self.mock.recv_buffer = ''
+
+ lines = network.LineProtocol.parse_lines(self.mock)
+ self.assertRaises(StopIteration, lines.next)
+
+ def test_parse_lines_no_terminator(self):
+ self.mock.delimeter = re.compile(r'\n')
+ self.mock.recv_buffer = 'data'
+
+ lines = network.LineProtocol.parse_lines(self.mock)
+ self.assertRaises(StopIteration, lines.next)
+
+ def test_parse_lines_termintor(self):
+ self.mock.delimeter = re.compile(r'\n')
+ self.mock.recv_buffer = 'data\n'
+
+ lines = network.LineProtocol.parse_lines(self.mock)
+ self.assertEqual('data', lines.next())
+ self.assertRaises(StopIteration, lines.next)
+ self.assertEqual('', self.mock.recv_buffer)
+
+ def test_parse_lines_termintor_with_carriage_return(self):
+ self.mock.delimeter = re.compile(r'\r?\n')
+ self.mock.recv_buffer = 'data\r\n'
+
+ lines = network.LineProtocol.parse_lines(self.mock)
+ self.assertEqual('data', lines.next())
+ self.assertRaises(StopIteration, lines.next)
+ self.assertEqual('', self.mock.recv_buffer)
+
+ def test_parse_lines_no_data_before_terminator(self):
+ self.mock.delimeter = re.compile(r'\n')
+ self.mock.recv_buffer = '\n'
+
+ lines = network.LineProtocol.parse_lines(self.mock)
+ self.assertEqual('', lines.next())
+ self.assertRaises(StopIteration, lines.next)
+ self.assertEqual('', self.mock.recv_buffer)
+
+ def test_parse_lines_extra_data_after_terminator(self):
+ self.mock.delimeter = re.compile(r'\n')
+ self.mock.recv_buffer = 'data1\ndata2'
+
+ lines = network.LineProtocol.parse_lines(self.mock)
+ self.assertEqual('data1', lines.next())
+ self.assertRaises(StopIteration, lines.next)
+ self.assertEqual('data2', self.mock.recv_buffer)
+
+ def test_parse_lines_unicode(self):
+ self.mock.delimeter = re.compile(r'\n')
+ self.mock.recv_buffer = u'æøå\n'.encode('utf-8')
+
+ lines = network.LineProtocol.parse_lines(self.mock)
+ self.assertEqual(u'æøå'.encode('utf-8'), lines.next())
+ self.assertRaises(StopIteration, lines.next)
+ self.assertEqual('', self.mock.recv_buffer)
+
+ def test_parse_lines_multiple_lines(self):
+ self.mock.delimeter = re.compile(r'\n')
+ self.mock.recv_buffer = 'abc\ndef\nghi\njkl'
+
+ lines = network.LineProtocol.parse_lines(self.mock)
+ self.assertEqual('abc', lines.next())
+ self.assertEqual('def', lines.next())
+ self.assertEqual('ghi', lines.next())
+ self.assertRaises(StopIteration, lines.next)
+ self.assertEqual('jkl', self.mock.recv_buffer)
+
+ def test_parse_lines_multiple_calls(self):
+ self.mock.delimeter = re.compile(r'\n')
+ self.mock.recv_buffer = 'data1'
+
+ lines = network.LineProtocol.parse_lines(self.mock)
+ self.assertRaises(StopIteration, lines.next)
+ self.assertEqual('data1', self.mock.recv_buffer)
+
+ self.mock.recv_buffer += '\ndata2'
+
+ lines = network.LineProtocol.parse_lines(self.mock)
+ self.assertEqual('data1', lines.next())
+ self.assertRaises(StopIteration, lines.next)
+ self.assertEqual('data2', self.mock.recv_buffer)
+
+ def test_send_lines_called_with_no_lines(self):
+ self.mock.connection = Mock(spec=network.Connection)
+
+ network.LineProtocol.send_lines(self.mock, [])
+ self.assertEqual(0, self.mock.encode.call_count)
+ self.assertEqual(0, self.mock.connection.queue_send.call_count)
+
+ def test_send_lines_calls_join_lines(self):
+ self.mock.connection = Mock(spec=network.Connection)
+ self.mock.join_lines.return_value = 'lines'
+
+ network.LineProtocol.send_lines(self.mock, sentinel.lines)
+ self.mock.join_lines.assert_called_once_with(sentinel.lines)
+
+ def test_send_line_encodes_joined_lines_with_final_terminator(self):
+ self.mock.connection = Mock(spec=network.Connection)
+ self.mock.join_lines.return_value = u'lines\n'
+
+ network.LineProtocol.send_lines(self.mock, sentinel.lines)
+ self.mock.encode.assert_called_once_with(u'lines\n')
+
+ def test_send_lines_sends_encoded_string(self):
+ self.mock.connection = Mock(spec=network.Connection)
+ self.mock.join_lines.return_value = 'lines'
+ self.mock.encode.return_value = sentinel.data
+
+ network.LineProtocol.send_lines(self.mock, sentinel.lines)
+ self.mock.connection.queue_send.assert_called_once_with(sentinel.data)
+
+ def test_join_lines_returns_empty_string_for_no_lines(self):
+ self.assertEqual(u'', network.LineProtocol.join_lines(self.mock, []))
+
+ def test_join_lines_returns_joined_lines(self):
+ self.assertEqual(u'1\n2\n', network.LineProtocol.join_lines(
+ self.mock, [u'1', u'2']))
+
+ def test_decode_calls_decode_on_string(self):
+ string = Mock()
+
+ network.LineProtocol.decode(self.mock, string)
+ string.decode.assert_called_once_with(self.mock.encoding)
+
+ def test_decode_plain_ascii(self):
+ result = network.LineProtocol.decode(self.mock, 'abc')
+ self.assertEqual(u'abc', result)
+ self.assertEqual(unicode, type(result))
+
+ def test_decode_utf8(self):
+ result = network.LineProtocol.decode(
+ self.mock, u'æøå'.encode('utf-8'))
+ self.assertEqual(u'æøå', result)
+ self.assertEqual(unicode, type(result))
+
+ def test_decode_invalid_data(self):
+ string = Mock()
+ string.decode.side_effect = UnicodeError
+
+ network.LineProtocol.decode(self.mock, string)
+ self.mock.stop.assert_called_once_with()
+
+ def test_encode_calls_encode_on_string(self):
+ string = Mock()
+
+ network.LineProtocol.encode(self.mock, string)
+ string.encode.assert_called_once_with(self.mock.encoding)
+
+ def test_encode_plain_ascii(self):
+ result = network.LineProtocol.encode(self.mock, u'abc')
+ self.assertEqual('abc', result)
+ self.assertEqual(str, type(result))
+
+ def test_encode_utf8(self):
+ result = network.LineProtocol.encode(self.mock, u'æøå')
+ self.assertEqual(u'æøå'.encode('utf-8'), result)
+ self.assertEqual(str, type(result))
+
+ def test_encode_invalid_data(self):
+ string = Mock()
+ string.encode.side_effect = UnicodeError
+
+ network.LineProtocol.encode(self.mock, string)
+ self.mock.stop.assert_called_once_with()
+
+ def test_host_property(self):
+ mock = Mock(spec=network.Connection)
+ mock.host = sentinel.host
+
+ lineprotocol = network.LineProtocol(mock)
+ self.assertEqual(sentinel.host, lineprotocol.host)
+
+ def test_port_property(self):
+ mock = Mock(spec=network.Connection)
+ mock.port = sentinel.port
+
+ lineprotocol = network.LineProtocol(mock)
+ self.assertEqual(sentinel.port, lineprotocol.port)
diff --git a/tests/utils/network/server_test.py b/tests/utils/network/server_test.py
new file mode 100644
index 00000000..e0399525
--- /dev/null
+++ b/tests/utils/network/server_test.py
@@ -0,0 +1,186 @@
+import errno
+import gobject
+import socket
+from mock import patch, sentinel, Mock
+
+from mopidy.utils import network
+
+from tests import unittest, any_int
+
+
+class ServerTest(unittest.TestCase):
+ def setUp(self):
+ self.mock = Mock(spec=network.Server)
+
+ def test_init_calls_create_server_socket(self):
+ network.Server.__init__(self.mock, sentinel.host,
+ sentinel.port, sentinel.protocol)
+ self.mock.create_server_socket.assert_called_once_with(
+ sentinel.host, sentinel.port)
+
+ def test_init_calls_register_server(self):
+ sock = Mock(spec=socket.SocketType)
+ sock.fileno.return_value = sentinel.fileno
+ self.mock.create_server_socket.return_value = sock
+
+ network.Server.__init__(self.mock, sentinel.host,
+ sentinel.port, sentinel.protocol)
+ self.mock.register_server_socket.assert_called_once_with(
+ sentinel.fileno)
+
+ def test_init_fails_on_fileno_call(self):
+ sock = Mock(spec=socket.SocketType)
+ sock.fileno.side_effect = socket.error
+ self.mock.create_server_socket.return_value = sock
+
+ self.assertRaises(socket.error, network.Server.__init__,
+ self.mock, sentinel.host, sentinel.port, sentinel.protocol)
+
+ def test_init_stores_values_in_attributes(self):
+ # This need to be a mock and no a sentinel as fileno() is called on it
+ sock = Mock(spec=socket.SocketType)
+ self.mock.create_server_socket.return_value = sock
+
+ network.Server.__init__(self.mock, sentinel.host, sentinel.port,
+ sentinel.protocol, max_connections=sentinel.max_connections,
+ timeout=sentinel.timeout)
+ self.assertEqual(sentinel.protocol, self.mock.protocol)
+ self.assertEqual(sentinel.max_connections, self.mock.max_connections)
+ self.assertEqual(sentinel.timeout, self.mock.timeout)
+ self.assertEqual(sock, self.mock.server_socket)
+
+ @patch.object(network, 'create_socket', spec=socket.SocketType)
+ def test_create_server_socket_sets_up_listener(self, create_socket):
+ sock = create_socket.return_value
+
+ network.Server.create_server_socket(self.mock,
+ sentinel.host, sentinel.port)
+ sock.setblocking.assert_called_once_with(False)
+ sock.bind.assert_called_once_with((sentinel.host, sentinel.port))
+ sock.listen.assert_called_once_with(any_int)
+
+ @patch.object(network, 'create_socket', new=Mock())
+ def test_create_server_socket_fails(self):
+ network.create_socket.side_effect = socket.error
+ self.assertRaises(socket.error, network.Server.create_server_socket,
+ self.mock, sentinel.host, sentinel.port)
+
+ @patch.object(network, 'create_socket', new=Mock())
+ def test_create_server_bind_fails(self):
+ sock = network.create_socket.return_value
+ sock.bind.side_effect = socket.error
+
+ self.assertRaises(socket.error, network.Server.create_server_socket,
+ self.mock, sentinel.host, sentinel.port)
+
+ @patch.object(network, 'create_socket', new=Mock())
+ def test_create_server_listen_fails(self):
+ sock = network.create_socket.return_value
+ sock.listen.side_effect = socket.error
+
+ self.assertRaises(socket.error, network.Server.create_server_socket,
+ self.mock, sentinel.host, sentinel.port)
+
+ @patch.object(gobject, 'io_add_watch', new=Mock())
+ def test_register_server_socket_sets_up_io_watch(self):
+ network.Server.register_server_socket(self.mock, sentinel.fileno)
+ gobject.io_add_watch.assert_called_once_with(sentinel.fileno,
+ gobject.IO_IN, self.mock.handle_connection)
+
+ def test_handle_connection(self):
+ self.mock.accept_connection.return_value = (
+ sentinel.sock, sentinel.addr)
+ self.mock.maximum_connections_exceeded.return_value = False
+
+ self.assertTrue(network.Server.handle_connection(
+ self.mock, sentinel.fileno, gobject.IO_IN))
+ self.mock.accept_connection.assert_called_once_with()
+ self.mock.maximum_connections_exceeded.assert_called_once_with()
+ self.mock.init_connection.assert_called_once_with(
+ sentinel.sock, sentinel.addr)
+ self.assertEqual(0, self.mock.reject_connection.call_count)
+
+ def test_handle_connection_exceeded_connections(self):
+ self.mock.accept_connection.return_value = (
+ sentinel.sock, sentinel.addr)
+ self.mock.maximum_connections_exceeded.return_value = True
+
+ self.assertTrue(network.Server.handle_connection(
+ self.mock, sentinel.fileno, gobject.IO_IN))
+ self.mock.accept_connection.assert_called_once_with()
+ self.mock.maximum_connections_exceeded.assert_called_once_with()
+ self.mock.reject_connection.assert_called_once_with(
+ sentinel.sock, sentinel.addr)
+ self.assertEqual(0, self.mock.init_connection.call_count)
+
+ def test_accept_connection(self):
+ sock = Mock(spec=socket.SocketType)
+ sock.accept.return_value = (sentinel.sock, sentinel.addr)
+ self.mock.server_socket = sock
+
+ sock, addr = network.Server.accept_connection(self.mock)
+ self.assertEqual(sentinel.sock, sock)
+ self.assertEqual(sentinel.addr, addr)
+
+ def test_accept_connection_recoverable_error(self):
+ sock = Mock(spec=socket.SocketType)
+ self.mock.server_socket = sock
+
+ for error in (errno.EAGAIN, errno.EINTR):
+ sock.accept.side_effect = socket.error(error, '')
+ self.assertRaises(network.ShouldRetrySocketCall,
+ network.Server.accept_connection, self.mock)
+
+ # FIXME decide if this should be allowed to propegate
+ def test_accept_connection_unrecoverable_error(self):
+ sock = Mock(spec=socket.SocketType)
+ self.mock.server_socket = sock
+ sock.accept.side_effect = socket.error
+ self.assertRaises(socket.error,
+ network.Server.accept_connection, self.mock)
+
+ def test_maximum_connections_exceeded(self):
+ self.mock.max_connections = 10
+
+ self.mock.number_of_connections.return_value = 11
+ self.assertTrue(network.Server.maximum_connections_exceeded(self.mock))
+
+ self.mock.number_of_connections.return_value = 10
+ self.assertTrue(network.Server.maximum_connections_exceeded(self.mock))
+
+ self.mock.number_of_connections.return_value = 9
+ self.assertFalse(network.Server.maximum_connections_exceeded(self.mock))
+
+ @patch('pykka.registry.ActorRegistry.get_by_class')
+ def test_number_of_connections(self, get_by_class):
+ self.mock.protocol = sentinel.protocol
+
+ get_by_class.return_value = [1, 2, 3]
+ self.assertEqual(3, network.Server.number_of_connections(self.mock))
+
+ get_by_class.return_value = []
+ self.assertEqual(0, network.Server.number_of_connections(self.mock))
+
+ @patch.object(network, 'Connection', new=Mock())
+ def test_init_connection(self):
+ self.mock.protocol = sentinel.protocol
+ self.mock.timeout = sentinel.timeout
+
+ network.Server.init_connection(self.mock, sentinel.sock, sentinel.addr)
+ network.Connection.assert_called_once_with(sentinel.protocol,
+ sentinel.sock, sentinel.addr, sentinel.timeout)
+
+ def test_reject_connection(self):
+ sock = Mock(spec=socket.SocketType)
+
+ network.Server.reject_connection(self.mock, sock,
+ (sentinel.host, sentinel.port))
+ sock.close.assert_called_once_with()
+
+ def test_reject_connection_error(self):
+ sock = Mock(spec=socket.SocketType)
+ sock.close.side_effect = socket.error
+
+ network.Server.reject_connection(self.mock, sock,
+ (sentinel.host, sentinel.port))
+ sock.close.assert_called_once_with()
diff --git a/tests/utils/network_test.py b/tests/utils/network/utils_test.py
similarity index 58%
rename from tests/utils/network_test.py
rename to tests/utils/network/utils_test.py
index 66229036..1e11673e 100644
--- a/tests/utils/network_test.py
+++ b/tests/utils/network/utils_test.py
@@ -1,57 +1,57 @@
-import mock
import socket
-import unittest
+from mock import patch, Mock
from mopidy.utils import network
-from tests import SkipTest
+from tests import unittest
+
class FormatHostnameTest(unittest.TestCase):
- @mock.patch('mopidy.utils.network.has_ipv6', True)
+ @patch('mopidy.utils.network.has_ipv6', True)
def test_format_hostname_prefixes_ipv4_addresses_when_ipv6_available(self):
network.has_ipv6 = True
self.assertEqual(network.format_hostname('0.0.0.0'), '::ffff:0.0.0.0')
self.assertEqual(network.format_hostname('1.0.0.1'), '::ffff:1.0.0.1')
- @mock.patch('mopidy.utils.network.has_ipv6', False)
+ @patch('mopidy.utils.network.has_ipv6', False)
def test_format_hostname_does_nothing_when_only_ipv4_available(self):
network.has_ipv6 = False
- self.assertEquals(network.format_hostname('0.0.0.0'), '0.0.0.0')
+ self.assertEqual(network.format_hostname('0.0.0.0'), '0.0.0.0')
class TryIPv6SocketTest(unittest.TestCase):
- @mock.patch('socket.has_ipv6', False)
+ @patch('socket.has_ipv6', False)
def test_system_that_claims_no_ipv6_support(self):
- self.assertFalse(network._try_ipv6_socket())
+ self.assertFalse(network.try_ipv6_socket())
- @mock.patch('socket.has_ipv6', True)
- @mock.patch('socket.socket')
+ @patch('socket.has_ipv6', True)
+ @patch('socket.socket')
def test_system_with_broken_ipv6(self, socket_mock):
socket_mock.side_effect = IOError()
- self.assertFalse(network._try_ipv6_socket())
+ self.assertFalse(network.try_ipv6_socket())
- @mock.patch('socket.has_ipv6', True)
- @mock.patch('socket.socket')
+ @patch('socket.has_ipv6', True)
+ @patch('socket.socket')
def test_with_working_ipv6(self, socket_mock):
- socket_mock.return_value = mock.Mock()
- self.assertTrue(network._try_ipv6_socket())
+ socket_mock.return_value = Mock()
+ self.assertTrue(network.try_ipv6_socket())
class CreateSocketTest(unittest.TestCase):
- @mock.patch('mopidy.utils.network.has_ipv6', False)
- @mock.patch('socket.socket')
+ @patch('mopidy.utils.network.has_ipv6', False)
+ @patch('socket.socket')
def test_ipv4_socket(self, socket_mock):
network.create_socket()
self.assertEqual(socket_mock.call_args[0],
(socket.AF_INET, socket.SOCK_STREAM))
- @mock.patch('mopidy.utils.network.has_ipv6', True)
- @mock.patch('socket.socket')
+ @patch('mopidy.utils.network.has_ipv6', True)
+ @patch('socket.socket')
def test_ipv6_socket(self, socket_mock):
network.create_socket()
self.assertEqual(socket_mock.call_args[0],
(socket.AF_INET6, socket.SOCK_STREAM))
- @SkipTest
+ @unittest.SkipTest
def test_ipv6_only_is_set(self):
pass
diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py
index 088a7049..19bae375 100644
--- a/tests/utils/path_test.py
+++ b/tests/utils/path_test.py
@@ -4,12 +4,12 @@ import os
import shutil
import sys
import tempfile
-import unittest
from mopidy.utils.path import (get_or_create_folder, mtime,
path_to_uri, uri_to_path, split_path, find_files)
-from tests import path_to_data_dir
+from tests import unittest, path_to_data_dir
+
class GetOrCreateFolderTest(unittest.TestCase):
def setUp(self):
@@ -28,12 +28,32 @@ class GetOrCreateFolderTest(unittest.TestCase):
self.assert_(os.path.isdir(folder))
self.assertEqual(created, folder)
+ def test_creating_nested_folders(self):
+ level2_folder = os.path.join(self.parent, 'test')
+ level3_folder = os.path.join(self.parent, 'test', 'test')
+ self.assert_(not os.path.exists(level2_folder))
+ self.assert_(not os.path.isdir(level2_folder))
+ self.assert_(not os.path.exists(level3_folder))
+ self.assert_(not os.path.isdir(level3_folder))
+ created = get_or_create_folder(level3_folder)
+ self.assert_(os.path.exists(level2_folder))
+ self.assert_(os.path.isdir(level2_folder))
+ self.assert_(os.path.exists(level3_folder))
+ self.assert_(os.path.isdir(level3_folder))
+ self.assertEqual(created, level3_folder)
+
def test_creating_existing_folder(self):
created = get_or_create_folder(self.parent)
self.assert_(os.path.exists(self.parent))
self.assert_(os.path.isdir(self.parent))
self.assertEqual(created, self.parent)
+ def test_create_folder_with_name_of_existing_file_throws_oserror(self):
+ conflicting_file = os.path.join(self.parent, 'test')
+ open(conflicting_file, 'w').close()
+ folder = os.path.join(self.parent, 'test')
+ self.assertRaises(OSError, get_or_create_folder, folder)
+
class PathToFileURITest(unittest.TestCase):
def test_simple_path(self):
diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py
index ec470ea9..55e1156b 100644
--- a/tests/utils/settings_test.py
+++ b/tests/utils/settings_test.py
@@ -1,10 +1,12 @@
import os
-import unittest
from mopidy import settings as default_settings_module, SettingsError
from mopidy.utils.settings import (format_settings_list, mask_value_if_secret,
SettingsProxy, validate_settings)
+from tests import unittest
+
+
class ValidateSettingsTest(unittest.TestCase):
def setUp(self):
self.defaults = {
diff --git a/tests/version_test.py b/tests/version_test.py
index 9b53c63f..26045ac1 100644
--- a/tests/version_test.py
+++ b/tests/version_test.py
@@ -1,12 +1,14 @@
from distutils.version import StrictVersion as SV
-import unittest
import platform
-from mopidy import get_version, get_plain_version, get_platform, get_python
+from mopidy import __version__, get_platform, get_python
+
+from tests import unittest
+
class VersionTest(unittest.TestCase):
def test_current_version_is_parsable_as_a_strict_version_number(self):
- SV(get_plain_version())
+ SV(__version__)
def test_versions_can_be_strictly_ordered(self):
self.assert_(SV('0.1.0a0') < SV('0.1.0a1'))
@@ -20,8 +22,13 @@ class VersionTest(unittest.TestCase):
self.assert_(SV('0.3.1') < SV('0.4.0'))
self.assert_(SV('0.4.0') < SV('0.4.1'))
self.assert_(SV('0.4.1') < SV('0.5.0'))
- self.assert_(SV('0.5.0') < SV(get_plain_version()))
- self.assert_(SV(get_plain_version()) < SV('0.6.1'))
+ self.assert_(SV('0.5.0') < SV('0.6.0'))
+ self.assert_(SV('0.6.0') < SV('0.6.1'))
+ self.assert_(SV('0.6.1') < SV('0.7.0'))
+ self.assert_(SV('0.7.0') < SV('0.7.1'))
+ self.assert_(SV('0.7.1') < SV('0.7.2'))
+ self.assert_(SV('0.7.2') < SV(__version__))
+ self.assert_(SV(__version__) < SV('0.8.0'))
def test_get_platform_contains_platform(self):
self.assert_(platform.platform() in get_platform())
diff --git a/tools/idle.py b/tools/idle.py
new file mode 100644
index 00000000..aa56dce2
--- /dev/null
+++ b/tools/idle.py
@@ -0,0 +1,201 @@
+#! /usr/bin/env python
+
+# This script is helper to systematicly test the behaviour of MPD's idle
+# command. It is simply provided as a quick hack, expect nothing more.
+
+import logging
+import pprint
+import socket
+
+host = ''
+port = 6601
+
+url = "13 - a-ha - White Canvas.mp3"
+artist = "a-ha"
+
+data = {'id': None, 'id2': None, 'url': url, 'artist': artist}
+
+# Commands to run before test requests to coerce MPD into right state
+setup_requests = [
+ 'clear',
+ 'add "%(url)s"',
+ 'add "%(url)s"',
+ 'add "%(url)s"',
+ 'play',
+# 'pause', # Uncomment to test paused idle behaviour
+# 'stop', # Uncomment to test stopped idle behaviour
+]
+
+# List of commands to test for idle behaviour. Ordering of list is important in
+# order to keep MPD state as intended. Commands that are obviously
+# informational only or "harmfull" have been excluded.
+test_requests = [
+ 'add "%(url)s"',
+ 'addid "%(url)s" "1"',
+ 'clear',
+# 'clearerror',
+# 'close',
+# 'commands',
+ 'consume "1"',
+ 'consume "0"',
+# 'count',
+ 'crossfade "1"',
+ 'crossfade "0"',
+# 'currentsong',
+# 'delete "1:2"',
+ 'delete "0"',
+ 'deleteid "%(id)s"',
+ 'disableoutput "0"',
+ 'enableoutput "0"',
+# 'find',
+# 'findadd "artist" "%(artist)s"',
+# 'idle',
+# 'kill',
+# 'list',
+# 'listall',
+# 'listallinfo',
+# 'listplaylist',
+# 'listplaylistinfo',
+# 'listplaylists',
+# 'lsinfo',
+ 'move "0:1" "2"',
+ 'move "0" "1"',
+ 'moveid "%(id)s" "1"',
+ 'next',
+# 'notcommands',
+# 'outputs',
+# 'password',
+ 'pause',
+# 'ping',
+ 'play',
+ 'playid "%(id)s"',
+# 'playlist',
+ 'playlistadd "foo" "%(url)s"',
+ 'playlistclear "foo"',
+ 'playlistadd "foo" "%(url)s"',
+ 'playlistdelete "foo" "0"',
+# 'playlistfind',
+# 'playlistid',
+# 'playlistinfo',
+ 'playlistadd "foo" "%(url)s"',
+ 'playlistadd "foo" "%(url)s"',
+ 'playlistmove "foo" "0" "1"',
+# 'playlistsearch',
+# 'plchanges',
+# 'plchangesposid',
+ 'previous',
+ 'random "1"',
+ 'random "0"',
+ 'rm "bar"',
+ 'rename "foo" "bar"',
+ 'repeat "0"',
+ 'rm "bar"',
+ 'save "bar"',
+ 'load "bar"',
+# 'search',
+ 'seek "1" "10"',
+ 'seekid "%(id)s" "10"',
+# 'setvol "10"',
+ 'shuffle',
+ 'shuffle "0:1"',
+ 'single "1"',
+ 'single "0"',
+# 'stats',
+# 'status',
+ 'stop',
+ 'swap "1" "2"',
+ 'swapid "%(id)s" "%(id2)s"',
+# 'tagtypes',
+# 'update',
+# 'urlhandlers',
+# 'volume',
+]
+
+
+def create_socketfile():
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.connect((host, port))
+ sock.settimeout(0.5)
+ fd = sock.makefile('rw', 1) # 1 = line buffered
+ fd.readline() # Read banner
+ return fd
+
+
+def wait(fd, prefix=None, collect=None):
+ while True:
+ line = fd.readline().rstrip()
+ if prefix:
+ logging.debug('%s: %s', prefix, repr(line))
+ if line.split()[0] in ('OK', 'ACK'):
+ break
+
+
+def collect_ids(fd):
+ fd.write('playlistinfo\n')
+
+ ids = []
+ while True:
+ line = fd.readline()
+ if line.split()[0] == 'OK':
+ break
+ if line.split()[0] == 'Id:':
+ ids.append(line.split()[1])
+ return ids
+
+
+def main():
+ subsystems = {}
+
+ command = create_socketfile()
+
+ for test in test_requests:
+ # Remove any old ids
+ del data['id']
+ del data['id2']
+
+ # Run setup code to force MPD into known state
+ for setup in setup_requests:
+ command.write(setup % data + '\n')
+ wait(command)
+
+ data['id'], data['id2'] = collect_ids(command)[:2]
+
+ # This connection needs to be make after setup commands are done or
+ # else they will cause idle events.
+ idle = create_socketfile()
+
+ # Wait for new idle events
+ idle.write('idle\n')
+
+ test = test % data
+
+ logging.debug('idle: %s', repr('idle'))
+ logging.debug('command: %s', repr(test))
+
+ command.write(test + '\n')
+ wait(command, prefix='command')
+
+ while True:
+ try:
+ line = idle.readline().rstrip()
+ except socket.timeout:
+ # Abort try if we time out.
+ idle.write('noidle\n')
+ break
+
+ logging.debug('idle: %s', repr(line))
+
+ if line == 'OK':
+ break
+
+ request_type = test.split()[0]
+ subsystem = line.split()[1]
+ subsystems.setdefault(request_type, set()).add(subsystem)
+
+ logging.debug('---')
+
+ pprint.pprint(subsystems)
+
+
+if __name__ == '__main__':
+ main()