Merge branch 'feature/simplify-outputs' into feature/switch-to-gst-mixers

Conflicts:
	mopidy/gstreamer.py
	mopidy/outputs/local.py
This commit is contained in:
Thomas Adamcik 2012-08-26 13:16:54 +02:00
commit 915130e352
86 changed files with 1030 additions and 1076 deletions

View File

@ -9,11 +9,9 @@ in Spotify's vast archive, manage playlists, and play music, you can use most
platforms, including Windows, Mac OS X, Linux, Android and iOS.
To install Mopidy, check out
`the installation docs <http://www.mopidy.com/docs/master/installation/>`_.
`the installation docs <http://docs.mopidy.com/en/latest/installation/>`_.
- `Documentation for the latest release <http://www.mopidy.com/docs/master/>`_
- `Documentation for the development version
<http://www.mopidy.com/docs/develop/>`_
- `Documentation <http://docs.mopidy.com/>`_
- `Source code <http://github.com/mopidy/mopidy>`_
- `Issue tracker <http://github.com/mopidy/mopidy/issues>`_
- IRC: ``#mopidy`` at `irc.freenode.net <http://freenode.net/>`_

View File

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

View File

@ -1,236 +0,0 @@
/**
* Sphinx stylesheet -- default theme
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
@import url("basic.css");
/* -- page layout ----------------------------------------------------------- */
body {
font-family: Arial, sans-serif;
font-size: 100%;
background-color: #111111;
color: #555555;
margin: 0;
padding: 0;
}
div.documentwrapper {
float: left;
width: 100%;
}
div.bodywrapper {
margin: 0 0 0 300px;
}
hr{
border: 1px solid #B1B4B6;
}
div.document {
background-color: #eeeeee;
}
div.body {
background-color: #ffffff;
color: #3E4349;
padding: 1em 30px 30px 30px;
font-size: 0.9em;
}
div.footer {
color: #555;
width: 100%;
padding: 13px 0;
text-align: center;
font-size: 75%;
}
div.footer a {
color: #444444;
}
div.related {
background-color: #6BA81E;
line-height: 36px;
color: #ffffff;
text-shadow: 0px 1px 0 #444444;
font-size: 1.1em;
}
div.related a {
color: #E2F3CC;
}
div.related .right {
font-size: 0.9em;
}
div.sphinxsidebar {
font-size: 0.9em;
line-height: 1.5em;
width: 300px
}
div.sphinxsidebarwrapper{
padding: 20px 0;
}
div.sphinxsidebar h3,
div.sphinxsidebar h4 {
font-family: Arial, sans-serif;
color: #222222;
font-size: 1.2em;
font-weight: bold;
margin: 0;
padding: 5px 10px;
text-shadow: 1px 1px 0 white
}
div.sphinxsidebar h3 a {
color: #444444;
}
div.sphinxsidebar p {
color: #888888;
padding: 5px 20px;
margin: 0.5em 0px;
}
div.sphinxsidebar p.topless {
}
div.sphinxsidebar ul {
margin: 10px 10px 10px 20px;
padding: 0;
color: #000000;
}
div.sphinxsidebar a {
color: #444444;
}
div.sphinxsidebar a:hover {
color: #E32E00;
}
div.sphinxsidebar input {
border: 1px solid #cccccc;
font-family: sans-serif;
font-size: 1.1em;
padding: 0.15em 0.3em;
}
div.sphinxsidebar input[type=text]{
margin-left: 20px;
}
/* -- body styles ----------------------------------------------------------- */
a {
color: #005B81;
text-decoration: none;
}
a:hover {
color: #E32E00;
}
div.body h1,
div.body h2,
div.body h3,
div.body h4,
div.body h5,
div.body h6 {
font-family: Arial, sans-serif;
font-weight: normal;
color: #212224;
margin: 30px 0px 10px 0px;
padding: 5px 0 5px 0px;
text-shadow: 0px 1px 0 white;
border-bottom: 1px solid #C8D5E3;
}
div.body h1 { margin-top: 0; font-size: 200%; }
div.body h2 { font-size: 150%; }
div.body h3 { font-size: 120%; }
div.body h4 { font-size: 110%; }
div.body h5 { font-size: 100%; }
div.body h6 { font-size: 100%; }
a.headerlink {
color: #c60f0f;
font-size: 0.8em;
padding: 0 4px 0 4px;
text-decoration: none;
}
a.headerlink:hover {
background-color: #c60f0f;
color: white;
}
div.body p, div.body dd, div.body li {
line-height: 1.8em;
}
div.admonition p.admonition-title + p {
display: inline;
}
div.highlight{
background-color: white;
}
div.note {
background-color: #eeeeee;
border: 1px solid #cccccc;
}
div.seealso {
background-color: #ffffcc;
border: 1px solid #ffff66;
}
div.topic {
background-color: #fafafa;
border-width: 0;
}
div.warning {
background-color: #ffe4e4;
border: 1px solid #ff6666;
}
p.admonition-title {
display: inline;
}
p.admonition-title:after {
content: ":";
}
pre {
padding: 10px;
background-color: #eeeeee;
color: #222222;
line-height: 1.5em;
font-size: 1.1em;
margin: 1.5em 0 1.5em 0;
-webkit-box-shadow: 0px 0px 4px #d8d8d8;
-moz-box-shadow: 0px 0px 4px #d8d8d8;
box-shadow: 0px 0px 4px #d8d8d8;
}
tt {
color: #222222;
padding: 1px 2px;
font-size: 1.2em;
font-family: monospace;
}
#table-of-contents ul {
padding-left: 2em;
}

View File

@ -1,54 +0,0 @@
.c { color: #999988; font-style: italic } /* Comment */
.k { font-weight: bold } /* Keyword */
.o { font-weight: bold } /* Operator */
.cm { color: #999988; font-style: italic } /* Comment.Multiline */
.cp { color: #999999; font-weight: bold } /* Comment.preproc */
.c1 { color: #999988; font-style: italic } /* Comment.Single */
.gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.ge { font-style: italic } /* Generic.Emph */
.gr { color: #aa0000 } /* Generic.Error */
.gh { color: #999999 } /* Generic.Heading */
.gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.go { color: #111 } /* Generic.Output */
.gp { color: #555555 } /* Generic.Prompt */
.gs { font-weight: bold } /* Generic.Strong */
.gu { color: #aaaaaa } /* Generic.Subheading */
.gt { color: #aa0000 } /* Generic.Traceback */
.kc { font-weight: bold } /* Keyword.Constant */
.kd { font-weight: bold } /* Keyword.Declaration */
.kp { font-weight: bold } /* Keyword.Pseudo */
.kr { font-weight: bold } /* Keyword.Reserved */
.kt { color: #445588; font-weight: bold } /* Keyword.Type */
.m { color: #009999 } /* Literal.Number */
.s { color: #bb8844 } /* Literal.String */
.na { color: #008080 } /* Name.Attribute */
.nb { color: #999999 } /* Name.Builtin */
.nc { color: #445588; font-weight: bold } /* Name.Class */
.no { color: #ff99ff } /* Name.Constant */
.ni { color: #800080 } /* Name.Entity */
.ne { color: #990000; font-weight: bold } /* Name.Exception */
.nf { color: #990000; font-weight: bold } /* Name.Function */
.nn { color: #555555 } /* Name.Namespace */
.nt { color: #000080 } /* Name.Tag */
.nv { color: purple } /* Name.Variable */
.ow { font-weight: bold } /* Operator.Word */
.mf { color: #009999 } /* Literal.Number.Float */
.mh { color: #009999 } /* Literal.Number.Hex */
.mi { color: #009999 } /* Literal.Number.Integer */
.mo { color: #009999 } /* Literal.Number.Oct */
.sb { color: #bb8844 } /* Literal.String.Backtick */
.sc { color: #bb8844 } /* Literal.String.Char */
.sd { color: #bb8844 } /* Literal.String.Doc */
.s2 { color: #bb8844 } /* Literal.String.Double */
.se { color: #bb8844 } /* Literal.String.Escape */
.sh { color: #bb8844 } /* Literal.String.Heredoc */
.si { color: #bb8844 } /* Literal.String.Interpol */
.sx { color: #bb8844 } /* Literal.String.Other */
.sr { color: #808000 } /* Literal.String.Regex */
.s1 { color: #bb8844 } /* Literal.String.Single */
.ss { color: #bb8844 } /* Literal.String.Symbol */
.bp { color: #999999 } /* Name.Builtin.Pseudo */
.vc { color: #ff99ff } /* Name.Variable.Class */
.vg { color: #ff99ff } /* Name.Variable.Global */
.vi { color: #ff99ff } /* Name.Variable.Instance */
.il { color: #009999 } /* Literal.Number.Integer.Long */

View File

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

View File

@ -7,7 +7,7 @@ The following requirements applies to any frontend implementation:
- A frontend MAY do mostly whatever it wants to, including creating threads,
opening TCP ports and exposing Mopidy for a group of clients.
- A frontend MUST implement at least one `Pykka
<http://jodal.github.com/pykka/>`_ actor, called the "main actor" from here
<http://pykka.readthedocs.org/>`_ actor, called the "main actor" from here
on.
- It MAY use additional actors to implement whatever it does, and using actors
in frontend implementations is encouraged.

View File

@ -4,14 +4,146 @@ Changes
This change log is used to track all major changes to Mopidy.
v0.8 (in development)
=====================
v0.6.0 (in development)
=======================
**Changes**
- Added tools/debug-proxy.py to tee client requests to two backends and diff
responses. Intended as a developer tool for checking for MPD protocol changes
and various client support. Requires gevent, which currently is not a
dependency of Mopidy.
- Fixed bug when the MPD command `playlistinfo` is used with a track position.
Track position and CPID was intermixed, so it would cause a crash if a CPID
matching the track position didn't exist. (Fixes: :issue:`162`)
- Removed most traces of multiple outputs support. Having this feature
currently seems to be more trouble than what it is worth.
:attr:`mopidy.settings.OUTPUTS` setting is no longer supported, and has been
replaced with :attr:`mopidy.settings.OUTPUT` which is a GStreamer
bin descriped in the same format as gst-launch expects. Default value is
``autoaudiosink``.
v0.7.3 (2012-08-11)
===================
A small maintenance release to fix a crash affecting a few users, and a couple
of small adjustments to the Spotify backend.
**Changes**
- Fixed crash when logging :exc:`IOError` exceptions on systems using languages
with non-ASCII characters, like French.
- Move the default location of the Spotify cache from `~/.cache/mopidy` to
`~/.cache/mopidy/spotify`. You can change this by setting
:attr:`mopidy.settings.SPOTIFY_CACHE_PATH`.
- Reduce time required to update the Spotify cache on startup. One one
system/Spotify account, the time from clean cache to ready for use was
reduced from 35s to 12s.
v0.7.2 (2012-05-07)
===================
This is a maintenance release to make Mopidy 0.7 build on systems without all
of Mopidy's runtime dependencies, like Launchpad PPAs.
**Changes**
- Change from version tuple at :attr:`mopidy.VERSION` to :pep:`386` compliant
version string at :attr:`mopidy.__version__` to conform to :pep:`396`.
v0.7.1 (2012-04-22)
===================
This is a maintenance release to make Mopidy 0.7 work with pyspotify >= 1.7.
**Changes**
- Don't override pyspotify's ``notify_main_thread`` callback. The default
implementation is sensible, while our override did nothing.
v0.7.0 (2012-02-25)
===================
Not a big release with regard to features, but this release got some
performance improvements over v0.6, especially for slower Atom systems. It also
fixes a couple of other bugs, including one which made Mopidy crash when using
GStreamer from the prereleases of Ubuntu 12.04.
**Changes**
- The MPD command ``playlistinfo`` is now faster, thanks to John Bäckstrand.
- Added the method
:meth:`mopidy.backends.base.CurrentPlaylistController.length()`,
:meth:`mopidy.backends.base.CurrentPlaylistController.index()`, and
:meth:`mopidy.backends.base.CurrentPlaylistController.slice()` to reduce the
need for copying the entire current playlist from one thread to another.
Thanks to John Bäckstrand for pinpointing the issue.
- Fix crash on creation of config and cache directories if intermediate
directories does not exist. This was especially the case on OS X, where
``~/.config`` doesn't exist for most users.
- Fix ``gst.LinkError`` which appeared when using newer versions of GStreamer,
e.g. on Ubuntu 12.04 Alpha. (Fixes: :issue:`144`)
- Fix crash on mismatching quotation in ``list`` MPD queries. (Fixes:
:issue:`137`)
- Volume is now reported to be the same as the volume was set to, also when
internal rounding have been done due to
:attr:`mopidy.settings.MIXER_MAX_VOLUME` has been set to cap the volume. This
should make it possible to manage capped volume from clients that only
increase volume with one step at a time, like ncmpcpp does.
v0.6.1 (2011-12-28)
===================
This is a maintenance release to make Mopidy 0.6 work with pyspotify >= 1.5,
which Mopidy's develop branch have supported for a long time. This should also
make the Debian packages work out of the box again.
**Important changes**
- pyspotify 1.5 or greater is required.
**Changes**
- Spotify playlist folder boundaries are now properly detected. In other words,
if you use playlist folders, you will no longer get lots of log messages
about bad playlists.
v0.6.0 (2011-10-09)
===================
The development of Mopidy have been quite slow for the last couple of months,
but we do have some goodies to release which have been idling in the
develop branch since the warmer days of the summer. This release brings support
for the MPD ``idle`` command, which makes it possible for a client wait for
updates from the server instead of polling every second. Also, we've added
support for the MPRIS standard, so that Mopidy can be controlled over D-Bus
from e.g. the Ubuntu Sound Menu.
Please note that 0.6.0 requires some updated dependencies, as listed under
*Important changes* below.
**Important changes**
- Pykka 0.12.3 or greater is required.
- pyspotify 1.4 or greater is required.
- All config, data, and cache locations are now based on the XDG spec.
- This means that your settings file will need to be moved from
@ -29,7 +161,7 @@ v0.6.0 (in development)
- A new frontend :mod:`mopidy.frontends.mpris` have been added. It exposes
Mopidy through the `MPRIS interface <http://www.mpris.org/>`_ over D-Bus. In
practice, this makes it possible to control Mopidy thorugh the `Ubuntu Sound
practice, this makes it possible to control Mopidy through the `Ubuntu Sound
Menu <https://wiki.ubuntu.com/SoundMenu>`_.
**Changes**
@ -50,6 +182,19 @@ v0.6.0 (in development)
- Unescape all incoming MPD requests. (Fixes: :issue:`113`)
- Increase the maximum number of results returned by Spotify searches from 32
to 100.
- Send Spotify search queries to pyspotify as unicode objects, as required by
pyspotify 1.4. (Fixes: :issue:`129`)
- Add setting :attr:`mopidy.settings.MPD_SERVER_MAX_CONNECTIONS`. (Fixes:
:issue:`134`)
- Remove `destroy()` methods from backend controller and provider APIs, as it
was not in use and actually not called by any code. Will reintroduce when
needed.
v0.5.0 (2011-06-15)
===================
@ -153,7 +298,7 @@ This is a bug fix release fixing audio problems on older GStreamer and some
minor bugs.
**Bugfixes**
**Bug fixes**
- Fix broken audio on at least GStreamer 0.10.30, which affects Ubuntu 10.10.
The GStreamer `appsrc` bin wasn't being linked due to lack of default caps.
@ -198,7 +343,7 @@ loading from Mopidy 0.3.0 is still present.
**Important changes**
- Mopidy now depends on `Pykka <http://jodal.github.com/pykka>`_ >=0.12. If you
- Mopidy now depends on `Pykka <http://pykka.readthedocs.org/>`_ >=0.12. If you
install from APT, Pykka will automatically be installed. If you are not
installing from APT, you may install Pykka from PyPI::
@ -280,7 +425,7 @@ v0.3.1 (2011-01-22)
A couple of fixes to the 0.3.0 release is needed to get a smooth installation.
**Bugfixes**
**Bug fixes**
- The Spotify application key was missing from the Python package.
@ -449,7 +594,7 @@ v0.2.1 (2011-01-07)
This is a maintenance release without any new features.
**Bugfixes**
**Bug fixes**
- Fix crash in :mod:`mopidy.frontends.lastfm` which occurred at playback if
either :mod:`pylast` was not installed or the Last.fm scrobbling was not
@ -779,7 +924,7 @@ As always, report problems at our IRC channel or our issue tracker. Thanks!
- Merged the ``gstreamer`` branch from Thomas Adamcik:
- More than 200 new tests, and thus several bugfixes to existing code.
- More than 200 new tests, and thus several bug fixes to existing code.
- Several new generic features, like shuffle, consume, and playlist repeat.
(Fixes: :issue:`3`)
- **[Work in Progress]** A new backend for playing music from a local music

View File

@ -11,7 +11,50 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
import os
import re
import sys
class Mock(object):
def __init__(self, *args, **kwargs):
pass
def __call__(self, *args, **kwargs):
return Mock()
@classmethod
def __getattr__(self, name):
if name in ('__file__', '__path__'):
return '/dev/null'
elif name[0] == name[0].upper():
return type(name, (), {})
else:
return Mock()
MOCK_MODULES = [
'alsaaudio',
'dbus',
'dbus.mainloop',
'dbus.mainloop.glib',
'dbus.service',
'glib',
'gobject',
'gst',
'pygst',
'pykka',
'pykka.actor',
'pykka.future',
'pykka.registry',
'pylast',
'serial',
]
for mod_name in MOCK_MODULES:
sys.modules[mod_name] = Mock()
def get_version():
init_py = open('../mopidy/__init__.py').read()
metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", init_py))
return metadata['version']
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
@ -19,14 +62,15 @@ import sys, os
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/../'))
import mopidy
# When RTD builds the project, it sets the READTHEDOCS environment variable to
# the string True.
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
# -- General configuration -----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc',
'sphinx.ext.graphviz', 'sphinx.ext.inheritance_diagram',
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.graphviz',
'sphinx.ext.extlinks', 'sphinx.ext.viewcode']
# Add any paths that contain templates here, relative to this directory.
@ -43,14 +87,14 @@ master_doc = 'index'
# General information about the project.
project = u'Mopidy'
copyright = u'2010-2011, Stein Magnus Jodal and contributors'
copyright = u'2010-2012, Stein Magnus Jodal and contributors'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The full version, including alpha/beta/rc tags.
release = mopidy.get_version()
release = get_version()
# The short X.Y version.
version = '.'.join(release.split('.')[:2])
@ -97,7 +141,7 @@ modindex_common_prefix = ['mopidy.']
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
html_theme = 'nature'
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
@ -116,7 +160,8 @@ html_theme_path = ['_themes']
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
html_logo = '_static/mopidy.png'
if on_rtd:
html_logo = '_static/mopidy.png'
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
@ -130,7 +175,7 @@ html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
html_last_updated_fmt = '%b %d, %Y'
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
@ -202,4 +247,4 @@ latex_documents = [
needs_sphinx = '1.0'
extlinks = {'issue': ('http://github.com/mopidy/mopidy/issues/%s', 'GH-')}
extlinks = {'issue': ('https://github.com/mopidy/mopidy/issues/%s', 'GH-')}

View File

@ -74,7 +74,7 @@ Running tests
To run tests, you need a couple of dependencies. They can be installed through
Debian/Ubuntu package management::
sudo aptitude install python-coverage python-mock python-nose
sudo apt-get install python-coverage python-mock python-nose
Or, they can be installed using ``pip``::
@ -126,7 +126,7 @@ from the documentation files, you need some additional dependencies.
You can install them through Debian/Ubuntu package management::
sudo aptitude install python-sphinx python-pygraphviz graphviz
sudo apt-get install python-sphinx python-pygraphviz graphviz
Then, to generate docs::
@ -134,18 +134,8 @@ Then, to generate docs::
make # For help on available targets
make html # To generate HTML docs
.. note::
The documentation at http://www.mopidy.com/ is automatically updated when a
documentation update is pushed to ``mopidy/mopidy`` at GitHub.
Documentation generated from the ``master`` branch is published at
http://www.mopidy.com/docs/master/, and will always be valid for the latest
release.
Documentation generated from the ``develop`` branch is published at
http://www.mopidy.com/docs/develop/, and will always be valid for the
latest development snapshot.
The documentation at http://docs.mopidy.com/ is automatically updated when a
documentation update is pushed to ``mopidy/mopidy`` at GitHub.
Creating releases

View File

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

View File

@ -2,19 +2,21 @@
GStreamer installation
**********************
To use the Mopidy, you first need to install GStreamer and its Python bindings.
To use the Mopidy, you first need to install GStreamer and the GStreamer Python
bindings.
Installing GStreamer
====================
On Linux
--------
Installing GStreamer on Linux
=============================
GStreamer is packaged for most popular Linux distributions. Search for
GStreamer in your package manager, and make sure to install the Python
bindings, and the "good" and "ugly" plugin sets.
Debian/Ubuntu
-------------
If you use Debian/Ubuntu you can install GStreamer like this::
sudo apt-get install python-gst0.10 gstreamer0.10-plugins-good \
@ -24,30 +26,67 @@ If you install Mopidy from our APT archive, you don't need to install GStreamer
yourself. The Mopidy Debian package will handle it for you.
On OS X from Homebrew
---------------------
Arch Linux
----------
If you use Arch Linux, install the following packages from the official
repository::
sudo pacman -S gstreamer0.10-python gstreamer0.10-good-plugins \
gstreamer0.10-ugly-plugins
Installing GStreamer on OS X
============================
.. note::
We have created GStreamer formulas for Homebrew to make the GStreamer
installation easy for you, but not all our formulas have been merged into
Homebrew's master branch yet. You should either fetch the formula files
from `Homebrew's issue #1612
<http://github.com/mxcl/homebrew/issues/issue/1612>`_ yourself, or fall
back to using MacPorts.
We have been working with `Homebrew <https://github.com/mxcl/homebrew>`_ to
make all the GStreamer packages easily installable on OS X using Homebrew.
We've gotten most of our packages included, but the Homebrew guys aren't
very happy to include Python specific packages into Homebrew, even though
they are not installable by pip. If you're interested, see the discussion
in `Homebrew's issue #1612
<https://github.com/mxcl/homebrew/issues/issue/1612>`_ for details.
To install GStreamer on OS X using Homebrew::
The following is currently the shortest path to installing GStreamer with
Python bindings on OS X using Homebrew.
brew install gst-python gst-plugins-good gst-plugins-ugly
#. Install `Homebrew <https://github.com/mxcl/homebrew>`_.
#. Download our Homebrew formulas for ``pycairo``, ``pygobject``, ``pygtk``,
and ``gst-python``::
On OS X from MacPorts
---------------------
curl -o $(brew --prefix)/Library/Formula/pycairo.rb \
https://raw.github.com/jodal/homebrew/gst-python/Library/Formula/pycairo.rb
curl -o $(brew --prefix)/Library/Formula/pygobject.rb \
https://raw.github.com/jodal/homebrew/gst-python/Library/Formula/pygobject.rb
curl -o $(brew --prefix)/Library/Formula/pygtk.rb \
https://raw.github.com/jodal/homebrew/gst-python/Library/Formula/pygtk.rb
curl -o $(brew --prefix)/Library/Formula/gst-python.rb \
https://raw.github.com/jodal/homebrew/gst-python/Library/Formula/gst-python.rb
To install GStreamer on OS X using MacPorts::
#. Install the required packages::
sudo port install py26-gst-python gstreamer-plugins-good \
gstreamer-plugins-ugly
brew install gst-python gst-plugins-good gst-plugins-ugly
#. Make sure to include Homebrew's Python ``site-packages`` directory in your
``PYTHONPATH``. If you don't include this, Mopidy will not find GStreamer
and crash.
You can either amend your ``PYTHONPATH`` permanently, by adding the
following statement to your shell's init file, e.g. ``~/.bashrc``::
export PYTHONPATH=$(brew --prefix)/lib/python2.6/site-packages:$PYTHONPATH
Or, you can prefix the Mopidy command every time you run it::
PYTHONPATH=$(brew --prefix)/lib/python2.6/site-packages mopidy
Note that you need to replace ``python2.6`` with ``python2.7`` if that's
the Python version you are using. To find your Python version, run::
python --version
Testing the installation
@ -73,12 +112,9 @@ Using a custom audio sink
=========================
If you for some reason want to use some other GStreamer audio sink than
``autoaudiosink``, you can add ``mopidy.outputs.custom.CustomOutput`` to the
:attr:`mopidy.settings.OUTPUTS` setting, and set the
:attr:`mopidy.settings.CUSTOM_OUTPUT` setting to a partial GStreamer pipeline
description describing the GStreamer sink you want to use.
``autoaudiosink``, you can set :attr:`mopidy.settings.OUTPUT` to a partial
GStreamer pipeline description describing the GStreamer sink you want to use.
Example of ``settings.py`` for OSS4::
OUTPUTS = (u'mopidy.outputs.custom.CustomOutput',)
CUSTOM_OUTPUT = u'oss4sink'
OUTPUT = u'oss4sink'

View File

@ -18,33 +18,36 @@ Requirements
gstreamer
libspotify
If you install Mopidy from the APT archive, as described below, you can skip
the dependency installation part.
If you install Mopidy from the APT archive, as described below, APT will take
care of all the dependencies for you. Otherwise, make sure you got the required
dependencies installed.
Otherwise, make sure you got the required dependencies installed.
- Hard dependencies:
- Python >= 2.6, < 3
- Python >= 2.6, < 3
- `Pykka <http://jodal.github.com/pykka/>`_ >= 0.12.3
- Pykka >= 0.12.3::
- GStreamer >= 0.10, with Python bindings. See :doc:`gstreamer`.
sudo pip install -U pykka
- Mixer dependencies: The default mixer does not require any additional
dependencies. If you use another mixer, see the mixer's docs for any
additional requirements.
- Dependencies for at least one Mopidy backend:
- The default backend, :mod:`mopidy.backends.spotify`, requires libspotify
and pyspotify. See :doc:`libspotify`.
- The local backend, :mod:`mopidy.backends.local`, requires no additional
dependencies.
- GStreamer 0.10.x, with Python bindings. See :doc:`gstreamer`.
- Optional dependencies:
- To use the Last.FM scrobbler, see :mod:`mopidy.frontends.lastfm` for
additional requirements.
- For Spotify support, you need libspotify and pyspotify. See
:doc:`libspotify`.
- To scrobble your played tracks to Last.fm, you need pylast::
sudo pip install -U pylast
- To use MPRIS, e.g. for controlling Mopidy from the Ubuntu Sound Menu, you
need some additional requirements::
sudo apt-get install python-dbus python-indicate
- Some custom mixers (but not the default one) require additional
dependencies. See the docs for each mixer.
Install latest stable release
@ -97,8 +100,8 @@ install Mopidy from PyPI using Pip.
#. Then, you need to install Pip::
sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian
sudo easy_install pip # On OS X
sudo apt-get install python-setuptools python-pip # On Ubuntu/Debian
sudo easy_install pip # On OS X
#. To install the currently latest stable release of Mopidy::
@ -109,8 +112,6 @@ install Mopidy from PyPI using Pip.
#. Next, you need to set a couple of :doc:`settings </settings>`, and then
you're ready to :doc:`run Mopidy </running>`.
If you for some reason can't use Pip, try ``easy_install`` instead.
Install development version
===========================
@ -131,8 +132,8 @@ Mopidy's ``develop`` branch.
#. Then, you need to install Pip::
sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian
sudo easy_install pip # On OS X
sudo apt-get install python-setuptools python-pip # On Ubuntu/Debian
sudo easy_install pip # On OS X
#. To install the latest snapshot of Mopidy, run::
@ -154,7 +155,7 @@ If you want to contribute to Mopidy, you should install Mopidy using Git.
#. Then install Git, if haven't already::
sudo aptitude install git-core # On Ubuntu/Debian
sudo apt-get install git-core # On Ubuntu/Debian
sudo brew install git # On OS X using Homebrew
#. Clone the official Mopidy repository, or your own fork of it::

View File

@ -12,12 +12,6 @@ install libspotify and `pyspotify <http://pyspotify.mopidy.com/>`_.
This backend requires a paid `Spotify premium account
<http://www.spotify.com/no/get-spotify/premium/>`_.
.. note::
This product uses SPOTIFY CORE but is not endorsed, certified or otherwise
approved in any way by Spotify. Spotify is the registered trade mark of the
Spotify Group.
Installing libspotify
=====================
@ -26,23 +20,20 @@ Installing libspotify
On Linux from APT archive
-------------------------
If you run a Debian based Linux distribution, like Ubuntu, see
http://apt.mopidy.com/ for how to the Mopidy APT archive as a software source
on your installation. Then, simply run::
sudo apt-get install libspotify8
When libspotify has been installed, continue with
:ref:`pyspotify_installation`.
If you install from APT, jump directly to :ref:`pyspotify_installation` below.
On Linux from source
--------------------
Download and install libspotify 0.0.8 for your OS and CPU architecture from
https://developer.spotify.com/en/libspotify/.
First, check pyspotify's changelog to see what's the latest version of
libspotify which is supported. The versions of libspotify and pyspotify are
tightly coupled.
For 64-bit Linux the process is as follows::
Download and install the appropriate version of libspotify for your OS and CPU
architecture from https://developer.spotify.com/en/libspotify/.
For libspotify 0.0.8 for 64-bit Linux the process is as follows::
wget http://developer.spotify.com/download/libspotify/libspotify-0.0.8-linux6-x86_64.tar.gz
tar zxfv libspotify-0.0.8-linux6-x86_64.tar.gz
@ -50,6 +41,9 @@ For 64-bit Linux the process is as follows::
sudo make install prefix=/usr/local
sudo ldconfig
Remember to adjust for the latest libspotify version supported by pyspotify,
your OS and your CPU architecture.
When libspotify has been installed, continue with
:ref:`pyspotify_installation`.
@ -66,7 +60,7 @@ libspotify::
To update your existing libspotify installation using Homebrew::
brew update
brew install `brew outdated`
brew upgrade
When libspotify has been installed, continue with
:ref:`pyspotify_installation`.
@ -84,29 +78,35 @@ by installing pyspotify.
On Linux from APT archive
-------------------------
Assuming that you've already set up http://apt.mopidy.com/ as a software
source, run::
If you run a Debian based Linux distribution, like Ubuntu, see
http://apt.mopidy.com/ for how to use the Mopidy APT archive as a software
source on your system. Then, simply run::
sudo apt-get install python-spotify
If you haven't already installed libspotify, this command will install both
libspotify and pyspotify for you.
This command will install both libspotify and pyspotify for you.
On Linux/OS X from source
On Linux from source
-------------------------
If you have have already installed libspotify, you can continue with installing
the libspotify Python bindings, called pyspotify.
On Linux, you need to get the Python development files installed. On
Debian/Ubuntu systems run::
sudo apt-get install python-dev
On OS X no additional dependencies are needed.
Then get, build, and install the latest releast of pyspotify using ``pip``::
sudo pip install -U pyspotify
Or using the older ``easy_install``::
sudo easy_install pyspotify
On OS X from source
-------------------
If you have already installed libspotify, you can get, build, and install the
latest releast of pyspotify using ``pip``::
sudo pip install -U pyspotify

View File

@ -8,7 +8,7 @@ contributed what, please refer to our git repository.
Source code license
===================
Copyright 2009-2011 Stein Magnus Jodal and contributors
Copyright 2009-2012 Stein Magnus Jodal and contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -26,7 +26,7 @@ limitations under the License.
Documentation license
=====================
Copyright 2010-2011 Stein Magnus Jodal and contributors
Copyright 2010-2012 Stein Magnus Jodal and contributors
This work is licensed under the Creative Commons Attribution-ShareAlike 3.0
Unported License. To view a copy of this license, visit

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,4 +10,11 @@ When Mopidy says ``MPD server running at [127.0.0.1]:6600`` it's ready to
accept connections by any MPD client. Check out our non-exhaustive
:doc:`/clients/mpd` list to find recommended clients.
To stop Mopidy, press ``CTRL+C``.
To stop Mopidy, press ``CTRL+C`` in the terminal where you started Mopidy.
Mopidy will also shut down properly if you send it the TERM signal, e.g. by
using ``kill``::
kill `ps ax | grep mopidy | grep -v grep | cut -d' ' -f1`
This can be useful e.g. if you create init script for managing Mopidy.

View File

@ -10,10 +10,10 @@ changes you may want to do, and a complete listing of available settings.
Changing settings
=================
Mopidy reads settings from the file ``~/.mopidy/settings.py``, where ``~``
means your *home directory*. If your username is ``alice`` and you are running
Linux, the settings file should probably be at
``/home/alice/.mopidy/settings.py``.
Mopidy reads settings from the file ``~/.config/mopidy/settings.py``, where
``~`` means your *home directory*. If your username is ``alice`` and you are
running Linux, the settings file should probably be at
``/home/alice/.config/mopidy/settings.py``.
You can either create the settings file yourself, or run the ``mopidy``
command, and it will create an empty settings file for you.
@ -22,7 +22,7 @@ When you have created the settings file, open it in a text editor, and add
settings you want to change. If you want to keep the default value for setting,
you should *not* redefine it in your own settings file.
A complete ``~/.mopidy/settings.py`` may look as simple as this::
A complete ``~/.config/mopidy/settings.py`` may look as simple as this::
MPD_SERVER_HOSTNAME = u'::'
SPOTIFY_USERNAME = u'alice'
@ -77,7 +77,7 @@ To make a ``tag_cache`` of your local music available for Mopidy:
mopidy --list-settings
#. Scan your music library. Currently the command outputs the ``tag_cache`` to
#. Scan your music library. The command outputs the ``tag_cache`` to
``stdout``, which means that you will need to redirect the output to a file
yourself::
@ -157,18 +157,17 @@ server simultaneously. To use the SHOUTcast output, do the following:
#. Install, configure and start the Icecast server. It can be found in the
``icecast2`` package in Debian/Ubuntu.
#. Add ``mopidy.outputs.shoutcast.ShoutcastOutput`` output to the
:attr:`mopidy.settings.OUTPUTS` setting.
#. Set :attr:`mopidy.settings.OUTPUT` to ``lame ! shout2send`` (an ogg-vorbis
encoder could be used instead of lame).
#. Check the default values for the following settings, and alter them to match
your Icecast setup if needed:
#. You might also need to change the shout2send default settings, run
``gst-inspect-0.10 shout2send`` to see the available settings. Most likely
you want to change ``ip``, ``username``, ``password`` and ``mount``. For
example, to set the password use: ``lame ! shout2send password="s3cret"``.
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_HOSTNAME`
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_PORT`
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_USERNAME`
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_PASSWORD`
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_MOUNT`
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_ENCODER`
Other advanced setups are also possible for outputs. Basically anything you can
get a gst-lauch command to output to can be plugged into
:attr:`mopidy.settings.OUTPUT``.
Available settings

View File

@ -1,25 +1,25 @@
import platform
import sys
if not (2, 6) <= sys.version_info < (3,):
sys.exit(u'Mopidy requires Python >= 2.6, < 3')
import glib
import os
import platform
from subprocess import PIPE, Popen
VERSION = (0, 6, 0)
import glib
DATA_PATH = os.path.join(glib.get_user_data_dir(), 'mopidy')
CACHE_PATH = os.path.join(glib.get_user_cache_dir(), 'mopidy')
SETTINGS_PATH = os.path.join(glib.get_user_config_dir(), 'mopidy')
__version__ = '0.7.3'
DATA_PATH = os.path.join(str(glib.get_user_data_dir()), 'mopidy')
CACHE_PATH = os.path.join(str(glib.get_user_cache_dir()), 'mopidy')
SETTINGS_PATH = os.path.join(str(glib.get_user_config_dir()), 'mopidy')
SETTINGS_FILE = os.path.join(SETTINGS_PATH, 'settings.py')
def get_version():
try:
return get_git_version()
except EnvironmentError:
return get_plain_version()
return __version__
def get_git_version():
process = Popen(['git', 'describe'], stdout=PIPE, stderr=PIPE)
@ -30,9 +30,6 @@ def get_git_version():
version = version[1:]
return version
def get_plain_version():
return '.'.join(map(str, VERSION))
def get_platform():
return platform.platform()

View File

@ -21,10 +21,6 @@ class CurrentPlaylistController(object):
self._cp_tracks = []
self._version = 0
def destroy(self):
"""Cleanup after component."""
pass
@property
def cp_tracks(self):
"""
@ -32,7 +28,7 @@ class CurrentPlaylistController(object):
Read-only.
"""
return [copy(ct) for ct in self._cp_tracks]
return [copy(cp_track) for cp_track in self._cp_tracks]
@property
def tracks(self):
@ -41,7 +37,14 @@ class CurrentPlaylistController(object):
Read-only.
"""
return [ct[1] for ct in self._cp_tracks]
return [cp_track.track for cp_track in self._cp_tracks]
@property
def length(self):
"""
Length of the current playlist.
"""
return len(self._cp_tracks)
@property
def version(self):
@ -120,9 +123,9 @@ class CurrentPlaylistController(object):
matches = self._cp_tracks
for (key, value) in criteria.iteritems():
if key == 'cpid':
matches = filter(lambda ct: ct[0] == value, matches)
matches = filter(lambda ct: ct.cpid == value, matches)
else:
matches = filter(lambda ct: getattr(ct[1], key) == value,
matches = filter(lambda ct: getattr(ct.track, key) == value,
matches)
if len(matches) == 1:
return matches[0]
@ -133,6 +136,19 @@ class CurrentPlaylistController(object):
else:
raise LookupError(u'"%s" match multiple tracks' % criteria_string)
def index(self, cp_track):
"""
Get index of the given (CPID integer, :class:`mopidy.models.Track`)
two-tuple in the current playlist.
Raises :exc:`ValueError` if not found.
:param cp_track: track to find the index of
:type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`)
:rtype: int
"""
return self._cp_tracks.index(cp_track)
def move(self, start, end, to_position):
"""
Move the tracks in the slice ``[start:end]`` to ``to_position``.
@ -172,7 +188,6 @@ class CurrentPlaylistController(object):
:param criteria: on or more criteria to match by
:type criteria: dict
:type track: :class:`mopidy.models.Track`
"""
cp_track = self.get(**criteria)
position = self._cp_tracks.index(cp_track)
@ -208,6 +223,19 @@ class CurrentPlaylistController(object):
self._cp_tracks = before + shuffled + after
self.version += 1
def slice(self, start, end):
"""
Returns a slice of the current playlist, limited by the given
start and end positions.
:param start: position of first track to include in slice
:type start: int
:param end: position after last track to include in slice
:type end: int
:rtype: two-tuple of (CPID integer, :class:`mopidy.models.Track`)
"""
return [copy(cp_track) for cp_track in self._cp_tracks[start:end]]
def _trigger_playlist_changed(self):
logger.debug(u'Triggering playlist changed event')
BackendListener.send('playlist_changed')

View File

@ -16,10 +16,6 @@ class LibraryController(object):
self.backend = backend
self.provider = provider
def destroy(self):
"""Cleanup after component."""
self.provider.destroy()
def find_exact(self, **query):
"""
Search the library for tracks where ``field`` is ``values``.
@ -89,14 +85,6 @@ class BaseLibraryProvider(object):
def __init__(self, backend):
self.backend = backend
def destroy(self):
"""
Cleanup after component.
*MAY be implemented by subclasses.*
"""
pass
def find_exact(self, **query):
"""
See :meth:`mopidy.backends.base.LibraryController.find_exact`.

View File

@ -2,8 +2,6 @@ import logging
import random
import time
from pykka.registry import ActorRegistry
from mopidy.listeners import BackendListener
logger = logging.getLogger('mopidy.backends.base')
@ -82,12 +80,6 @@ class PlaybackController(object):
self.play_time_accumulated = 0
self.play_time_started = None
def destroy(self):
"""
Cleanup after component.
"""
self.provider.destroy()
def _get_cpid(self, cp_track):
if cp_track is None:
return None
@ -559,14 +551,6 @@ class BasePlaybackProvider(object):
def __init__(self, backend):
self.backend = backend
def destroy(self):
"""
Cleanup after component.
*MAY be implemented by subclasses.*
"""
pass
def pause(self):
"""
Pause playback.

View File

@ -17,10 +17,6 @@ class StoredPlaylistsController(object):
self.backend = backend
self.provider = provider
def destroy(self):
"""Cleanup after component."""
self.provider.destroy()
@property
def playlists(self):
"""
@ -133,14 +129,6 @@ class BaseStoredPlaylistsProvider(object):
self.backend = backend
self._playlists = []
def destroy(self):
"""
Cleanup after component.
*MAY be implemented by subclass.*
"""
pass
@property
def playlists(self):
"""
@ -201,4 +189,3 @@ class BaseStoredPlaylistsProvider(object):
*MUST be implemented by subclass.*
"""
raise NotImplementedError

View File

@ -21,7 +21,7 @@ logger = logging.getLogger(u'mopidy.backends.local')
DEFAULT_PLAYLIST_PATH = os.path.join(DATA_PATH, 'playlists')
DEFAULT_TAG_CACHE_FILE = os.path.join(DATA_PATH, 'tag_cache')
DEFAULT_MUSIC_PATH = glib.get_user_special_dir(glib.USER_DIRECTORY_MUSIC)
DEFAULT_MUSIC_PATH = str(glib.get_user_special_dir(glib.USER_DIRECTORY_MUSIC))
if not DEFAULT_MUSIC_PATH or DEFAULT_MUSIC_PATH == os.path.expanduser(u'~'):
DEFAULT_MUSIC_PATH = os.path.expanduser(u'~/music')
@ -67,7 +67,8 @@ class LocalBackend(ThreadingActor, Backend):
def on_start(self):
gstreamer_refs = ActorRegistry.get_by_class(GStreamer)
assert len(gstreamer_refs) == 1, 'Expected exactly one running gstreamer.'
assert len(gstreamer_refs) == 1, \
'Expected exactly one running GStreamer.'
self.gstreamer = gstreamer_refs[0].proxy()

View File

@ -4,6 +4,7 @@ import os
logger = logging.getLogger('mopidy.backends.local.translator')
from mopidy.models import Track, Artist, Album
from mopidy.utils import locale_decode
from mopidy.utils.path import path_to_uri
def parse_m3u(file_path):
@ -33,8 +34,8 @@ def parse_m3u(file_path):
try:
with open(file_path) as m3u:
contents = m3u.readlines()
except IOError, e:
logger.error('Couldn\'t open m3u: %s', e)
except IOError as error:
logger.error('Couldn\'t open m3u: %s', locale_decode(error))
return uris
for line in contents:
@ -61,8 +62,8 @@ def parse_mpd_tag_cache(tag_cache, music_dir=''):
try:
with open(tag_cache) as library:
contents = library.read()
except IOError, e:
logger.error('Could not open tag cache: %s', e)
except IOError as error:
logger.error('Could not open tag cache: %s', locale_decode(error))
return tracks
current = {}

View File

@ -10,7 +10,6 @@ from mopidy.gstreamer import GStreamer
logger = logging.getLogger('mopidy.backends.spotify')
ENCODING = 'utf-8'
BITRATES = {96: 2, 160: 0, 320: 1}
class SpotifyBackend(ThreadingActor, Backend):
@ -32,8 +31,8 @@ class SpotifyBackend(ThreadingActor, Backend):
**Dependencies:**
- libspotify == 0.0.8 (libspotify8 package from apt.mopidy.com)
- pyspotify == 1.3 (python-spotify package from apt.mopidy.com)
- libspotify >= 10, < 11 (libspotify10 package from apt.mopidy.com)
- pyspotify >= 1.5 (python-spotify package from apt.mopidy.com)
**Settings:**
@ -78,12 +77,16 @@ class SpotifyBackend(ThreadingActor, Backend):
def on_start(self):
gstreamer_refs = ActorRegistry.get_by_class(GStreamer)
assert len(gstreamer_refs) == 1, 'Expected exactly one running gstreamer.'
assert len(gstreamer_refs) == 1, \
'Expected exactly one running GStreamer.'
self.gstreamer = gstreamer_refs[0].proxy()
logger.info(u'Mopidy uses SPOTIFY(R) CORE')
self.spotify = self._connect()
def on_stop(self):
self.spotify.logout()
def _connect(self):
from .session_manager import SpotifySessionManager

View File

@ -13,13 +13,15 @@ class SpotifyContainerManager(PyspotifyContainerManager):
def container_loaded(self, container, userdata):
"""Callback used by pyspotify"""
logger.debug(u'Callback called: playlist container loaded')
self.session_manager.refresh_stored_playlists()
playlist_container = self.session_manager.session.playlist_container()
for playlist in playlist_container:
self.session_manager.playlist_manager.watch(playlist)
logger.debug(u'Watching %d playlist(s) for changes',
len(playlist_container))
count = 0
for playlist in self.session_manager.session.playlist_container():
if playlist.type() == 'playlist':
self.session_manager.playlist_manager.watch(playlist)
count += 1
logger.debug(u'Watching %d playlist(s) for changes', count)
def playlist_added(self, container, playlist, position, userdata):
"""Callback used by pyspotify"""

View File

@ -4,7 +4,6 @@ import Queue
from spotify import Link, SpotifyError
from mopidy.backends.base import BaseLibraryProvider
from mopidy.backends.spotify import ENCODING
from mopidy.backends.spotify.translator import SpotifyTranslator
from mopidy.models import Playlist
@ -55,7 +54,7 @@ class SpotifyLibraryProvider(BaseLibraryProvider):
spotify_query = u' '.join(spotify_query)
logger.debug(u'Spotify search query: %s' % spotify_query)
queue = Queue.Queue()
self.backend.spotify.search(spotify_query.encode(ENCODING), queue)
self.backend.spotify.search(spotify_query, queue)
try:
return queue.get(timeout=3) # XXX What is an reasonable timeout?
except Queue.Empty:

View File

@ -27,7 +27,8 @@ class SpotifyPlaylistManager(PyspotifyPlaylistManager):
def tracks_removed(self, playlist, tracks, userdata):
"""Callback used by pyspotify"""
logger.debug(u'Callback called: '
u'%d track(s) removed from playlist "%s"', len(tracks), playlist.name())
u'%d track(s) removed from playlist "%s"',
len(tracks), playlist.name())
self.session_manager.refresh_stored_playlists()
def playlist_renamed(self, playlist, userdata):

View File

@ -1,4 +1,3 @@
import glib
import logging
import os
import threading
@ -24,8 +23,9 @@ logger = logging.getLogger('mopidy.backends.spotify.session_manager')
class SpotifySessionManager(BaseThread, PyspotifySessionManager):
cache_location = settings.SPOTIFY_CACHE_PATH or CACHE_PATH
settings_location = settings.SPOTIFY_CACHE_PATH or CACHE_PATH
cache_location = (settings.SPOTIFY_CACHE_PATH
or os.path.join(CACHE_PATH, 'spotify'))
settings_location = cache_location
appkey_file = os.path.join(os.path.dirname(__file__), 'spotify_appkey.key')
user_agent = 'Mopidy %s' % get_version()
@ -43,6 +43,8 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
self.container_manager = None
self.playlist_manager = None
self._initial_data_receive_completed = False
def run_inside_try(self):
self.setup()
self.connect()
@ -97,10 +99,6 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
"""Callback used by pyspotify"""
logger.debug(u'User message: %s', message.strip())
def notify_main_thread(self, session):
"""Callback used by pyspotify"""
logger.debug(u'notify_main_thread() called')
def music_delivery(self, session, frames, frame_size, num_frames,
sample_type, sample_rate, channels):
"""Callback used by pyspotify"""
@ -130,6 +128,17 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
def log_message(self, session, data):
"""Callback used by pyspotify"""
logger.debug(u'System message: %s' % data.strip())
if 'offline-mgr' in data and 'files unlocked' in data:
# XXX This is a very very fragile and ugly hack, but we get no
# proper event when libspotify is done with initial data loading.
# We delay the expensive refresh of Mopidy's stored playlists until
# this message arrives. This way, we avoid doing the refresh once
# for every playlist or other change. This reduces the time from
# startup until the Spotify backend is ready from 35s to 12s in one
# test with clean Spotify cache. In cases with an outdated cache
# the time improvements should be a lot better.
self._initial_data_receive_completed = True
self.refresh_stored_playlists()
def end_of_track(self, session):
"""Callback used by pyspotify"""
@ -139,10 +148,11 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
def refresh_stored_playlists(self):
"""Refresh the stored playlists in the backend with fresh meta data
from Spotify"""
playlists = []
for spotify_playlist in self.session.playlist_container():
playlists.append(
SpotifyTranslator.to_mopidy_playlist(spotify_playlist))
if not self._initial_data_receive_completed:
logger.debug(u'Still getting data; skipped refresh of playlists')
return
playlists = map(SpotifyTranslator.to_mopidy_playlist,
self.session.playlist_container())
playlists = filter(None, playlists)
self.backend.stored_playlists.playlists = playlists
logger.debug(u'Refreshed %d stored playlist(s)', len(playlists))
@ -151,9 +161,18 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
"""Search method used by Mopidy backend"""
def callback(results, userdata=None):
# TODO Include results from results.albums(), etc. too
# TODO Consider launching a second search if results.total_tracks()
# is larger than len(results.tracks())
playlist = Playlist(tracks=[
SpotifyTranslator.to_mopidy_track(t)
for t in results.tracks()])
queue.put(playlist)
self.connected.wait()
self.session.search(query, callback)
self.session.search(query, callback, track_count=100,
album_count=0, artist_count=0)
def logout(self):
"""Log out from spotify"""
logger.debug(u'Logging out from Spotify')
if self.session:
self.session.logout()

View File

@ -4,7 +4,6 @@ import logging
from spotify import Link, SpotifyError
from mopidy import settings
from mopidy.backends.spotify import ENCODING
from mopidy.models import Artist, Album, Track, Playlist
logger = logging.getLogger('mopidy.backends.spotify.translator')
@ -31,9 +30,10 @@ class SpotifyTranslator(object):
uri = str(Link.from_track(spotify_track, 0))
if not spotify_track.is_loaded():
return Track(uri=uri, name=u'[loading...]')
if (spotify_track.album() is not None and
dt.MINYEAR <= int(spotify_track.album().year()) <= dt.MAXYEAR):
date = dt.date(spotify_track.album().year(), 1, 1)
spotify_album = spotify_track.album()
if (spotify_album is not None and spotify_album.is_loaded()
and dt.MINYEAR <= int(spotify_album.year()) <= dt.MAXYEAR):
date = dt.date(spotify_album.year(), 1, 1)
else:
date = None
return Track(
@ -51,9 +51,8 @@ class SpotifyTranslator(object):
def to_mopidy_playlist(cls, spotify_playlist):
if not spotify_playlist.is_loaded():
return Playlist(name=u'[loading...]')
# FIXME Replace this try-except with a check on the playlist type,
# which is currently not supported by pyspotify, to avoid handling
# playlist folder boundaries like normal playlists.
if spotify_playlist.type() != 'playlist':
return
try:
return Playlist(
uri=str(Link.from_playlist(spotify_playlist)),
@ -63,5 +62,4 @@ class SpotifyTranslator(object):
if str(Link.from_track(t, 0))],
)
except SpotifyError, e:
logger.info(u'Failed translating Spotify playlist '
'(probably a playlist folder boundary): %s', e)
logger.warning(u'Failed translating Spotify playlist: %s', e)

View File

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

View File

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

View File

@ -5,7 +5,7 @@ from pykka import registry, actor
from mopidy import listeners, settings
from mopidy.frontends.mpd import dispatcher, protocol
from mopidy.utils import network, process, log
from mopidy.utils import locale_decode, log, network, process
logger = logging.getLogger('mopidy.frontends.mpd')
@ -25,13 +25,15 @@ class MpdFrontend(actor.ThreadingActor, listeners.BackendListener):
"""
def __init__(self):
super(MpdFrontend, self).__init__()
hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME)
port = settings.MPD_SERVER_PORT
try:
network.Server(hostname, port, protocol=MpdSession)
except IOError, e:
logger.error(u'MPD server startup failed: %s', e)
network.Server(hostname, port, protocol=MpdSession,
max_connections=settings.MPD_SERVER_MAX_CONNECTIONS)
except IOError as error:
logger.error(u'MPD server startup failed: %s', locale_decode(error))
sys.exit(1)
logger.info(u'MPD server running at [%s]:%s', hostname, port)

View File

@ -90,7 +90,7 @@ class MpdDispatcher(object):
def _authenticate_filter(self, request, response, filter_chain):
if self.authenticated:
return self._call_next_filter(request, response, filter_chain)
elif settings.MPD_SERVER_PASSWORD is None:
elif settings.MPD_SERVER_PASSWORD is None:
self.authenticated = True
return self._call_next_filter(request, response, filter_chain)
else:
@ -161,6 +161,7 @@ class MpdDispatcher(object):
def _has_error(self, response):
return response and response[-1].startswith(u'ACK')
### Filter: call handler
def _call_handler_filter(self, request, response, filter_chain):
@ -241,11 +242,11 @@ class MpdContext(object):
"""
The backend. An instance of :class:`mopidy.backends.base.Backend`.
"""
if self._backend is not None:
return self._backend
backend_refs = ActorRegistry.get_by_class(Backend)
assert len(backend_refs) == 1, 'Expected exactly one running backend.'
self._backend = backend_refs[0].proxy()
if self._backend is None:
backend_refs = ActorRegistry.get_by_class(Backend)
assert len(backend_refs) == 1, \
'Expected exactly one running backend.'
self._backend = backend_refs[0].proxy()
return self._backend
@property
@ -253,9 +254,8 @@ class MpdContext(object):
"""
The mixer. An instance of :class:`mopidy.mixers.base.BaseMixer`.
"""
if self._mixer is not None:
return self._mixer
mixer_refs = ActorRegistry.get_by_class(BaseMixer)
assert len(mixer_refs) == 1, 'Expected exactly one running mixer.'
self._mixer = mixer_refs[0].proxy()
if self._mixer is None:
mixer_refs = ActorRegistry.get_by_class(BaseMixer)
assert len(mixer_refs) == 1, 'Expected exactly one running mixer.'
self._mixer = mixer_refs[0].proxy()
return self._mixer

View File

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

View File

@ -1,8 +1,9 @@
import re
import shlex
from mopidy.frontends.mpd.protocol import handle_request, stored_playlists
from mopidy.frontends.mpd.exceptions import MpdArgError, MpdNotImplemented
from mopidy.frontends.mpd.protocol import handle_request, stored_playlists
from mopidy.frontends.mpd.translator import playlist_to_mpd_format
def _build_query(mpd_query):
"""
@ -68,7 +69,8 @@ def find(context, mpd_query):
- also uses the search type "date".
"""
query = _build_query(mpd_query)
return context.backend.library.find_exact(**query).get().mpd_format()
return playlist_to_mpd_format(
context.backend.library.find_exact(**query).get())
@handle_request(r'^findadd '
r'(?P<query>("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? '
@ -187,8 +189,14 @@ def _list_build_query(field, mpd_query):
"""Converts a ``list`` query to a Mopidy query."""
if mpd_query is None:
return {}
# shlex does not seem to be friends with unicode objects
tokens = shlex.split(mpd_query.encode('utf-8'))
try:
# shlex does not seem to be friends with unicode objects
tokens = shlex.split(mpd_query.encode('utf-8'))
except ValueError as error:
if error.message == 'No closing quotation':
raise MpdArgError(u'Invalid unquoted character', command=u'list')
else:
raise error
tokens = [t.decode('utf-8') for t in tokens]
if len(tokens) == 1:
if field == u'album':
@ -324,7 +332,8 @@ def search(context, mpd_query):
- also uses the search type "date".
"""
query = _build_query(mpd_query)
return context.backend.library.search(**query).get().mpd_format()
return playlist_to_mpd_format(
context.backend.library.search(**query).get())
@handle_request(r'^update( "(?P<uri>[^"]+)")*$')
def update(context, uri=None, rescan_unmodified_files=False):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,21 +7,11 @@ import logging
from pykka.actor import ThreadingActor
from pykka.registry import ActorRegistry
from mopidy import settings
from mopidy.utils import get_class
from mopidy import settings, utils
from mopidy.backends.base import Backend
logger = logging.getLogger('mopidy.gstreamer')
default_caps = gst.Caps("""
audio/x-raw-int,
endianness=(int)1234,
channels=(int)2,
width=(int)16,
depth=(int)16,
signed=(boolean)true,
rate=(int)44100""")
class GStreamer(ThreadingActor):
"""
@ -29,42 +19,50 @@ class GStreamer(ThreadingActor):
**Settings:**
- :attr:`mopidy.settings.OUTPUTS`
- :attr:`mopidy.settings.OUTPUT`
"""
def __init__(self):
super(GStreamer, self).__init__()
self._default_caps = gst.Caps("""
audio/x-raw-int,
endianness=(int)1234,
channels=(int)2,
width=(int)16,
depth=(int)16,
signed=(boolean)true,
rate=(int)44100""")
self._pipeline = None
self._source = None
self._tee = None
self._uridecodebin = None
self._outputs = []
self._handlers = {}
self._output = None
def on_start(self):
self._setup_pipeline()
self._setup_outputs()
self._setup_output()
self._setup_message_processor()
def _setup_pipeline(self):
description = ' ! '.join([
'uridecodebin name=uri',
'audioconvert name=convert',
'tee name=tee'])
'audioconvert name=convert'])
logger.debug(u'Setting up base GStreamer pipeline: %s', description)
self._pipeline = gst.parse_launch(description)
self._tee = self._pipeline.get_by_name('tee')
self._uridecodebin = self._pipeline.get_by_name('uri')
self._uridecodebin.connect('notify::source', self._on_new_source)
self._uridecodebin.connect('pad-added', self._on_new_pad,
self._pipeline.get_by_name('convert').get_pad('sink'))
def _setup_outputs(self):
for output in settings.OUTPUTS:
get_class(output)(self).connect()
def _setup_output(self):
self._output = gst.parse_bin_from_description(settings.OUTPUT, True)
self._pipeline.add(self._output)
gst.element_link_many(self._pipeline.get_by_name('convert'),
self._output)
logger.debug('Output set to %s', settings.OUTPUT)
def _setup_message_processor(self):
bus = self._pipeline.get_bus()
@ -74,19 +72,17 @@ class GStreamer(ThreadingActor):
def _on_new_source(self, element, pad):
self._source = element.get_property('source')
try:
self._source.set_property('caps', default_caps)
self._source.set_property('caps', self._default_caps)
except TypeError:
pass
def _on_new_pad(self, source, pad, target_pad):
if not pad.is_linked():
if target_pad.is_linked():
target_pad.get_peer().unlink(target_pad)
pad.link(target_pad)
def _on_message(self, bus, message):
if message.src in self._handlers:
if self._handlers[message.src](message):
return # Message was handeled by output
if message.type == gst.MESSAGE_EOS:
logger.debug(u'GStreamer signalled end-of-stream. '
'Telling backend ...')
@ -299,103 +295,3 @@ class GStreamer(ThreadingActor):
event = gst.event_new_tag(taglist)
self._pipeline.send_event(event)
def connect_output(self, output):
"""
Connect output to pipeline.
:param output: output to connect to the pipeline
:type output: :class:`gst.Bin`
"""
self._pipeline.add(output)
output.sync_state_with_parent() # Required to add to running pipe
gst.element_link_many(self._tee, output)
self._outputs.append(output)
logger.debug('GStreamer added %s', output.get_name())
def list_outputs(self):
"""
Get list with the name of all active outputs.
:rtype: list of strings
"""
return [output.get_name() for output in self._outputs]
def remove_output(self, output):
"""
Remove output from our pipeline.
:param output: output to remove from the pipeline
:type output: :class:`gst.Bin`
"""
if output not in self._outputs:
raise LookupError('Ouput %s not present in pipeline'
% output.get_name)
teesrc = output.get_pad('sink').get_peer()
handler = teesrc.add_event_probe(self._handle_event_probe)
struct = gst.Structure('mopidy-unlink-tee')
struct.set_value('handler', handler)
event = gst.event_new_custom(gst.EVENT_CUSTOM_DOWNSTREAM, struct)
self._tee.send_event(event)
def _handle_event_probe(self, teesrc, event):
if event.type == gst.EVENT_CUSTOM_DOWNSTREAM and event.has_name('mopidy-unlink-tee'):
data = self._get_structure_data(event.get_structure())
output = teesrc.get_peer().get_parent()
teesrc.unlink(teesrc.get_peer())
teesrc.remove_event_probe(data['handler'])
output.set_state(gst.STATE_NULL)
self._pipeline.remove(output)
logger.warning('Removed %s', output.get_name())
return False
return True
def _get_structure_data(self, struct):
# Ugly hack to get around missing get_value in pygst bindings :/
data = {}
def get_data(key, value):
data[key] = value
struct.foreach(get_data)
return data
def connect_message_handler(self, element, handler):
"""
Attach custom message handler for given element.
Hook to allow outputs (or other code) to register custom message
handlers for all messages coming from the element in question.
In the case of outputs, :meth:`mopidy.outputs.BaseOutput.on_connect`
should be used to attach such handlers and care should be taken to
remove them in :meth:`mopidy.outputs.BaseOutput.on_remove` using
:meth:`remove_message_handler`.
The handler callback will only be given the message in question, and
is free to ignore the message. However, if the handler wants to prevent
the default handling of the message it should return :class:`True`
indicating that the message has been handled.
Note that there can only be one handler per element.
:param element: element to watch messages from
:type element: :class:`gst.Element`
:param handler: callable that takes :class:`gst.Message` and returns
:class:`True` if the message has been handeled
:type handler: callable
"""
self._handlers[element] = handler
def remove_message_handler(self, element):
"""
Remove custom message handler.
:param element: element to remove message handling from.
:type element: :class:`gst.Element`
"""
self._handlers.pop(element, None)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -185,10 +185,6 @@ class Track(ImmutableObject):
self.__dict__['artists'] = frozenset(kwargs.pop('artists', []))
super(Track, self).__init__(*args, **kwargs)
def mpd_format(self, *args, **kwargs):
from mopidy.frontends.mpd import translator
return translator.track_to_mpd_format(self, *args, **kwargs)
class Playlist(ImmutableObject):
"""
@ -224,7 +220,3 @@ class Playlist(ImmutableObject):
def length(self):
"""The number of tracks in the playlist. Read-only."""
return len(self.tracks)
def mpd_format(self, *args, **kwargs):
from mopidy.frontends.mpd import translator
return translator.playlist_to_mpd_format(self, *args, **kwargs)

View File

@ -1,105 +0,0 @@
import pygst
pygst.require('0.10')
import gst
import logging
logger = logging.getLogger('mopidy.outputs')
class BaseOutput(object):
"""Base class for pluggable audio outputs."""
MESSAGE_EOS = gst.MESSAGE_EOS
MESSAGE_ERROR = gst.MESSAGE_ERROR
MESSAGE_WARNING = gst.MESSAGE_WARNING
def __init__(self, gstreamer):
self.gstreamer = gstreamer
self.bin = self._build_bin()
self.bin.set_name(self.get_name())
self.modify_bin()
def _build_bin(self):
description = 'queue ! %s' % self.describe_bin()
logger.debug('Creating new output: %s', description)
return gst.parse_bin_from_description(description, True)
def connect(self):
"""Attach output to GStreamer pipeline."""
self.gstreamer.connect_output(self.bin)
self.on_connect()
def on_connect(self):
"""
Called after output has been connected to GStreamer pipeline.
*MAY be implemented by subclass.*
"""
pass
def remove(self):
"""Remove output from GStreamer pipeline."""
self.gstreamer.remove_output(self.bin)
self.on_remove()
def on_remove(self):
"""
Called after output has been removed from GStreamer pipeline.
*MAY be implemented by subclass.*
"""
pass
def get_name(self):
"""
Get name of the output. Defaults to the output's class name.
*MAY be implemented by subclass.*
:rtype: string
"""
return self.__class__.__name__
def modify_bin(self):
"""
Modifies ``self.bin`` before it is installed if needed.
Overriding this method allows for outputs to modify the constructed bin
before it is installed. This can for instance be a good place to call
`set_properties` on elements that need to be configured.
*MAY be implemented by subclass.*
"""
pass
def describe_bin(self):
"""
Return string describing the output bin in :command:`gst-launch`
format.
For simple cases this can just be a sink such as ``autoaudiosink``,
or it can be a chain like ``element1 ! element2 ! sink``. See the
manpage of :command:`gst-launch` for details on the format.
*MUST be implemented by subclass.*
:rtype: string
"""
raise NotImplementedError
def set_properties(self, element, properties):
"""
Helper method for setting of properties on elements.
Will call :meth:`gst.Element.set_property` on ``element`` for each key
in ``properties`` that has a value that is not :class:`None`.
:param element: element to set properties on
:type element: :class:`gst.Element`
:param properties: properties to set on element
:type properties: dict
"""
for key, value in properties.items():
if value is not None:
element.set_property(key, value)

View File

@ -1,34 +0,0 @@
from mopidy import settings
from mopidy.outputs import BaseOutput
class CustomOutput(BaseOutput):
"""
Custom output for using alternate setups.
This output is intended to handle two main cases:
1. Simple things like switching which sink to use. Say :class:`LocalOutput`
doesn't work for you and you want to switch to ALSA, simple. Set
:attr:`mopidy.settings.CUSTOM_OUTPUT` to ``alsasink`` and you are good
to go. Some possible sinks include:
- alsasink
- osssink
- pulsesink
- ...and many more
2. Advanced setups that require complete control of the output bin. For
these cases setup :attr:`mopidy.settings.CUSTOM_OUTPUT` with a
:command:`gst-launch` compatible string describing the target setup.
**Dependencies:**
- None
**Settings:**
- :attr:`mopidy.settings.CUSTOM_OUTPUT`
"""
def describe_bin(self):
return settings.CUSTOM_OUTPUT

View File

@ -1,20 +0,0 @@
from mopidy.outputs import BaseOutput
class LocalOutput(BaseOutput):
"""
Basic output to local audio sink.
This output will normally tell GStreamer to choose whatever it thinks is
best for your system. In other words this is usually a sane choice.
**Dependencies:**
- None
**Settings:**
- None
"""
def describe_bin(self):
return 'volume ! autoaudiosink'

View File

@ -1,58 +0,0 @@
import logging
from mopidy import settings
from mopidy.outputs import BaseOutput
logger = logging.getLogger('mopidy.outputs.shoutcast')
class ShoutcastOutput(BaseOutput):
"""
Shoutcast streaming output.
This output allows for streaming to an icecast server or anything else that
supports Shoutcast. The output supports setting for: server address, port,
mount point, user, password and encoder to use. Please see
:class:`mopidy.settings` for details about settings.
**Dependencies:**
- A SHOUTcast/Icecast server
**Settings:**
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_HOSTNAME`
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_PORT`
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_USERNAME`
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_PASSWORD`
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_MOUNT`
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_ENCODER`
"""
def describe_bin(self):
return 'audioconvert ! %s ! shout2send name=shoutcast' \
% settings.SHOUTCAST_OUTPUT_ENCODER
def modify_bin(self):
self.set_properties(self.bin.get_by_name('shoutcast'), {
u'ip': settings.SHOUTCAST_OUTPUT_HOSTNAME,
u'port': settings.SHOUTCAST_OUTPUT_PORT,
u'mount': settings.SHOUTCAST_OUTPUT_MOUNT,
u'username': settings.SHOUTCAST_OUTPUT_USERNAME,
u'password': settings.SHOUTCAST_OUTPUT_PASSWORD,
})
def on_connect(self):
self.gstreamer.connect_message_handler(
self.bin.get_by_name('shoutcast'), self.message_handler)
def on_remove(self):
self.gstreamer.remove_message_handler(
self.bin.get_by_name('shoutcast'))
def message_handler(self, message):
if message.type != self.MESSAGE_ERROR:
return False
error, debug = message.parse_error()
logger.warning('%s (%s)', error, debug)
self.remove()
return True

View File

@ -26,14 +26,6 @@ BACKENDS = (
#: details on the format.
CONSOLE_LOG_FORMAT = u'%(levelname)-8s %(message)s'
#: Which GStreamer bin description to use in
#: :class:`mopidy.outputs.custom.CustomOutput`.
#:
#: Default::
#:
#: CUSTOM_OUTPUT = u'fakesink'
CUSTOM_OUTPUT = u'fakesink'
#: The log format used for debug logging.
#:
#: See http://docs.python.org/library/logging.html#formatter-objects for
@ -180,17 +172,17 @@ MPD_SERVER_PORT = 6600
#: Default: :class:`None`, which means no password required.
MPD_SERVER_PASSWORD = None
#: List of outputs to use. See :mod:`mopidy.outputs` for all available
#: backends
#: The maximum number of concurrent connections the MPD server will accept.
#:
#: Default: 20
MPD_SERVER_MAX_CONNECTIONS = 20
#: Output to use. See :mod:`mopidy.outputs` for all available backends
#:
#: Default::
#:
#: OUTPUTS = (
#: u'mopidy.outputs.local.LocalOutput',
#: )
OUTPUTS = (
u'mopidy.outputs.local.LocalOutput',
)
#: OUTPUT = u'autoaudiosink'
OUTPUT = u'autoaudiosink'
#: Hostname of the SHOUTcast server which Mopidy should stream audio to.
#:

View File

@ -1,9 +1,12 @@
import locale
import logging
import os
import sys
logger = logging.getLogger('mopidy.utils')
# TODO: user itertools.chain.from_iterable(the_list)?
def flatten(the_list):
result = []
for element in the_list:
@ -13,19 +16,28 @@ def flatten(the_list):
result.append(element)
return result
def import_module(name):
__import__(name)
return sys.modules[name]
def get_class(name):
logger.debug('Loading: %s', name)
if '.' not in name:
raise ImportError("Couldn't load: %s" % name)
module_name = name[:name.rindex('.')]
class_name = name[name.rindex('.') + 1:]
cls_name = name[name.rindex('.') + 1:]
try:
module = import_module(module_name)
class_object = getattr(module, class_name)
cls = getattr(module, cls_name)
except (ImportError, AttributeError):
raise ImportError("Couldn't load: %s" % name)
return class_object
return cls
def locale_decode(bytestr):
try:
return unicode(bytestr)
except UnicodeError:
return str(bytestr).decode(locale.getpreferredencoding())

View File

@ -9,6 +9,8 @@ from pykka import ActorDeadError
from pykka.actor import ThreadingActor
from pykka.registry import ActorRegistry
from mopidy.utils import locale_decode
logger = logging.getLogger('mopidy.utils.server')
class ShouldRetrySocketCall(Exception):
@ -21,9 +23,9 @@ def try_ipv6_socket():
try:
socket.socket(socket.AF_INET6).close()
return True
except IOError, e:
except IOError as error:
logger.debug(u'Platform supports IPv6, but socket '
'creation failed, disabling: %s', e)
'creation failed, disabling: %s', locale_decode(error))
return False
#: Boolean value that indicates if creating an IPv6 socket will succeed.
@ -297,6 +299,7 @@ class LineProtocol(ThreadingActor):
encoding = 'utf-8'
def __init__(self, connection):
super(LineProtocol, self).__init__()
self.connection = connection
self.prevent_timeout = False
self.recv_buffer = ''

View File

@ -8,9 +8,12 @@ logger = logging.getLogger('mopidy.utils.path')
def get_or_create_folder(folder):
folder = os.path.expanduser(folder)
if not os.path.isdir(folder):
if os.path.isfile(folder):
raise OSError('A file with the same name as the desired ' \
'dir, "%s", already exists.' % folder)
elif not os.path.isdir(folder):
logger.info(u'Creating dir %s', folder)
os.mkdir(folder, 0755)
os.makedirs(folder, 0755)
return folder
def get_or_create_file(filename):

View File

@ -2,7 +2,6 @@
from __future__ import absolute_import
from copy import copy
import getpass
import glib
import logging
import os
from pprint import pformat
@ -113,6 +112,7 @@ def validate_settings(defaults, settings):
errors = {}
changed = {
'CUSTOM_OUTPUT': 'OUTPUT',
'DUMP_LOG_FILENAME': 'DEBUG_LOG_FILENAME',
'DUMP_LOG_FORMAT': 'DEBUG_LOG_FORMAT',
'FRONTEND': 'FRONTENDS',
@ -121,7 +121,6 @@ def validate_settings(defaults, settings):
'LOCAL_OUTPUT_OVERRIDE': 'CUSTOM_OUTPUT',
'LOCAL_PLAYLIST_FOLDER': 'LOCAL_PLAYLIST_PATH',
'LOCAL_TAG_CACHE': 'LOCAL_TAG_CACHE_FILE',
'OUTPUT': None,
'SERVER': None,
'SERVER_HOSTNAME': 'MPD_SERVER_HOSTNAME',
'SERVER_PORT': 'MPD_SERVER_PORT',
@ -141,15 +140,23 @@ def validate_settings(defaults, settings):
if setting == 'BACKENDS':
if 'mopidy.backends.despotify.DespotifyBackend' in value:
errors[setting] = (u'Deprecated setting value. ' +
'"mopidy.backends.despotify.DespotifyBackend" is no ' +
'longer available.')
errors[setting] = (
u'Deprecated setting value. '
u'"mopidy.backends.despotify.DespotifyBackend" is no '
u'longer available.')
continue
if setting == 'OUTPUTS':
errors[setting] = (
u'Deprecated setting, please change to OUTPUT. OUTPUT expectes '
u'a GStreamer bin describing your desired output.')
continue
if setting == 'SPOTIFY_BITRATE':
if value not in (96, 160, 320):
errors[setting] = (u'Unavailable Spotify bitrate. ' +
u'Available bitrates are 96, 160, and 320.')
errors[setting] = (
u'Unavailable Spotify bitrate. Available bitrates are 96, '
u'160, and 320.')
if setting not in defaults:
errors[setting] = u'Unknown setting. Is it misspelled?'

View File

@ -18,6 +18,7 @@
# R0921 - Abstract class not referenced
# W0141 - Used builtin function '%s'
# W0142 - Used * or ** magic
# W0511 - TODO, FIXME and XXX in the code
# W0613 - Unused argument %r
#
disable = C0103,C0111,E0102,E0202,E1101,R0201,R0801,R0903,R0904,R0921,W0141,W0142,W0613
disable = C0103,C0111,E0102,E0202,E1101,R0201,R0801,R0903,R0904,R0921,W0141,W0142,W0511,W0613

View File

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

View File

@ -6,9 +6,13 @@ from distutils.core import setup
from distutils.command.install_data import install_data
from distutils.command.install import INSTALL_SCHEMES
import os
import re
import sys
from mopidy import get_version
def get_version():
init_py = open('mopidy/__init__.py').read()
metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", init_py))
return metadata['version']
class osx_install_data(install_data):
# On MacOS, the platform-specific lib dir is

View File

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

View File

@ -1,7 +1,7 @@
import mock
import random
from mopidy.models import Playlist, Track
from mopidy.models import CpTrack, Playlist, Track
from mopidy.gstreamer import GStreamer
from tests.backends.base import populate_playlist
@ -18,6 +18,13 @@ class CurrentPlaylistControllerTest(object):
assert len(self.tracks) == 3, 'Need three tracks to run tests.'
def test_length(self):
self.assertEqual(0, len(self.controller.cp_tracks))
self.assertEqual(0, self.controller.length)
self.controller.append(self.tracks)
self.assertEqual(3, len(self.controller.cp_tracks))
self.assertEqual(3, self.controller.length)
def test_add(self):
for track in self.tracks:
cp_track = self.controller.add(track)
@ -136,6 +143,18 @@ class CurrentPlaylistControllerTest(object):
self.assertEqual(self.playback.state, self.playback.STOPPED)
self.assertEqual(self.playback.current_track, None)
def test_index_returns_index_of_track(self):
cp_tracks = []
for track in self.tracks:
cp_tracks.append(self.controller.add(track))
self.assertEquals(0, self.controller.index(cp_tracks[0]))
self.assertEquals(1, self.controller.index(cp_tracks[1]))
self.assertEquals(2, self.controller.index(cp_tracks[2]))
def test_index_raises_value_error_if_item_not_found(self):
test = lambda: self.controller.index(CpTrack(0, Track()))
self.assertRaises(ValueError, test)
@populate_playlist
def test_move_single(self):
self.controller.move(0, 0, 2)
@ -241,6 +260,18 @@ class CurrentPlaylistControllerTest(object):
self.assertEqual(self.tracks[0], shuffled_tracks[0])
self.assertEqual(set(self.tracks), set(shuffled_tracks))
@populate_playlist
def test_slice_returns_a_subset_of_tracks(self):
track_slice = self.controller.slice(1, 3)
self.assertEqual(2, len(track_slice))
self.assertEqual(self.tracks[1], track_slice[0].track)
self.assertEqual(self.tracks[2], track_slice[1].track)
@populate_playlist
def test_slice_returns_empty_list_if_indexes_outside_tracks_list(self):
self.assertEqual(0, len(self.controller.slice(7, 8)))
self.assertEqual(0, len(self.controller.slice(-1, 1)))
def test_version_does_not_change_when_appending_nothing(self):
version = self.controller.version
self.controller.append([])

View File

@ -8,9 +8,9 @@ from mopidy.mixers import dummy as mixer
from tests import unittest
class MockConnetion(mock.Mock):
class MockConnection(mock.Mock):
def __init__(self, *args, **kwargs):
super(MockConnetion, self).__init__(*args, **kwargs)
super(MockConnection, self).__init__(*args, **kwargs)
self.host = mock.sentinel.host
self.port = mock.sentinel.port
self.response = []
@ -25,7 +25,7 @@ class BaseTestCase(unittest.TestCase):
self.backend = backend.DummyBackend.start().proxy()
self.mixer = mixer.DummyMixer.start().proxy()
self.connection = MockConnetion()
self.connection = MockConnection()
self.session = mpd.MpdSession(self.connection)
self.dispatcher = self.session.dispatcher
self.context = self.dispatcher.context

View File

@ -271,14 +271,22 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase):
self.sendRequest(u'playlistinfo')
self.assertInResponse(u'Title: a')
self.assertInResponse(u'Pos: 0')
self.assertInResponse(u'Title: b')
self.assertInResponse(u'Pos: 1')
self.assertInResponse(u'Title: c')
self.assertInResponse(u'Pos: 2')
self.assertInResponse(u'Title: d')
self.assertInResponse(u'Pos: 3')
self.assertInResponse(u'Title: e')
self.assertInResponse(u'Pos: 4')
self.assertInResponse(u'Title: f')
self.assertInResponse(u'Pos: 5')
self.assertInResponse(u'OK')
def test_playlistinfo_with_songpos(self):
# Make the track's CPID not match the playlist position
self.backend.current_playlist.cp_id = 17
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
@ -286,11 +294,17 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase):
self.sendRequest(u'playlistinfo "4"')
self.assertNotInResponse(u'Title: a')
self.assertNotInResponse(u'Pos: 0')
self.assertNotInResponse(u'Title: b')
self.assertNotInResponse(u'Pos: 1')
self.assertNotInResponse(u'Title: c')
self.assertNotInResponse(u'Pos: 2')
self.assertNotInResponse(u'Title: d')
self.assertNotInResponse(u'Pos: 3')
self.assertInResponse(u'Title: e')
self.assertInResponse(u'Pos: 4')
self.assertNotInResponse(u'Title: f')
self.assertNotInResponse(u'Pos: 5')
self.assertInResponse(u'OK')
def test_playlistinfo_with_negative_songpos_same_as_playlistinfo(self):
@ -306,11 +320,17 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase):
self.sendRequest(u'playlistinfo "2:"')
self.assertNotInResponse(u'Title: a')
self.assertNotInResponse(u'Pos: 0')
self.assertNotInResponse(u'Title: b')
self.assertNotInResponse(u'Pos: 1')
self.assertInResponse(u'Title: c')
self.assertInResponse(u'Pos: 2')
self.assertInResponse(u'Title: d')
self.assertInResponse(u'Pos: 3')
self.assertInResponse(u'Title: e')
self.assertInResponse(u'Pos: 4')
self.assertInResponse(u'Title: f')
self.assertInResponse(u'Pos: 5')
self.assertInResponse(u'OK')
def test_playlistinfo_with_closed_range(self):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,38 @@
import mock
from mopidy.utils import locale_decode
from tests import unittest
@mock.patch('mopidy.utils.locale.getpreferredencoding')
class LocaleDecodeTest(unittest.TestCase):
def test_can_decode_utf8_strings_with_french_content(self, mock):
mock.return_value = 'UTF-8'
result = locale_decode(
'[Errno 98] Adresse d\xc3\xa9j\xc3\xa0 utilis\xc3\xa9e')
self.assertEquals(u'[Errno 98] Adresse d\xe9j\xe0 utilis\xe9e', result)
def test_can_decode_an_ioerror_with_french_content(self, mock):
mock.return_value = 'UTF-8'
error = IOError(98, 'Adresse d\xc3\xa9j\xc3\xa0 utilis\xc3\xa9e')
result = locale_decode(error)
self.assertEquals(u'[Errno 98] Adresse d\xe9j\xe0 utilis\xe9e', result)
def test_does_not_use_locale_to_decode_unicode_strings(self, mock):
mock.return_value = 'UTF-8'
locale_decode(u'abc')
self.assertFalse(mock.called)
def test_does_not_use_locale_to_decode_ascii_bytestrings(self, mock):
mock.return_value = 'UTF-8'
locale_decode('abc')
self.assertFalse(mock.called)

View File

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

View File

@ -1,14 +1,14 @@
from distutils.version import StrictVersion as SV
import platform
from mopidy import get_plain_version, get_platform, get_python
from mopidy import __version__, get_platform, get_python
from tests import unittest
class VersionTest(unittest.TestCase):
def test_current_version_is_parsable_as_a_strict_version_number(self):
SV(get_plain_version())
SV(__version__)
def test_versions_can_be_strictly_ordered(self):
self.assert_(SV('0.1.0a0') < SV('0.1.0a1'))
@ -22,8 +22,13 @@ class VersionTest(unittest.TestCase):
self.assert_(SV('0.3.1') < SV('0.4.0'))
self.assert_(SV('0.4.0') < SV('0.4.1'))
self.assert_(SV('0.4.1') < SV('0.5.0'))
self.assert_(SV('0.5.0') < SV(get_plain_version()))
self.assert_(SV(get_plain_version()) < SV('0.6.1'))
self.assert_(SV('0.5.0') < SV('0.6.0'))
self.assert_(SV('0.6.0') < SV('0.6.1'))
self.assert_(SV('0.6.1') < SV('0.7.0'))
self.assert_(SV('0.7.0') < SV('0.7.1'))
self.assert_(SV('0.7.1') < SV('0.7.2'))
self.assert_(SV('0.7.2') < SV(__version__))
self.assert_(SV(__version__) < SV('0.8.0'))
def test_get_platform_contains_platform(self):
self.assert_(platform.platform() in get_platform())

190
tools/debug-proxy.py Executable file
View File

@ -0,0 +1,190 @@
#! /usr/bin/env python
import argparse
import difflib
import sys
from gevent import select, server, socket
COLORS = ['\033[1;%dm' % (30+i) for i in range(8)]
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = COLORS
RESET = "\033[0m"
BOLD = "\033[1m"
def proxy(client, address, reference_address, actual_address):
"""Main handler code that gets called for each connection."""
client.setblocking(False)
reference = connect(reference_address)
actual = connect(actual_address)
if reference and actual:
loop(client, address, reference, actual)
else:
print 'Could not connect to one of the backends.'
for sock in (client, reference, actual):
close(sock)
def connect(address):
"""Connect to given address and set socket non blocking."""
try:
sock = socket.socket()
sock.connect(address)
sock.setblocking(False)
except socket.error:
return None
return sock
def close(sock):
"""Shutdown and close our sockets."""
try:
sock.shutdown(socket.SHUT_WR)
sock.close()
except socket.error:
pass
def loop(client, address, reference, actual):
"""Loop that handles one MPD reqeust/response pair per iteration."""
# Consume banners from backends
responses = dict()
disconnected = read([reference, actual], responses, find_response_end_token)
diff(address, '', responses[reference], responses[actual])
# We lost a backend, might as well give up.
if disconnected:
return
client.sendall(responses[reference])
while True:
responses = dict()
# Get the command from the client. Not sure how an if this will handle
# client sending multiple commands currently :/
disconnected = read([client], responses, find_request_end_token)
# We lost the client, might as well give up.
if disconnected:
return
# Send the entire command to both backends.
reference.sendall(responses[client])
actual.sendall(responses[client])
# Get the entire resonse from both backends.
disconnected = read([reference, actual], responses, find_response_end_token)
# Send the client the complete reference response
client.sendall(responses[reference])
# Compare our responses
diff(address, responses[client], responses[reference], responses[actual])
# Give up if we lost a backend.
if disconnected:
return
def read(sockets, responses, find_end_token):
"""Keep reading from sockets until they disconnet or we find our token."""
# This function doesn't go to well with idle when backends are out of sync.
disconnected = False
for sock in sockets:
responses.setdefault(sock, '')
while sockets:
for sock in select.select(sockets, [], [])[0]:
data = sock.recv(4096)
responses[sock] += data
if find_end_token(responses[sock]):
sockets.remove(sock)
if not data:
sockets.remove(sock)
disconnected = True
return disconnected
def find_response_end_token(data):
"""Find token that indicates the response is over."""
for line in data.splitlines(True):
if line.startswith(('OK', 'ACK')) and line.endswith('\n'):
return True
return False
def find_request_end_token(data):
"""Find token that indicates that request is over."""
lines = data.splitlines(True)
if not lines:
return False
elif 'command_list_ok_begin' == lines[0].strip():
return 'command_list_end' == lines[-1].strip()
else:
return lines[0].endswith('\n')
def diff(address, command, reference_response, actual_response):
"""Print command from client and a unified diff of the responses."""
sys.stdout.write('[%s]:%s\n%s' % (address[0], address[1], command))
for line in difflib.unified_diff(reference_response.splitlines(True),
actual_response.splitlines(True),
fromfile='Reference response',
tofile='Actual response'):
if line.startswith('+') and not line.startswith('+++'):
sys.stdout.write(GREEN)
elif line.startswith('-') and not line.startswith('---'):
sys.stdout.write(RED)
elif line.startswith('@@'):
sys.stdout.write(CYAN)
sys.stdout.write(line)
sys.stdout.write(RESET)
sys.stdout.flush()
def parse_args():
"""Handle flag parsing."""
parser = argparse.ArgumentParser(
description='Proxy and compare MPD protocol interactions.')
parser.add_argument('--listen', default=':6600', type=parse_address,
help='address:port to listen on.')
parser.add_argument('--reference', default=':6601', type=parse_address,
help='address:port for the reference backend.')
parser.add_argument('--actual', default=':6602', type=parse_address,
help='address:port for the actual backend.')
return parser.parse_args()
def parse_address(address):
"""Convert host:port or port to address to pass to connect."""
if ':' not in address:
return ('', int(address))
host, port = address.rsplit(':', 1)
return (host, int(port))
if __name__ == '__main__':
args = parse_args()
def handle(client, address):
"""Wrapper that adds reference and actual backends to proxy calls."""
return proxy(client, address, args.reference, args.actual)
try:
server.StreamServer(args.listen, handle).serve_forever()
except (KeyboardInterrupt, SystemExit):
pass