Merge branch 'develop' into feature/simplify-outputs

Conflicts:
	docs/changes.rst
	mopidy/gstreamer.py
This commit is contained in:
Thomas Adamcik 2012-08-23 00:06:01 +02:00
commit 74a58be60c
137 changed files with 6141 additions and 2526 deletions

View File

@ -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 <http://www.mopidy.com/docs/master/installation/>`_.
`the installation docs <http://docs.mopidy.com/en/latest/installation/>`_.
- `Documentation for the latest release <http://www.mopidy.com/docs/master/>`_
- `Documentation for the development version
<http://www.mopidy.com/docs/develop/>`_
- `Documentation <http://docs.mopidy.com/>`_
- `Source code <http://github.com/mopidy/mopidy>`_
- `Issue tracker <http://github.com/mopidy/mopidy/issues>`_
- IRC: ``#mopidy`` at `irc.freenode.net <http://freenode.net/>`_

View File

@ -8,3 +8,4 @@ TryExec=mopidy
Exec=mopidy
Terminal=true
Categories=AudioVideo;Audio;Player;ConsoleOnly;
StartupNotify=true

View File

@ -1,15 +0,0 @@
{% extends "!layout.html" %}
{% block footer %}
{{ super() }}
<script type="text/javascript">
var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
</script>
<script type="text/javascript">
try {
var pageTracker = _gat._getTracker("UA-15510432-1");
pageTracker._trackPageview();
} catch(err) {}
</script>
{% endblock %}

View File

@ -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;
}

View File

@ -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 */

View File

@ -1,4 +0,0 @@
[theme]
inherit = basic
stylesheet = nature.css
pygments_style = tango

View File

@ -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
<http://jodal.github.com/pykka/>`_ actor, called the "main actor" from here
<http://pykka.readthedocs.org/>`_ 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`

View File

@ -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 <http://www.mpris.org/>`_ over D-Bus. In
practice, this makes it possible to control Mopidy through the `Ubuntu Sound
Menu <https://wiki.ubuntu.com/SoundMenu>`_.
**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 <http://jodal.github.com/pykka>`_ >=0.12. If you
- Mopidy now depends on `Pykka <http://pykka.readthedocs.org/>`_ >=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

View File

@ -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 <https://launchpad.net/ubuntu/+source/ncmpcpp>`_.
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

View File

@ -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-')}

View File

@ -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

View File

@ -19,9 +19,7 @@ please create an issue in the `issue tracker
Project resources
=================
- `Documentation for the latest release <http://www.mopidy.com/docs/master/>`_
- `Documentation for the development version
<http://www.mopidy.com/docs/develop/>`_
- `Documentation <http://docs.mopidy.com/>`_
- `Source code <http://github.com/mopidy/mopidy>`_
- `Issue tracker <http://github.com/mopidy/mopidy/issues>`_
- IRC: ``#mopidy`` at `irc.freenode.net <http://freenode.net/>`_

View File

@ -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
<http://github.com/mxcl/homebrew/issues/issue/1612>`_ yourself, or fall
back to using MacPorts.
We have been working with `Homebrew <https://github.com/mxcl/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
<https://github.com/mxcl/homebrew/issues/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 <https://github.com/mxcl/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

View File

@ -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 <http://jodal.github.com/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 </settings>`, and then
you're ready to :doc:`run Mopidy </running>`.
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::

View File

@ -12,12 +12,6 @@ install libspotify and `pyspotify <http://pyspotify.mopidy.com/>`_.
This backend requires a paid `Spotify premium account
<http://www.spotify.com/no/get-spotify/premium/>`_.
.. 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

View File

@ -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

View File

@ -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:

View File

@ -0,0 +1,7 @@
***********************************************
:mod:`mopidy.frontends.mpris` -- MPRIS frontend
***********************************************
.. automodule:: mopidy.frontends.mpris
:synopsis: MPRIS frontend
:members:

View File

@ -2,8 +2,6 @@
:mod:`mopidy.gstreamer` -- GStreamer adapter
********************************************
.. inheritance-diagram:: mopidy.gstreamer
.. automodule:: mopidy.gstreamer
:synopsis: GStreamer adapter
:members:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -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.

View File

@ -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
<https://wiki.ubuntu.com/SoundMenu>`_ 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 <http://www.mpris.org/>`_. The
``TrackList`` and the ``Playlists`` interfaces of the spec are not supported.
Streaming audio through a SHOUTcast/Icecast server
==================================================

View File

@ -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()

View File

@ -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')

View File

@ -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`.

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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 = {}

View File

@ -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

View File

@ -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"""

View File

@ -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:

View File

@ -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):

View File

@ -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()

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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<uri>[^"]*)"$')
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<songpos>-?\d+)"$')
@handle_request(r'^playlistinfo "(?P<start>\d+):(?P<end>\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<tag>[^"]+)" "(?P<needle>[^"]+)"$')
@handle_request(r'^playlistsearch (?P<tag>\S+) "(?P<needle>[^"]+)"$')
@ -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<version>\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)

View File

@ -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

View File

@ -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<query>("?([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<uri>[^"]+)")*$')
def update(context, uri=None, rescan_unmodified_files=False):

View File

@ -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

View File

@ -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)]

View File

@ -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<subsystems>.+)$')
@ -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()

View File

@ -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<name>[^"]+)"$')
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')

View File

@ -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)

View File

@ -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)

View File

@ -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):

View File

@ -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 <http://www.mpris.org/>`_) D-Bus
interface.
An example of an MPRIS client is the `Ubuntu Sound Menu
<https://wiki.ubuntu.com/SoundMenu>`_.
**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 <https://wiki.ubuntu.com/SoundMenu>`_.
A reference to the libindicate server is kept for as long as Mopidy is
running. When Mopidy exits, the server will be unreferenced and Mopidy
will automatically be unregistered from e.g. the sound menu.
"""
if not indicate:
return
logger.debug(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'))

View File

@ -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

View File

@ -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?

View File

@ -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

View File

@ -23,6 +23,7 @@ class AlsaMixer(ThreadingActor, BaseMixer):
"""
def __init__(self):
super(AlsaMixer, self).__init__()
self._mixer = None
def on_start(self):

View File

@ -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')

View File

@ -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

View File

@ -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):

View File

@ -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):

View File

@ -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):

View File

@ -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)

View File

@ -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 <http://www.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.
#:

View File

@ -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())

View File

@ -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(

View File

@ -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))

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -1,4 +1,6 @@
coverage
mock
mock >= 0.7
nose
tox
unittest2
yappi

View File

@ -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

View File

@ -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)

View File

@ -1,4 +1,8 @@
import nose
import yappi
if __name__ == '__main__':
try:
yappi.start()
nose.main()
finally:
yappi.print_stats()

View File

@ -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([])

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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')

View File

@ -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):

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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'),

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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:

View File

@ -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')

View File

@ -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])

View File

@ -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')

View File

@ -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')

View File

@ -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.

View File

@ -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')

View File

@ -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')

Some files were not shown because too many files have changed in this diff Show More