Release v0.7.0

This commit is contained in:
Stein Magnus Jodal 2012-02-25 01:03:29 +01:00
commit 2bf6a0cd4e
69 changed files with 507 additions and 586 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

@ -1,15 +0,0 @@
{% extends "!layout.html" %}
{% block extrahead %}
{{ super() }}
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-15510432-1']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</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.

View File

@ -4,6 +4,41 @@ Changes
This change log is used to track all major changes to Mopidy.
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)
===================
@ -23,6 +58,7 @@ make the Debian packages work out of the box again.
about bad playlists.
v0.6.0 (2011-10-09)
===================
@ -197,7 +233,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.
@ -242,7 +278,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::
@ -324,7 +360,7 @@ 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.
@ -493,7 +529,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
@ -823,7 +859,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

@ -13,20 +13,57 @@
import sys, os
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()
# 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
# documentation root, use os.path.abspath to make it absolute, like shown here.
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,13 +80,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.
import mopidy
release = mopidy.get_version()
# The short X.Y version.
version = '.'.join(release.split('.')[:2])
@ -97,7 +135,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 +154,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 +169,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 +241,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,36 +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 use the MPRIS frontend, e.g. using the Ubuntu Sound Menu, see
:mod:`mopidy.frontends.mpris` for additional requirements.
- 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
@ -100,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::
@ -112,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
===========================
@ -134,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::
@ -157,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,8 +2,6 @@
:mod:`mopidy.frontends.mpd` -- MPD server
*****************************************
.. inheritance-diagram:: mopidy.frontends.mpd
.. automodule:: mopidy.frontends.mpd
:synopsis: MPD server frontend
:members:
@ -12,8 +10,6 @@
MPD dispatcher
==============
.. inheritance-diagram:: mopidy.frontends.mpd.dispatcher
.. automodule:: mopidy.frontends.mpd.dispatcher
:synopsis: MPD request dispatcher
: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

@ -8,11 +8,11 @@ import os
from subprocess import PIPE, Popen
VERSION = (0, 6, 1)
VERSION = (0, 7, 0)
DATA_PATH = os.path.join(glib.get_user_data_dir(), 'mopidy')
CACHE_PATH = os.path.join(glib.get_user_cache_dir(), 'mopidy')
SETTINGS_PATH = os.path.join(glib.get_user_config_dir(), 'mopidy')
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():

View File

@ -28,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):
@ -37,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):
@ -116,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]
@ -129,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``.
@ -168,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)
@ -204,6 +223,19 @@ class CurrentPlaylistController(object):
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

@ -2,8 +2,6 @@ import logging
import random
import time
from pykka.registry import ActorRegistry
from mopidy.listeners import BackendListener
logger = logging.getLogger('mopidy.backends.base')

View File

@ -21,7 +21,7 @@ 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 = glib.get_user_special_dir(glib.USER_DIRECTORY_MUSIC)
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')

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

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

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

@ -1,4 +1,3 @@
import glib
import logging
import os
import threading

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(

View File

@ -12,14 +12,12 @@ gobject.threads_init()
# 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, DATA_PATH, SETTINGS_PATH, SETTINGS_FILE)
from mopidy.gstreamer import GStreamer

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

View File

@ -25,6 +25,7 @@ class MpdFrontend(actor.ThreadingActor, listeners.BackendListener):
"""
def __init__(self):
super(MpdFrontend, self).__init__()
hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME)
port = settings.MPD_SERVER_PORT

View File

@ -244,7 +244,8 @@ class MpdContext(object):
"""
if self._backend is None:
backend_refs = ActorRegistry.get_by_class(Backend)
assert len(backend_refs) == 1, 'Expected exactly one running backend.'
assert len(backend_refs) == 1, \
'Expected exactly one running backend.'
self._backend = backend_refs[0].proxy()
return self._backend

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

@ -1,8 +1,9 @@
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',
@ -32,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>.+)$')
@ -166,7 +166,7 @@ def status(context):
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,
@ -213,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()

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

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

@ -57,6 +57,7 @@ class MprisFrontend(ThreadingActor, BackendListener):
"""
def __init__(self):
super(MprisFrontend, self).__init__()
self.indicate_server = None
self.mpris_object = None

View File

@ -23,7 +23,6 @@ from mopidy.utils.process import exit_process
# Must be done before dbus.SessionBus() is called
gobject.threads_init()
dbus.mainloop.glib.threads_init()
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
BUS_NAME = 'org.mpris.MediaPlayer2.mopidy'
OBJECT_PATH = '/org/mpris/MediaPlayer2'
@ -81,7 +80,9 @@ class MprisObject(dbus.service.Object):
def _connect_to_dbus(self):
logger.debug(u'Connecting to D-Bus...')
bus_name = dbus.service.BusName(BUS_NAME, dbus.SessionBus())
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

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._tee = None
@ -77,12 +77,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):
@ -333,7 +335,8 @@ class GStreamer(ThreadingActor):
self._tee.send_event(event)
def _handle_event_probe(self, teesrc, event):
if event.type == gst.EVENT_CUSTOM_DOWNSTREAM and event.has_name('mopidy-unlink-tee'):
if (event.type == gst.EVENT_CUSTOM_DOWNSTREAM
and event.has_name('mopidy-unlink-tee')):
data = self._get_structure_data(event.get_structure())
output = teesrc.get_peer().get_parent()

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

@ -2,7 +2,7 @@ import logging
from mopidy import listeners, settings
logger = logging.getLogger('mopdy.mixers')
logger = logging.getLogger('mopidy.mixers')
class BaseMixer(object):
"""
@ -21,19 +21,30 @@ 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):

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

@ -297,6 +297,7 @@ class LineProtocol(ThreadingActor):
encoding = 'utf-8'
def __init__(self, connection):
super(LineProtocol, self).__init__()
self.connection = connection
self.prevent_timeout = False
self.recv_buffer = ''

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

View File

@ -2,7 +2,6 @@
from __future__ import absolute_import
from copy import copy
import getpass
import glib
import logging
import os
from pprint import pformat

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

@ -2,4 +2,5 @@ coverage
mock >= 0.7
nose
tox
unittest2
yappi

View File

@ -1,7 +1,7 @@
import mock
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
@ -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

@ -271,11 +271,17 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase):
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):
@ -286,11 +292,17 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase):
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):
@ -306,11 +318,17 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase):
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):

View File

@ -146,3 +146,19 @@ class IssueGH113RegressionTest(protocol.BaseTestCase):
self.sendRequest(
r'listplaylistinfo "all lart spotify:track:\\w\\{22\\} pastes"')
self.assertInResponse('OK')
class IssueGH137RegressionTest(protocol.BaseTestCase):
"""
The issue: https://github.com/mopidy/mopidy/issues/137
How to reproduce:
- Send "list" query with mismatching quotes
"""
def test(self):
self.sendRequest(u'list Date Artist "Anita Ward" '
u'Album "This Is Remixed Hits - Mashups & Rare 12" Mixes"')
self.assertInResponse('ACK [2@0] {list} Invalid unquoted character')

View File

@ -4,7 +4,7 @@ import os
from mopidy import settings
from mopidy.utils.path import mtime, uri_to_path
from mopidy.frontends.mpd import translator, protocol
from mopidy.models import Album, Artist, Playlist, Track
from mopidy.models import Album, Artist, CpTrack, Playlist, Track
from tests import unittest
@ -45,17 +45,17 @@ class TrackMpdFormatTest(unittest.TestCase):
self.assert_(('Pos', 1) not in result)
def test_track_to_mpd_format_with_cpid(self):
result = translator.track_to_mpd_format(Track(), cpid=1)
result = translator.track_to_mpd_format(CpTrack(1, Track()))
self.assert_(('Id', 1) not in result)
def test_track_to_mpd_format_with_position_and_cpid(self):
result = translator.track_to_mpd_format(Track(), position=1, cpid=2)
result = translator.track_to_mpd_format(CpTrack(2, Track()), position=1)
self.assert_(('Pos', 1) in result)
self.assert_(('Id', 2) in result)
def test_track_to_mpd_format_for_nonempty_track(self):
result = translator.track_to_mpd_format(
self.track, position=9, cpid=122)
CpTrack(122, self.track), position=9)
self.assert_(('file', 'a uri') in result)
self.assert_(('Time', 137) in result)
self.assert_(('Artist', 'an artist') in result)

View File

@ -1,11 +1,19 @@
import sys
import mock
from mopidy.frontends.mpris import MprisFrontend, objects
from mopidy import OptionalDependencyError
from mopidy.models import Track
try:
from mopidy.frontends.mpris import MprisFrontend, objects
except OptionalDependencyError:
pass
from tests import unittest
@unittest.skipUnless(sys.platform.startswith('linux'), 'requires Linux')
class BackendEventsTest(unittest.TestCase):
def setUp(self):
self.mpris_frontend = MprisFrontend() # As a plain class, not an actor

View File

@ -1,11 +1,18 @@
import sys
import mock
from mopidy import OptionalDependencyError
from mopidy.backends.dummy import DummyBackend
from mopidy.backends.base.playback import PlaybackController
from mopidy.frontends.mpris import objects
from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Album, Artist, Track
try:
from mopidy.frontends.mpris import objects
except OptionalDependencyError:
pass
from tests import unittest
PLAYING = PlaybackController.PLAYING
@ -13,6 +20,7 @@ PAUSED = PlaybackController.PAUSED
STOPPED = PlaybackController.STOPPED
@unittest.skipUnless(sys.platform.startswith('linux'), 'requires Linux')
class PlayerInterfaceTest(unittest.TestCase):
def setUp(self):
objects.MprisObject._connect_to_dbus = mock.Mock()

View File

@ -1,12 +1,19 @@
import sys
import mock
from mopidy import settings
from mopidy import OptionalDependencyError, settings
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpris import objects
try:
from mopidy.frontends.mpris import objects
except OptionalDependencyError:
pass
from tests import unittest
@unittest.skipUnless(sys.platform.startswith('linux'), 'requires Linux')
class RootInterfaceTest(unittest.TestCase):
def setUp(self):
objects.exit_process = mock.Mock()

View File

@ -6,8 +6,6 @@ from mopidy.utils.path import path_to_uri
from tests import unittest, path_to_data_dir
# TODO BaseOutputTest?
@unittest.skipIf(sys.platform == 'win32',
'Our Windows build server does not support GStreamer yet')

View File

@ -34,7 +34,7 @@ class DenonMixerTest(BaseMixerTest, unittest.TestCase):
def setUp(self):
self.device = DenonMixerDeviceMock()
self.mixer = DenonMixer(None, device=self.device)
self.mixer = DenonMixer(device=self.device)
def test_reopen_device(self):
self.device._open = False

View File

@ -4,7 +4,7 @@ from tests import unittest
from tests.mixers.base_test import BaseMixerTest
class DenonMixerTest(BaseMixerTest, unittest.TestCase):
class DummyMixerTest(BaseMixerTest, unittest.TestCase):
mixer_class = DummyMixer
def test_set_volume_is_capped(self):
@ -16,3 +16,8 @@ class DenonMixerTest(BaseMixerTest, unittest.TestCase):
self.mixer.amplification_factor = 0.5
self.mixer._volume = 50
self.assertEquals(self.mixer.volume, 100)
def test_get_volume_get_the_same_number_as_was_set(self):
self.mixer.amplification_factor = 0.5
self.mixer.volume = 13
self.assertEquals(self.mixer.volume, 13)

View File

@ -28,12 +28,32 @@ class GetOrCreateFolderTest(unittest.TestCase):
self.assert_(os.path.isdir(folder))
self.assertEqual(created, folder)
def test_creating_nested_folders(self):
level2_folder = os.path.join(self.parent, 'test')
level3_folder = os.path.join(self.parent, 'test', 'test')
self.assert_(not os.path.exists(level2_folder))
self.assert_(not os.path.isdir(level2_folder))
self.assert_(not os.path.exists(level3_folder))
self.assert_(not os.path.isdir(level3_folder))
created = get_or_create_folder(level3_folder)
self.assert_(os.path.exists(level2_folder))
self.assert_(os.path.isdir(level2_folder))
self.assert_(os.path.exists(level3_folder))
self.assert_(os.path.isdir(level3_folder))
self.assertEqual(created, level3_folder)
def test_creating_existing_folder(self):
created = get_or_create_folder(self.parent)
self.assert_(os.path.exists(self.parent))
self.assert_(os.path.isdir(self.parent))
self.assertEqual(created, self.parent)
def test_create_folder_with_name_of_existing_file_throws_oserror(self):
conflicting_file = os.path.join(self.parent, 'test')
open(conflicting_file, 'w').close()
folder = os.path.join(self.parent, 'test')
self.assertRaises(OSError, get_or_create_folder, folder)
class PathToFileURITest(unittest.TestCase):
def test_simple_path(self):

View File

@ -23,8 +23,9 @@ class VersionTest(unittest.TestCase):
self.assert_(SV('0.4.0') < SV('0.4.1'))
self.assert_(SV('0.4.1') < SV('0.5.0'))
self.assert_(SV('0.5.0') < SV('0.6.0'))
self.assert_(SV('0.6.0') < SV(get_plain_version()))
self.assert_(SV(get_plain_version()) < SV('0.7.0'))
self.assert_(SV('0.6.0') < SV('0.6.1'))
self.assert_(SV('0.6.1') < SV(get_plain_version()))
self.assert_(SV(get_plain_version()) < SV('0.7.1'))
def test_get_platform_contains_platform(self):
self.assert_(platform.platform() in get_platform())