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. platforms, including Windows, Mac OS X, Linux, Android and iOS.
To install Mopidy, check out 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 <http://docs.mopidy.com/>`_
- `Documentation for the development version
<http://www.mopidy.com/docs/develop/>`_
- `Source code <http://github.com/mopidy/mopidy>`_ - `Source code <http://github.com/mopidy/mopidy>`_
- `Issue tracker <http://github.com/mopidy/mopidy/issues>`_ - `Issue tracker <http://github.com/mopidy/mopidy/issues>`_
- IRC: ``#mopidy`` at `irc.freenode.net <http://freenode.net/>`_ - 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, - A frontend MAY do mostly whatever it wants to, including creating threads,
opening TCP ports and exposing Mopidy for a group of clients. opening TCP ports and exposing Mopidy for a group of clients.
- A frontend MUST implement at least one `Pykka - 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. on.
- It MAY use additional actors to implement whatever it does, and using actors - It MAY use additional actors to implement whatever it does, and using actors
in frontend implementations is encouraged. in frontend implementations is encouraged.

View File

@ -4,6 +4,41 @@ Changes
This change log is used to track all major changes to Mopidy. 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) v0.6.1 (2011-12-28)
=================== ===================
@ -23,6 +58,7 @@ make the Debian packages work out of the box again.
about bad playlists. about bad playlists.
v0.6.0 (2011-10-09) 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. minor bugs.
**Bugfixes** **Bug fixes**
- Fix broken audio on at least GStreamer 0.10.30, which affects Ubuntu 10.10. - 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. 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** **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 install from APT, Pykka will automatically be installed. If you are not
installing from APT, you may install Pykka from PyPI:: 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. 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. - 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. This is a maintenance release without any new features.
**Bugfixes** **Bug fixes**
- Fix crash in :mod:`mopidy.frontends.lastfm` which occurred at playback if - 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 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: - 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. - Several new generic features, like shuffle, consume, and playlist repeat.
(Fixes: :issue:`3`) (Fixes: :issue:`3`)
- **[Work in Progress]** A new backend for playing music from a local music - **[Work in Progress]** A new backend for playing music from a local music

View File

@ -13,20 +13,57 @@
import sys, os 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, # 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 # 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. # 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__)))
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 ----------------------------------------------------- # -- General configuration -----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be extensions # Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', extensions = ['sphinx.ext.autodoc', 'sphinx.ext.graphviz',
'sphinx.ext.graphviz', 'sphinx.ext.inheritance_diagram',
'sphinx.ext.extlinks', 'sphinx.ext.viewcode'] 'sphinx.ext.extlinks', 'sphinx.ext.viewcode']
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
@ -43,13 +80,14 @@ master_doc = 'index'
# General information about the project. # General information about the project.
project = u'Mopidy' 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 # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
# built documents. # built documents.
# #
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
import mopidy
release = mopidy.get_version() release = mopidy.get_version()
# The short X.Y version. # The short X.Y version.
version = '.'.join(release.split('.')[:2]) 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 # The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'. # 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 # 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 # 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 # The name of an image file (relative to this directory) to place at the top
# of the sidebar. # 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 # 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 # 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, # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format. # 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 # If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities. # typographically correct entities.
@ -202,4 +241,4 @@ latex_documents = [
needs_sphinx = '1.0' 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 To run tests, you need a couple of dependencies. They can be installed through
Debian/Ubuntu package management:: 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``:: 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:: 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:: Then, to generate docs::
@ -134,18 +134,8 @@ Then, to generate docs::
make # For help on available targets make # For help on available targets
make html # To generate HTML docs make html # To generate HTML docs
.. note:: The documentation at http://docs.mopidy.com/ is automatically updated when a
documentation update is pushed to ``mopidy/mopidy`` at GitHub.
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.
Creating releases Creating releases

View File

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

View File

@ -2,19 +2,21 @@
GStreamer installation 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 Installing GStreamer on Linux
==================== =============================
On Linux
--------
GStreamer is packaged for most popular Linux distributions. Search for GStreamer is packaged for most popular Linux distributions. Search for
GStreamer in your package manager, and make sure to install the Python GStreamer in your package manager, and make sure to install the Python
bindings, and the "good" and "ugly" plugin sets. bindings, and the "good" and "ugly" plugin sets.
Debian/Ubuntu
-------------
If you use Debian/Ubuntu you can install GStreamer like this:: If you use Debian/Ubuntu you can install GStreamer like this::
sudo apt-get install python-gst0.10 gstreamer0.10-plugins-good \ sudo apt-get install python-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. 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:: .. note::
We have created GStreamer formulas for Homebrew to make the GStreamer We have been working with `Homebrew <https://github.com/mxcl/homebrew>`_ to
installation easy for you, but not all our formulas have been merged into make all the GStreamer packages easily installable on OS X using Homebrew.
Homebrew's master branch yet. You should either fetch the formula files We've gotten most of our packages included, but the Homebrew guys aren't
from `Homebrew's issue #1612 very happy to include Python specific packages into Homebrew, even though
<http://github.com/mxcl/homebrew/issues/issue/1612>`_ yourself, or fall they are not installable by pip. If you're interested, see the discussion
back to using MacPorts. 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 \ brew install gst-python gst-plugins-good gst-plugins-ugly
gstreamer-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 Testing the installation

View File

@ -18,36 +18,36 @@ Requirements
gstreamer gstreamer
libspotify libspotify
If you install Mopidy from the APT archive, as described below, you can skip If you install Mopidy from the APT archive, as described below, APT will take
the dependency installation part. 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 - GStreamer 0.10.x, with Python bindings. See :doc:`gstreamer`.
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.
- Optional dependencies: - Optional dependencies:
- To use the Last.FM scrobbler, see :mod:`mopidy.frontends.lastfm` for - For Spotify support, you need libspotify and pyspotify. See
additional requirements. :doc:`libspotify`.
- To use the MPRIS frontend, e.g. using the Ubuntu Sound Menu, see - To scrobble your played tracks to Last.FM, you need pylast::
:mod:`mopidy.frontends.mpris` for additional requirements.
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 Install latest stable release
@ -100,8 +100,8 @@ install Mopidy from PyPI using Pip.
#. Then, you need to install Pip:: #. Then, you need to install Pip::
sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian sudo apt-get install python-setuptools python-pip # On Ubuntu/Debian
sudo easy_install pip # On OS X sudo easy_install pip # On OS X
#. To install the currently latest stable release of Mopidy:: #. 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 #. Next, you need to set a couple of :doc:`settings </settings>`, and then
you're ready to :doc:`run Mopidy </running>`. you're ready to :doc:`run Mopidy </running>`.
If you for some reason can't use Pip, try ``easy_install`` instead.
Install development version Install development version
=========================== ===========================
@ -134,8 +132,8 @@ Mopidy's ``develop`` branch.
#. Then, you need to install Pip:: #. Then, you need to install Pip::
sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian sudo apt-get install python-setuptools python-pip # On Ubuntu/Debian
sudo easy_install pip # On OS X sudo easy_install pip # On OS X
#. To install the latest snapshot of Mopidy, run:: #. 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:: #. 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 sudo brew install git # On OS X using Homebrew
#. Clone the official Mopidy repository, or your own fork of it:: #. 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 This backend requires a paid `Spotify premium account
<http://www.spotify.com/no/get-spotify/premium/>`_. <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 Installing libspotify
===================== =====================
@ -26,23 +20,20 @@ Installing libspotify
On Linux from APT archive On Linux from APT archive
------------------------- -------------------------
If you run a Debian based Linux distribution, like Ubuntu, see If you install from APT, jump directly to :ref:`pyspotify_installation` below.
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`.
On Linux from source On Linux from source
-------------------- --------------------
Download and install libspotify 0.0.8 for your OS and CPU architecture from First, check pyspotify's changelog to see what's the latest version of
https://developer.spotify.com/en/libspotify/. 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 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 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 make install prefix=/usr/local
sudo ldconfig 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 When libspotify has been installed, continue with
:ref:`pyspotify_installation`. :ref:`pyspotify_installation`.
@ -66,7 +60,7 @@ libspotify::
To update your existing libspotify installation using Homebrew:: To update your existing libspotify installation using Homebrew::
brew update brew update
brew install `brew outdated` brew upgrade
When libspotify has been installed, continue with When libspotify has been installed, continue with
:ref:`pyspotify_installation`. :ref:`pyspotify_installation`.
@ -84,29 +78,35 @@ by installing pyspotify.
On Linux from APT archive On Linux from APT archive
------------------------- -------------------------
Assuming that you've already set up http://apt.mopidy.com/ as a software If you run a Debian based Linux distribution, like Ubuntu, see
source, run:: 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 sudo apt-get install python-spotify
If you haven't already installed libspotify, this command will install both This command will install both libspotify and pyspotify for you.
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 On Linux, you need to get the Python development files installed. On
Debian/Ubuntu systems run:: Debian/Ubuntu systems run::
sudo apt-get install python-dev 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``:: Then get, build, and install the latest releast of pyspotify using ``pip``::
sudo pip install -U pyspotify 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 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -26,7 +26,7 @@ limitations under the License.
Documentation 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 This work is licensed under the Creative Commons Attribution-ShareAlike 3.0
Unported License. To view a copy of this license, visit Unported License. To view a copy of this license, visit

View File

@ -2,8 +2,6 @@
:mod:`mopidy.frontends.mpd` -- MPD server :mod:`mopidy.frontends.mpd` -- MPD server
***************************************** *****************************************
.. inheritance-diagram:: mopidy.frontends.mpd
.. automodule:: mopidy.frontends.mpd .. automodule:: mopidy.frontends.mpd
:synopsis: MPD server frontend :synopsis: MPD server frontend
:members: :members:
@ -12,8 +10,6 @@
MPD dispatcher MPD dispatcher
============== ==============
.. inheritance-diagram:: mopidy.frontends.mpd.dispatcher
.. automodule:: mopidy.frontends.mpd.dispatcher .. automodule:: mopidy.frontends.mpd.dispatcher
:synopsis: MPD request dispatcher :synopsis: MPD request dispatcher
:members: :members:

View File

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

View File

@ -2,8 +2,6 @@
:mod:`mopidy.mixers.alsa` -- ALSA mixer for Linux :mod:`mopidy.mixers.alsa` -- ALSA mixer for Linux
************************************************* *************************************************
.. inheritance-diagram:: mopidy.mixers.alsa
.. automodule:: mopidy.mixers.alsa .. automodule:: mopidy.mixers.alsa
:synopsis: ALSA mixer for Linux :synopsis: ALSA mixer for Linux
:members: :members:

View File

@ -2,8 +2,6 @@
:mod:`mopidy.mixers.denon` -- Hardware mixer for Denon amplifiers :mod:`mopidy.mixers.denon` -- Hardware mixer for Denon amplifiers
***************************************************************** *****************************************************************
.. inheritance-diagram:: mopidy.mixers.denon
.. automodule:: mopidy.mixers.denon .. automodule:: mopidy.mixers.denon
:synopsis: Hardware mixer for Denon amplifiers :synopsis: Hardware mixer for Denon amplifiers
:members: :members:

View File

@ -2,8 +2,6 @@
:mod:`mopidy.mixers.dummy` -- Dummy mixer for testing :mod:`mopidy.mixers.dummy` -- Dummy mixer for testing
***************************************************** *****************************************************
.. inheritance-diagram:: mopidy.mixers.dummy
.. automodule:: mopidy.mixers.dummy .. automodule:: mopidy.mixers.dummy
:synopsis: Dummy mixer for testing :synopsis: Dummy mixer for testing
:members: :members:

View File

@ -2,8 +2,6 @@
:mod:`mopidy.mixers.gstreamer_software` -- Software mixer for all platforms :mod:`mopidy.mixers.gstreamer_software` -- Software mixer for all platforms
*************************************************************************** ***************************************************************************
.. inheritance-diagram:: mopidy.mixers.gstreamer_software
.. automodule:: mopidy.mixers.gstreamer_software .. automodule:: mopidy.mixers.gstreamer_software
:synopsis: Software mixer for all platforms :synopsis: Software mixer for all platforms
:members: :members:

View File

@ -2,8 +2,6 @@
:mod:`mopidy.mixers.nad` -- Hardware mixer for NAD amplifiers :mod:`mopidy.mixers.nad` -- Hardware mixer for NAD amplifiers
************************************************************* *************************************************************
.. inheritance-diagram:: mopidy.mixers.nad
.. automodule:: mopidy.mixers.nad .. automodule:: mopidy.mixers.nad
:synopsis: Hardware mixer for NAD amplifiers :synopsis: Hardware mixer for NAD amplifiers
:members: :members:

View File

@ -2,8 +2,6 @@
:mod:`mopidy.mixers.osa` -- Osa mixer for OS X :mod:`mopidy.mixers.osa` -- Osa mixer for OS X
********************************************** **********************************************
.. inheritance-diagram:: mopidy.mixers.osa
.. automodule:: mopidy.mixers.osa .. automodule:: mopidy.mixers.osa
:synopsis: Osa mixer for OS X :synopsis: Osa mixer for OS X
:members: :members:

View File

@ -4,11 +4,8 @@
The following GStreamer audio outputs implements the :ref:`output-api`. The following GStreamer audio outputs implements the :ref:`output-api`.
.. inheritance-diagram:: mopidy.outputs.custom
.. autoclass:: mopidy.outputs.custom.CustomOutput .. autoclass:: mopidy.outputs.custom.CustomOutput
.. inheritance-diagram:: mopidy.outputs.local
.. autoclass:: mopidy.outputs.local.LocalOutput .. autoclass:: mopidy.outputs.local.LocalOutput
.. inheritance-diagram:: mopidy.outputs.shoutcast
.. autoclass:: mopidy.outputs.shoutcast.ShoutcastOutput .. autoclass:: mopidy.outputs.shoutcast.ShoutcastOutput

View File

@ -8,11 +8,11 @@ import os
from subprocess import PIPE, Popen from subprocess import PIPE, Popen
VERSION = (0, 6, 1) VERSION = (0, 7, 0)
DATA_PATH = os.path.join(glib.get_user_data_dir(), 'mopidy') DATA_PATH = os.path.join(str(glib.get_user_data_dir()), 'mopidy')
CACHE_PATH = os.path.join(glib.get_user_cache_dir(), 'mopidy') CACHE_PATH = os.path.join(str(glib.get_user_cache_dir()), 'mopidy')
SETTINGS_PATH = os.path.join(glib.get_user_config_dir(), 'mopidy') SETTINGS_PATH = os.path.join(str(glib.get_user_config_dir()), 'mopidy')
SETTINGS_FILE = os.path.join(SETTINGS_PATH, 'settings.py') SETTINGS_FILE = os.path.join(SETTINGS_PATH, 'settings.py')
def get_version(): def get_version():

View File

@ -28,7 +28,7 @@ class CurrentPlaylistController(object):
Read-only. Read-only.
""" """
return [copy(ct) for ct in self._cp_tracks] return [copy(cp_track) for cp_track in self._cp_tracks]
@property @property
def tracks(self): def tracks(self):
@ -37,7 +37,14 @@ class CurrentPlaylistController(object):
Read-only. 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 @property
def version(self): def version(self):
@ -116,9 +123,9 @@ class CurrentPlaylistController(object):
matches = self._cp_tracks matches = self._cp_tracks
for (key, value) in criteria.iteritems(): for (key, value) in criteria.iteritems():
if key == 'cpid': if key == 'cpid':
matches = filter(lambda ct: ct[0] == value, matches) matches = filter(lambda ct: ct.cpid == value, matches)
else: else:
matches = filter(lambda ct: getattr(ct[1], key) == value, matches = filter(lambda ct: getattr(ct.track, key) == value,
matches) matches)
if len(matches) == 1: if len(matches) == 1:
return matches[0] return matches[0]
@ -129,6 +136,19 @@ class CurrentPlaylistController(object):
else: else:
raise LookupError(u'"%s" match multiple tracks' % criteria_string) 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): def move(self, start, end, to_position):
""" """
Move the tracks in the slice ``[start:end]`` to ``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 :param criteria: on or more criteria to match by
:type criteria: dict :type criteria: dict
:type track: :class:`mopidy.models.Track`
""" """
cp_track = self.get(**criteria) cp_track = self.get(**criteria)
position = self._cp_tracks.index(cp_track) position = self._cp_tracks.index(cp_track)
@ -204,6 +223,19 @@ class CurrentPlaylistController(object):
self._cp_tracks = before + shuffled + after self._cp_tracks = before + shuffled + after
self.version += 1 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): def _trigger_playlist_changed(self):
logger.debug(u'Triggering playlist changed event') logger.debug(u'Triggering playlist changed event')
BackendListener.send('playlist_changed') BackendListener.send('playlist_changed')

View File

@ -2,8 +2,6 @@ import logging
import random import random
import time import time
from pykka.registry import ActorRegistry
from mopidy.listeners import BackendListener from mopidy.listeners import BackendListener
logger = logging.getLogger('mopidy.backends.base') 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_PLAYLIST_PATH = os.path.join(DATA_PATH, 'playlists')
DEFAULT_TAG_CACHE_FILE = os.path.join(DATA_PATH, 'tag_cache') 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'~'): if not DEFAULT_MUSIC_PATH or DEFAULT_MUSIC_PATH == os.path.expanduser(u'~'):
DEFAULT_MUSIC_PATH = os.path.expanduser(u'~/music') 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') logger = logging.getLogger('mopidy.backends.spotify')
ENCODING = 'utf-8'
BITRATES = {96: 2, 160: 0, 320: 1} BITRATES = {96: 2, 160: 0, 320: 1}
class SpotifyBackend(ThreadingActor, Backend): class SpotifyBackend(ThreadingActor, Backend):

View File

@ -4,7 +4,6 @@ import Queue
from spotify import Link, SpotifyError from spotify import Link, SpotifyError
from mopidy.backends.base import BaseLibraryProvider from mopidy.backends.base import BaseLibraryProvider
from mopidy.backends.spotify import ENCODING
from mopidy.backends.spotify.translator import SpotifyTranslator from mopidy.backends.spotify.translator import SpotifyTranslator
from mopidy.models import Playlist from mopidy.models import Playlist

View File

@ -27,7 +27,8 @@ class SpotifyPlaylistManager(PyspotifyPlaylistManager):
def tracks_removed(self, playlist, tracks, userdata): def tracks_removed(self, playlist, tracks, userdata):
"""Callback used by pyspotify""" """Callback used by pyspotify"""
logger.debug(u'Callback called: ' 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() self.session_manager.refresh_stored_playlists()
def playlist_renamed(self, playlist, userdata): def playlist_renamed(self, playlist, userdata):

View File

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

View File

@ -4,7 +4,6 @@ import logging
from spotify import Link, SpotifyError from spotify import Link, SpotifyError
from mopidy import settings from mopidy import settings
from mopidy.backends.spotify import ENCODING
from mopidy.models import Artist, Album, Track, Playlist from mopidy.models import Artist, Album, Track, Playlist
logger = logging.getLogger('mopidy.backends.spotify.translator') logger = logging.getLogger('mopidy.backends.spotify.translator')
@ -31,9 +30,10 @@ class SpotifyTranslator(object):
uri = str(Link.from_track(spotify_track, 0)) uri = str(Link.from_track(spotify_track, 0))
if not spotify_track.is_loaded(): if not spotify_track.is_loaded():
return Track(uri=uri, name=u'[loading...]') return Track(uri=uri, name=u'[loading...]')
if (spotify_track.album() is not None and spotify_album = spotify_track.album()
dt.MINYEAR <= int(spotify_track.album().year()) <= dt.MAXYEAR): if (spotify_album is not None and spotify_album.is_loaded()
date = dt.date(spotify_track.album().year(), 1, 1) and dt.MINYEAR <= int(spotify_album.year()) <= dt.MAXYEAR):
date = dt.date(spotify_album.year(), 1, 1)
else: else:
date = None date = None
return Track( return Track(

View File

@ -12,14 +12,12 @@ gobject.threads_init()
# so that GStreamer doesn't hijack e.g. ``--help``. # so that GStreamer doesn't hijack e.g. ``--help``.
# NOTE This naive fix does not support values like ``bar`` in # NOTE This naive fix does not support values like ``bar`` in
# ``--gst-foo bar``. Use equals to pass values, like ``--gst-foo=bar``. # ``--gst-foo bar``. Use equals to pass values, like ``--gst-foo=bar``.
def is_gst_arg(arg): def is_gst_arg(argument):
return arg.startswith('--gst') or arg == '--help-gst' return argument.startswith('--gst') or argument == '--help-gst'
gstreamer_args = [arg for arg in sys.argv[1:] if is_gst_arg(arg)] 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)] mopidy_args = [arg for arg in sys.argv[1:] if not is_gst_arg(arg)]
sys.argv[1:] = gstreamer_args sys.argv[1:] = gstreamer_args
from pykka.registry import ActorRegistry
from mopidy import (get_version, settings, OptionalDependencyError, from mopidy import (get_version, settings, OptionalDependencyError,
SettingsError, DATA_PATH, SETTINGS_PATH, SETTINGS_FILE) SettingsError, DATA_PATH, SETTINGS_PATH, SETTINGS_FILE)
from mopidy.gstreamer import GStreamer from mopidy.gstreamer import GStreamer

View File

@ -37,6 +37,7 @@ class LastfmFrontend(ThreadingActor, BackendListener):
""" """
def __init__(self): def __init__(self):
super(LastfmFrontend, self).__init__()
self.lastfm = None self.lastfm = None
self.last_start_time = None self.last_start_time = None

View File

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

View File

@ -244,7 +244,8 @@ class MpdContext(object):
""" """
if self._backend is None: if self._backend is None:
backend_refs = ActorRegistry.get_by_class(Backend) 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() self._backend = backend_refs[0].proxy()
return self._backend return self._backend

View File

@ -1,7 +1,8 @@
from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError, from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError,
MpdNotImplemented) MpdNotImplemented)
from mopidy.frontends.mpd.protocol import handle_request 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>[^"]*)"$') @handle_request(r'^add "(?P<uri>[^"]*)"$')
def add(context, uri): def add(context, uri):
@ -74,8 +75,8 @@ def delete_range(context, start, end=None):
if end is not None: if end is not None:
end = int(end) end = int(end)
else: else:
end = len(context.backend.current_playlist.tracks.get()) end = context.backend.current_playlist.length.get()
cp_tracks = context.backend.current_playlist.cp_tracks.get()[start:end] cp_tracks = context.backend.current_playlist.slice(start, end).get()
if not cp_tracks: if not cp_tracks:
raise MpdArgError(u'Bad song index', command=u'delete') raise MpdArgError(u'Bad song index', command=u'delete')
for (cpid, _) in cp_tracks: for (cpid, _) in cp_tracks:
@ -86,7 +87,8 @@ def delete_songpos(context, songpos):
"""See :meth:`delete_range`""" """See :meth:`delete_range`"""
try: try:
songpos = int(songpos) 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) context.backend.current_playlist.remove(cpid=cpid)
except IndexError: except IndexError:
raise MpdArgError(u'Bad song index', command=u'delete') raise MpdArgError(u'Bad song index', command=u'delete')
@ -157,8 +159,7 @@ def moveid(context, cpid, to):
cpid = int(cpid) cpid = int(cpid)
to = int(to) to = int(to)
cp_track = context.backend.current_playlist.get(cpid=cpid).get() cp_track = context.backend.current_playlist.get(cpid=cpid).get()
position = context.backend.current_playlist.cp_tracks.get().index( position = context.backend.current_playlist.index(cp_track).get()
cp_track)
context.backend.current_playlist.move(position, position + 1, to) context.backend.current_playlist.move(position, position + 1, to)
@handle_request(r'^playlist$') @handle_request(r'^playlist$')
@ -193,10 +194,8 @@ def playlistfind(context, tag, needle):
if tag == 'filename': if tag == 'filename':
try: try:
cp_track = context.backend.current_playlist.get(uri=needle).get() cp_track = context.backend.current_playlist.get(uri=needle).get()
(cpid, track) = cp_track position = context.backend.current_playlist.index(cp_track).get()
position = context.backend.current_playlist.cp_tracks.get().index( return track_to_mpd_format(cp_track, position=position)
cp_track)
return track.mpd_format(cpid=cpid, position=position)
except LookupError: except LookupError:
return None return None
raise MpdNotImplemented # TODO raise MpdNotImplemented # TODO
@ -215,18 +214,16 @@ def playlistid(context, cpid=None):
try: try:
cpid = int(cpid) cpid = int(cpid)
cp_track = context.backend.current_playlist.get(cpid=cpid).get() cp_track = context.backend.current_playlist.get(cpid=cpid).get()
position = context.backend.current_playlist.cp_tracks.get().index( position = context.backend.current_playlist.index(cp_track).get()
cp_track) return track_to_mpd_format(cp_track, position=position)
return cp_track.track.mpd_format(position=position, cpid=cpid)
except LookupError: except LookupError:
raise MpdNoExistError(u'No such song', command=u'playlistid') raise MpdNoExistError(u'No such song', command=u'playlistid')
else: else:
cpids = [ct[0] for ct in
context.backend.current_playlist.cp_tracks.get()]
return tracks_to_mpd_format( 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$')
@handle_request(r'^playlistinfo "-1"$')
@handle_request(r'^playlistinfo "(?P<songpos>-?\d+)"$') @handle_request(r'^playlistinfo "(?P<songpos>-?\d+)"$')
@handle_request(r'^playlistinfo "(?P<start>\d+):(?P<end>\d+)*"$') @handle_request(r'^playlistinfo "(?P<start>\d+):(?P<end>\d+)*"$')
def playlistinfo(context, songpos=None, def playlistinfo(context, songpos=None,
@ -245,36 +242,22 @@ def playlistinfo(context, songpos=None,
- uses negative indexes, like ``playlistinfo "-1"``, to request - uses negative indexes, like ``playlistinfo "-1"``, to request
the entire playlist the entire playlist
""" """
if songpos == "-1":
songpos = None
if songpos is not None: if songpos is not None:
songpos = int(songpos) songpos = int(songpos)
start = songpos cp_track = context.backend.current_playlist.get(cpid=songpos).get()
end = songpos + 1 return track_to_mpd_format(cp_track, position=songpos)
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)
else: else:
if start is None: if start is None:
start = 0 start = 0
start = int(start) start = int(start)
if not (0 <= start <= len( if not (0 <= start <= context.backend.current_playlist.length.get()):
context.backend.current_playlist.tracks.get())):
raise MpdArgError(u'Bad song index', command=u'playlistinfo') raise MpdArgError(u'Bad song index', command=u'playlistinfo')
if end is not None: if end is not None:
end = int(end) end = int(end)
if end > len(context.backend.current_playlist.tracks.get()): if end > context.backend.current_playlist.length.get():
end = None end = None
cpids = [ct[0] for ct in cp_tracks = context.backend.current_playlist.cp_tracks.get()
context.backend.current_playlist.cp_tracks.get()] return tracks_to_mpd_format(cp_tracks, start, end)
return tracks_to_mpd_format(
context.backend.current_playlist.tracks.get(),
start, end, cpids=cpids)
@handle_request(r'^playlistsearch "(?P<tag>[^"]+)" "(?P<needle>[^"]+)"$') @handle_request(r'^playlistsearch "(?P<tag>[^"]+)" "(?P<needle>[^"]+)"$')
@handle_request(r'^playlistsearch (?P<tag>\S+) "(?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 # XXX Naive implementation that returns all tracks as changed
if int(version) < context.backend.current_playlist.version: 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( 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+)"$') @handle_request(r'^plchangesposid "(?P<version>\d+)"$')
def plchangesposid(context, version): def plchangesposid(context, version):
@ -392,7 +373,6 @@ def swapid(context, cpid1, cpid2):
cpid2 = int(cpid2) cpid2 = int(cpid2)
cp_track1 = context.backend.current_playlist.get(cpid=cpid1).get() cp_track1 = context.backend.current_playlist.get(cpid=cpid1).get()
cp_track2 = context.backend.current_playlist.get(cpid=cpid2).get() cp_track2 = context.backend.current_playlist.get(cpid=cpid2).get()
cp_tracks = context.backend.current_playlist.cp_tracks.get() position1 = context.backend.current_playlist.index(cp_track1).get()
position1 = cp_tracks.index(cp_track1) position2 = context.backend.current_playlist.index(cp_track2).get()
position2 = cp_tracks.index(cp_track2)
swap(context, position1, position2) swap(context, position1, position2)

View File

@ -1,8 +1,9 @@
import re import re
import shlex import shlex
from mopidy.frontends.mpd.protocol import handle_request, stored_playlists
from mopidy.frontends.mpd.exceptions import MpdArgError, MpdNotImplemented 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): def _build_query(mpd_query):
""" """
@ -68,7 +69,8 @@ def find(context, mpd_query):
- also uses the search type "date". - also uses the search type "date".
""" """
query = _build_query(mpd_query) 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 ' @handle_request(r'^findadd '
r'(?P<query>("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? ' 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.""" """Converts a ``list`` query to a Mopidy query."""
if mpd_query is None: if mpd_query is None:
return {} return {}
# shlex does not seem to be friends with unicode objects try:
tokens = shlex.split(mpd_query.encode('utf-8')) # 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] tokens = [t.decode('utf-8') for t in tokens]
if len(tokens) == 1: if len(tokens) == 1:
if field == u'album': if field == u'album':
@ -324,7 +332,8 @@ def search(context, mpd_query):
- also uses the search type "date". - also uses the search type "date".
""" """
query = _build_query(mpd_query) 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>[^"]+)")*$') @handle_request(r'^update( "(?P<uri>[^"]+)")*$')
def update(context, uri=None, rescan_unmodified_files=False): def update(context, uri=None, rescan_unmodified_files=False):

View File

@ -178,7 +178,8 @@ def playpos(context, songpos):
if songpos == -1: if songpos == -1:
return _play_minus_one(context) return _play_minus_one(context)
try: 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() return context.backend.playback.play(cp_track).get()
except IndexError: except IndexError:
raise MpdArgError(u'Bad song index', command=u'play') 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: elif context.backend.playback.current_cp_track.get() is not None:
cp_track = context.backend.playback.current_cp_track.get() cp_track = context.backend.playback.current_cp_track.get()
return context.backend.playback.play(cp_track).get() return context.backend.playback.play(cp_track).get()
elif context.backend.current_playlist.cp_tracks.get(): elif context.backend.current_playlist.slice(0, 1).get():
cp_track = context.backend.current_playlist.cp_tracks.get()[0] cp_track = context.backend.current_playlist.slice(0, 1).get()[0]
return context.backend.playback.play(cp_track).get() return context.backend.playback.play(cp_track).get()
else: else:
return # Fail silently return # Fail silently

View File

@ -1,8 +1,9 @@
import pykka.future import pykka.future
from mopidy.backends.base import PlaybackController 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.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 that can be registered with idle command.
SUBSYSTEMS = ['database', 'mixer', 'options', 'output', SUBSYSTEMS = ['database', 'mixer', 'options', 'output',
@ -32,9 +33,8 @@ def currentsong(context):
""" """
current_cp_track = context.backend.playback.current_cp_track.get() current_cp_track = context.backend.playback.current_cp_track.get()
if current_cp_track is not None: if current_cp_track is not None:
return current_cp_track.track.mpd_format( position = context.backend.playback.current_playlist_position.get()
position=context.backend.playback.current_playlist_position.get(), return track_to_mpd_format(current_cp_track, position=position)
cpid=current_cp_track.cpid)
@handle_request(r'^idle$') @handle_request(r'^idle$')
@handle_request(r'^idle (?P<subsystems>.+)$') @handle_request(r'^idle (?P<subsystems>.+)$')
@ -166,7 +166,7 @@ def status(context):
decimal places for millisecond precision. decimal places for millisecond precision.
""" """
futures = { 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, 'current_playlist.version': context.backend.current_playlist.version,
'mixer.volume': context.mixer.volume, 'mixer.volume': context.mixer.volume,
'playback.consume': context.backend.playback.consume, 'playback.consume': context.backend.playback.consume,
@ -213,7 +213,7 @@ def _status_consume(futures):
return 0 return 0
def _status_playlist_length(futures): def _status_playlist_length(futures):
return len(futures['current_playlist.tracks'].get()) return futures['current_playlist.length'].get()
def _status_playlist_version(futures): def _status_playlist_version(futures):
return futures['current_playlist.version'].get() return futures['current_playlist.version'].get()

View File

@ -1,7 +1,8 @@
import datetime as dt import datetime as dt
from mopidy.frontends.mpd.protocol import handle_request
from mopidy.frontends.mpd.exceptions import MpdNoExistError, MpdNotImplemented 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>[^"]+)"$') @handle_request(r'^listplaylist "(?P<name>[^"]+)"$')
def listplaylist(context, name): def listplaylist(context, name):
@ -40,7 +41,7 @@ def listplaylistinfo(context, name):
""" """
try: try:
playlist = context.backend.stored_playlists.get(name=name).get() playlist = context.backend.stored_playlists.get(name=name).get()
return playlist.mpd_format() return playlist_to_mpd_format(playlist)
except LookupError: except LookupError:
raise MpdNoExistError( raise MpdNoExistError(
u'No such playlist', command=u'listplaylistinfo') u'No such playlist', command=u'listplaylistinfo')

View File

@ -2,26 +2,28 @@ import os
import re import re
from mopidy import settings from mopidy import settings
from mopidy.utils.path import mtime as get_mtime
from mopidy.frontends.mpd import protocol 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. Format track for output to MPD client.
:param track: the track :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 :param position: track's position in playlist
:type position: integer :type position: integer
:param cpid: track's CPID (current playlist ID)
:type cpid: integer
:param key: if we should set key :param key: if we should set key
:type key: boolean :type key: boolean
:param mtime: if we should set mtime :param mtime: if we should set mtime
:type mtime: boolean :type mtime: boolean
:rtype: list of two-tuples :rtype: list of two-tuples
""" """
if isinstance(track, CpTrack):
(cpid, track) = track
else:
(cpid, track) = (None, track)
result = [ result = [
('file', track.uri or ''), ('file', track.uri or ''),
('Time', track.length and (track.length // 1000) or 0), ('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) artists.sort(key=lambda a: a.name)
return u', '.join([a.name for a in artists if 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. Format list of tracks for output to MPD client.
Optionally limit output to the slice ``[start:end]`` of the list. Optionally limit output to the slice ``[start:end]`` of the list.
:param tracks: the tracks :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 :param start: position of first track to include in output
:type start: int (positive or negative) :type start: int (positive or negative)
:param end: position after last track to include in output :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) end = len(tracks)
tracks = tracks[start:end] tracks = tracks[start:end]
positions = range(start, end) positions = range(start, end)
cpids = cpids and cpids[start:end] or [None for _ in tracks] assert len(tracks) == len(positions)
assert len(tracks) == len(positions) == len(cpids)
result = [] result = []
for track, position, cpid in zip(tracks, positions, cpids): for track, position in zip(tracks, positions):
result.append(track_to_mpd_format(track, position, cpid)) result.append(track_to_mpd_format(track, position))
return result return result
def playlist_to_mpd_format(playlist, *args, **kwargs): def playlist_to_mpd_format(playlist, *args, **kwargs):

View File

@ -57,6 +57,7 @@ class MprisFrontend(ThreadingActor, BackendListener):
""" """
def __init__(self): def __init__(self):
super(MprisFrontend, self).__init__()
self.indicate_server = None self.indicate_server = None
self.mpris_object = 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 # Must be done before dbus.SessionBus() is called
gobject.threads_init() gobject.threads_init()
dbus.mainloop.glib.threads_init() dbus.mainloop.glib.threads_init()
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
BUS_NAME = 'org.mpris.MediaPlayer2.mopidy' BUS_NAME = 'org.mpris.MediaPlayer2.mopidy'
OBJECT_PATH = '/org/mpris/MediaPlayer2' OBJECT_PATH = '/org/mpris/MediaPlayer2'
@ -81,7 +80,9 @@ class MprisObject(dbus.service.Object):
def _connect_to_dbus(self): def _connect_to_dbus(self):
logger.debug(u'Connecting to D-Bus...') 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') logger.info(u'Connected to D-Bus')
return bus_name return bus_name

View File

@ -13,15 +13,6 @@ from mopidy.backends.base import Backend
logger = logging.getLogger('mopidy.gstreamer') 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): class GStreamer(ThreadingActor):
""" """
@ -34,6 +25,15 @@ class GStreamer(ThreadingActor):
""" """
def __init__(self): 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._pipeline = None
self._source = None self._source = None
self._tee = None self._tee = None
@ -77,12 +77,14 @@ class GStreamer(ThreadingActor):
def _on_new_source(self, element, pad): def _on_new_source(self, element, pad):
self._source = element.get_property('source') self._source = element.get_property('source')
try: try:
self._source.set_property('caps', default_caps) self._source.set_property('caps', self._default_caps)
except TypeError: except TypeError:
pass pass
def _on_new_pad(self, source, pad, target_pad): def _on_new_pad(self, source, pad, target_pad):
if not pad.is_linked(): if not pad.is_linked():
if target_pad.is_linked():
target_pad.get_peer().unlink(target_pad)
pad.link(target_pad) pad.link(target_pad)
def _on_message(self, bus, message): def _on_message(self, bus, message):
@ -333,7 +335,8 @@ class GStreamer(ThreadingActor):
self._tee.send_event(event) self._tee.send_event(event)
def _handle_event_probe(self, teesrc, 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()) data = self._get_structure_data(event.get_structure())
output = teesrc.get_peer().get_parent() output = teesrc.get_peer().get_parent()

View File

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

View File

@ -2,7 +2,7 @@ import logging
from mopidy import listeners, settings from mopidy import listeners, settings
logger = logging.getLogger('mopdy.mixers') logger = logging.getLogger('mopidy.mixers')
class BaseMixer(object): class BaseMixer(object):
""" """
@ -21,19 +21,30 @@ class BaseMixer(object):
Integer in range [0, 100]. :class:`None` if unknown. Values below 0 is Integer in range [0, 100]. :class:`None` if unknown. Values below 0 is
equal to 0. Values above 100 is equal to 100. equal to 0. Values above 100 is equal to 100.
""" """
if not hasattr(self, '_user_volume'):
self._user_volume = 0
volume = self.get_volume() volume = self.get_volume()
if volume is None: if volume is None or not self.amplification_factor < 1:
return None return volume
return int(volume / self.amplification_factor) 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 @volume.setter
def volume(self, volume): 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: if volume < 0:
volume = 0 volume = 0
elif volume > 100: elif volume > 100:
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() self._trigger_volume_changed()
def get_volume(self): def get_volume(self):

View File

@ -25,8 +25,9 @@ class DenonMixer(ThreadingActor, BaseMixer):
- :attr:`mopidy.settings.MIXER_EXT_PORT` -- Example: ``/dev/ttyUSB0`` - :attr:`mopidy.settings.MIXER_EXT_PORT` -- Example: ``/dev/ttyUSB0``
""" """
def __init__(self, *args, **kwargs): def __init__(self, device=None):
self._device = kwargs.get('device', None) super(DenonMixer, self).__init__()
self._device = device
self._levels = ['99'] + ["%(#)02d" % {'#': v} for v in range(0, 99)] self._levels = ['99'] + ["%(#)02d" % {'#': v} for v in range(0, 99)]
self._volume = 0 self._volume = 0

View File

@ -6,6 +6,7 @@ class DummyMixer(ThreadingActor, BaseMixer):
"""Mixer which just stores and reports the chosen volume.""" """Mixer which just stores and reports the chosen volume."""
def __init__(self): def __init__(self):
super(DummyMixer, self).__init__()
self._volume = None self._volume = None
def get_volume(self): def get_volume(self):

View File

@ -8,6 +8,7 @@ class GStreamerSoftwareMixer(ThreadingActor, BaseMixer):
"""Mixer which uses GStreamer to control volume in software.""" """Mixer which uses GStreamer to control volume in software."""
def __init__(self): def __init__(self):
super(GStreamerSoftwareMixer, self).__init__()
self.output = None self.output = None
def on_start(self): def on_start(self):

View File

@ -37,6 +37,7 @@ class NadMixer(ThreadingActor, BaseMixer):
""" """
def __init__(self): def __init__(self):
super(NadMixer, self).__init__()
self._volume_cache = None self._volume_cache = None
self._nad_talker = NadTalker.start().proxy() self._nad_talker = NadTalker.start().proxy()
@ -71,6 +72,7 @@ class NadTalker(ThreadingActor):
_nad_volume = None _nad_volume = None
def __init__(self): def __init__(self):
super(NadTalker, self).__init__()
self._device = None self._device = None
def on_start(self): def on_start(self):

View File

@ -185,10 +185,6 @@ class Track(ImmutableObject):
self.__dict__['artists'] = frozenset(kwargs.pop('artists', [])) self.__dict__['artists'] = frozenset(kwargs.pop('artists', []))
super(Track, self).__init__(*args, **kwargs) 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): class Playlist(ImmutableObject):
""" """
@ -224,7 +220,3 @@ class Playlist(ImmutableObject):
def length(self): def length(self):
"""The number of tracks in the playlist. Read-only.""" """The number of tracks in the playlist. Read-only."""
return len(self.tracks) 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' encoding = 'utf-8'
def __init__(self, connection): def __init__(self, connection):
super(LineProtocol, self).__init__()
self.connection = connection self.connection = connection
self.prevent_timeout = False self.prevent_timeout = False
self.recv_buffer = '' self.recv_buffer = ''

View File

@ -8,9 +8,12 @@ logger = logging.getLogger('mopidy.utils.path')
def get_or_create_folder(folder): def get_or_create_folder(folder):
folder = os.path.expanduser(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) logger.info(u'Creating dir %s', folder)
os.mkdir(folder, 0755) os.makedirs(folder, 0755)
return folder return folder
def get_or_create_file(filename): def get_or_create_file(filename):

View File

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

View File

@ -18,6 +18,7 @@
# R0921 - Abstract class not referenced # R0921 - Abstract class not referenced
# W0141 - Used builtin function '%s' # W0141 - Used builtin function '%s'
# W0142 - Used * or ** magic # W0142 - Used * or ** magic
# W0511 - TODO, FIXME and XXX in the code
# W0613 - Unused argument %r # 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 mock >= 0.7
nose nose
tox tox
unittest2
yappi yappi

View File

@ -1,7 +1,7 @@
import mock import mock
import random import random
from mopidy.models import Playlist, Track from mopidy.models import CpTrack, Playlist, Track
from mopidy.gstreamer import GStreamer from mopidy.gstreamer import GStreamer
from tests.backends.base import populate_playlist 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.' 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): def test_add(self):
for track in self.tracks: for track in self.tracks:
cp_track = self.controller.add(track) 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.state, self.playback.STOPPED)
self.assertEqual(self.playback.current_track, None) 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 @populate_playlist
def test_move_single(self): def test_move_single(self):
self.controller.move(0, 0, 2) self.controller.move(0, 0, 2)
@ -241,6 +260,18 @@ class CurrentPlaylistControllerTest(object):
self.assertEqual(self.tracks[0], shuffled_tracks[0]) self.assertEqual(self.tracks[0], shuffled_tracks[0])
self.assertEqual(set(self.tracks), set(shuffled_tracks)) 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): def test_version_does_not_change_when_appending_nothing(self):
version = self.controller.version version = self.controller.version
self.controller.append([]) self.controller.append([])

View File

@ -271,11 +271,17 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase):
self.sendRequest(u'playlistinfo') self.sendRequest(u'playlistinfo')
self.assertInResponse(u'Title: a') self.assertInResponse(u'Title: a')
self.assertInResponse(u'Pos: 0')
self.assertInResponse(u'Title: b') self.assertInResponse(u'Title: b')
self.assertInResponse(u'Pos: 1')
self.assertInResponse(u'Title: c') self.assertInResponse(u'Title: c')
self.assertInResponse(u'Pos: 2')
self.assertInResponse(u'Title: d') self.assertInResponse(u'Title: d')
self.assertInResponse(u'Pos: 3')
self.assertInResponse(u'Title: e') self.assertInResponse(u'Title: e')
self.assertInResponse(u'Pos: 4')
self.assertInResponse(u'Title: f') self.assertInResponse(u'Title: f')
self.assertInResponse(u'Pos: 5')
self.assertInResponse(u'OK') self.assertInResponse(u'OK')
def test_playlistinfo_with_songpos(self): def test_playlistinfo_with_songpos(self):
@ -286,11 +292,17 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase):
self.sendRequest(u'playlistinfo "4"') self.sendRequest(u'playlistinfo "4"')
self.assertNotInResponse(u'Title: a') self.assertNotInResponse(u'Title: a')
self.assertNotInResponse(u'Pos: 0')
self.assertNotInResponse(u'Title: b') self.assertNotInResponse(u'Title: b')
self.assertNotInResponse(u'Pos: 1')
self.assertNotInResponse(u'Title: c') self.assertNotInResponse(u'Title: c')
self.assertNotInResponse(u'Pos: 2')
self.assertNotInResponse(u'Title: d') self.assertNotInResponse(u'Title: d')
self.assertNotInResponse(u'Pos: 3')
self.assertInResponse(u'Title: e') self.assertInResponse(u'Title: e')
self.assertInResponse(u'Pos: 4')
self.assertNotInResponse(u'Title: f') self.assertNotInResponse(u'Title: f')
self.assertNotInResponse(u'Pos: 5')
self.assertInResponse(u'OK') self.assertInResponse(u'OK')
def test_playlistinfo_with_negative_songpos_same_as_playlistinfo(self): def test_playlistinfo_with_negative_songpos_same_as_playlistinfo(self):
@ -306,11 +318,17 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase):
self.sendRequest(u'playlistinfo "2:"') self.sendRequest(u'playlistinfo "2:"')
self.assertNotInResponse(u'Title: a') self.assertNotInResponse(u'Title: a')
self.assertNotInResponse(u'Pos: 0')
self.assertNotInResponse(u'Title: b') self.assertNotInResponse(u'Title: b')
self.assertNotInResponse(u'Pos: 1')
self.assertInResponse(u'Title: c') self.assertInResponse(u'Title: c')
self.assertInResponse(u'Pos: 2')
self.assertInResponse(u'Title: d') self.assertInResponse(u'Title: d')
self.assertInResponse(u'Pos: 3')
self.assertInResponse(u'Title: e') self.assertInResponse(u'Title: e')
self.assertInResponse(u'Pos: 4')
self.assertInResponse(u'Title: f') self.assertInResponse(u'Title: f')
self.assertInResponse(u'Pos: 5')
self.assertInResponse(u'OK') self.assertInResponse(u'OK')
def test_playlistinfo_with_closed_range(self): def test_playlistinfo_with_closed_range(self):

View File

@ -146,3 +146,19 @@ class IssueGH113RegressionTest(protocol.BaseTestCase):
self.sendRequest( self.sendRequest(
r'listplaylistinfo "all lart spotify:track:\\w\\{22\\} pastes"') r'listplaylistinfo "all lart spotify:track:\\w\\{22\\} pastes"')
self.assertInResponse('OK') 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 import settings
from mopidy.utils.path import mtime, uri_to_path from mopidy.utils.path import mtime, uri_to_path
from mopidy.frontends.mpd import translator, protocol 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 from tests import unittest
@ -45,17 +45,17 @@ class TrackMpdFormatTest(unittest.TestCase):
self.assert_(('Pos', 1) not in result) self.assert_(('Pos', 1) not in result)
def test_track_to_mpd_format_with_cpid(self): 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) self.assert_(('Id', 1) not in result)
def test_track_to_mpd_format_with_position_and_cpid(self): 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_(('Pos', 1) in result)
self.assert_(('Id', 2) in result) self.assert_(('Id', 2) in result)
def test_track_to_mpd_format_for_nonempty_track(self): def test_track_to_mpd_format_for_nonempty_track(self):
result = translator.track_to_mpd_format( 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_(('file', 'a uri') in result)
self.assert_(('Time', 137) in result) self.assert_(('Time', 137) in result)
self.assert_(('Artist', 'an artist') in result) self.assert_(('Artist', 'an artist') in result)

View File

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

View File

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

View File

@ -1,12 +1,19 @@
import sys
import mock import mock
from mopidy import settings from mopidy import OptionalDependencyError, settings
from mopidy.backends.dummy import DummyBackend 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 from tests import unittest
@unittest.skipUnless(sys.platform.startswith('linux'), 'requires Linux')
class RootInterfaceTest(unittest.TestCase): class RootInterfaceTest(unittest.TestCase):
def setUp(self): def setUp(self):
objects.exit_process = mock.Mock() 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 from tests import unittest, path_to_data_dir
# TODO BaseOutputTest?
@unittest.skipIf(sys.platform == 'win32', @unittest.skipIf(sys.platform == 'win32',
'Our Windows build server does not support GStreamer yet') 'Our Windows build server does not support GStreamer yet')

View File

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

View File

@ -4,7 +4,7 @@ from tests import unittest
from tests.mixers.base_test import BaseMixerTest from tests.mixers.base_test import BaseMixerTest
class DenonMixerTest(BaseMixerTest, unittest.TestCase): class DummyMixerTest(BaseMixerTest, unittest.TestCase):
mixer_class = DummyMixer mixer_class = DummyMixer
def test_set_volume_is_capped(self): def test_set_volume_is_capped(self):
@ -16,3 +16,8 @@ class DenonMixerTest(BaseMixerTest, unittest.TestCase):
self.mixer.amplification_factor = 0.5 self.mixer.amplification_factor = 0.5
self.mixer._volume = 50 self.mixer._volume = 50
self.assertEquals(self.mixer.volume, 100) 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.assert_(os.path.isdir(folder))
self.assertEqual(created, 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): def test_creating_existing_folder(self):
created = get_or_create_folder(self.parent) created = get_or_create_folder(self.parent)
self.assert_(os.path.exists(self.parent)) self.assert_(os.path.exists(self.parent))
self.assert_(os.path.isdir(self.parent)) self.assert_(os.path.isdir(self.parent))
self.assertEqual(created, 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): class PathToFileURITest(unittest.TestCase):
def test_simple_path(self): 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.0') < SV('0.4.1'))
self.assert_(SV('0.4.1') < SV('0.5.0')) self.assert_(SV('0.4.1') < SV('0.5.0'))
self.assert_(SV('0.5.0') < SV('0.6.0')) self.assert_(SV('0.5.0') < SV('0.6.0'))
self.assert_(SV('0.6.0') < SV(get_plain_version())) self.assert_(SV('0.6.0') < SV('0.6.1'))
self.assert_(SV(get_plain_version()) < SV('0.7.0')) 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): def test_get_platform_contains_platform(self):
self.assert_(platform.platform() in get_platform()) self.assert_(platform.platform() in get_platform())