Release v0.7.0
This commit is contained in:
commit
2bf6a0cd4e
@ -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/>`_
|
||||
|
||||
15
docs/_templates/layout.html
vendored
15
docs/_templates/layout.html
vendored
@ -1,15 +0,0 @@
|
||||
{% extends "!layout.html" %}
|
||||
|
||||
{% block extrahead %}
|
||||
{{ super() }}
|
||||
<script type="text/javascript">
|
||||
var _gaq = _gaq || [];
|
||||
_gaq.push(['_setAccount', 'UA-15510432-1']);
|
||||
_gaq.push(['_trackPageview']);
|
||||
(function() {
|
||||
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
|
||||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
236
docs/_themes/nature/static/nature.css_t
vendored
236
docs/_themes/nature/static/nature.css_t
vendored
@ -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;
|
||||
}
|
||||
54
docs/_themes/nature/static/pygments.css
vendored
54
docs/_themes/nature/static/pygments.css
vendored
@ -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 */
|
||||
4
docs/_themes/nature/theme.conf
vendored
4
docs/_themes/nature/theme.conf
vendored
@ -1,4 +0,0 @@
|
||||
[theme]
|
||||
inherit = basic
|
||||
stylesheet = nature.css
|
||||
pygments_style = tango
|
||||
@ -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.
|
||||
|
||||
@ -4,6 +4,41 @@ Changes
|
||||
|
||||
This change log is used to track all major changes to Mopidy.
|
||||
|
||||
v0.7.0 (2012-02-25)
|
||||
===================
|
||||
|
||||
Not a big release with regard to features, but this release got some
|
||||
performance improvements over v0.6, especially for slower Atom systems. It also
|
||||
fixes a couple of other bugs, including one which made Mopidy crash when using
|
||||
GStreamer from the prereleases of Ubuntu 12.04.
|
||||
|
||||
**Changes**
|
||||
|
||||
- The MPD command ``playlistinfo`` is now faster, thanks to John Bäckstrand.
|
||||
|
||||
- Added the method
|
||||
:meth:`mopidy.backends.base.CurrentPlaylistController.length()`,
|
||||
:meth:`mopidy.backends.base.CurrentPlaylistController.index()`, and
|
||||
:meth:`mopidy.backends.base.CurrentPlaylistController.slice()` to reduce the
|
||||
need for copying the entire current playlist from one thread to another.
|
||||
Thanks to John Bäckstrand for pinpointing the issue.
|
||||
|
||||
- Fix crash on creation of config and cache directories if intermediate
|
||||
directories does not exist. This was especially the case on OS X, where
|
||||
``~/.config`` doesn't exist for most users.
|
||||
|
||||
- Fix ``gst.LinkError`` which appeared when using newer versions of GStreamer,
|
||||
e.g. on Ubuntu 12.04 Alpha. (Fixes: :issue:`144`)
|
||||
|
||||
- Fix crash on mismatching quotation in ``list`` MPD queries. (Fixes:
|
||||
:issue:`137`)
|
||||
|
||||
- Volume is now reported to be the same as the volume was set to, also when
|
||||
internal rounding have been done due to
|
||||
:attr:`mopidy.settings.MIXER_MAX_VOLUME` has been set to cap the volume. This
|
||||
should make it possible to manage capped volume from clients that only
|
||||
increase volume with one step at a time, like ncmpcpp does.
|
||||
|
||||
|
||||
v0.6.1 (2011-12-28)
|
||||
===================
|
||||
@ -23,6 +58,7 @@ make the Debian packages work out of the box again.
|
||||
about bad playlists.
|
||||
|
||||
|
||||
|
||||
v0.6.0 (2011-10-09)
|
||||
===================
|
||||
|
||||
@ -197,7 +233,7 @@ This is a bug fix release fixing audio problems on older GStreamer and some
|
||||
minor bugs.
|
||||
|
||||
|
||||
**Bugfixes**
|
||||
**Bug fixes**
|
||||
|
||||
- Fix broken audio on at least GStreamer 0.10.30, which affects Ubuntu 10.10.
|
||||
The GStreamer `appsrc` bin wasn't being linked due to lack of default caps.
|
||||
@ -242,7 +278,7 @@ loading from Mopidy 0.3.0 is still present.
|
||||
|
||||
**Important changes**
|
||||
|
||||
- Mopidy now depends on `Pykka <http://jodal.github.com/pykka>`_ >=0.12. If you
|
||||
- Mopidy now depends on `Pykka <http://pykka.readthedocs.org/>`_ >=0.12. If you
|
||||
install from APT, Pykka will automatically be installed. If you are not
|
||||
installing from APT, you may install Pykka from PyPI::
|
||||
|
||||
@ -324,7 +360,7 @@ v0.3.1 (2011-01-22)
|
||||
|
||||
A couple of fixes to the 0.3.0 release is needed to get a smooth installation.
|
||||
|
||||
**Bugfixes**
|
||||
**Bug fixes**
|
||||
|
||||
- The Spotify application key was missing from the Python package.
|
||||
|
||||
@ -493,7 +529,7 @@ v0.2.1 (2011-01-07)
|
||||
|
||||
This is a maintenance release without any new features.
|
||||
|
||||
**Bugfixes**
|
||||
**Bug fixes**
|
||||
|
||||
- Fix crash in :mod:`mopidy.frontends.lastfm` which occurred at playback if
|
||||
either :mod:`pylast` was not installed or the Last.fm scrobbling was not
|
||||
@ -823,7 +859,7 @@ As always, report problems at our IRC channel or our issue tracker. Thanks!
|
||||
|
||||
- Merged the ``gstreamer`` branch from Thomas Adamcik:
|
||||
|
||||
- More than 200 new tests, and thus several bugfixes to existing code.
|
||||
- More than 200 new tests, and thus several bug fixes to existing code.
|
||||
- Several new generic features, like shuffle, consume, and playlist repeat.
|
||||
(Fixes: :issue:`3`)
|
||||
- **[Work in Progress]** A new backend for playing music from a local music
|
||||
|
||||
55
docs/conf.py
55
docs/conf.py
@ -13,20 +13,57 @@
|
||||
|
||||
import sys, os
|
||||
|
||||
class Mock(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return Mock()
|
||||
|
||||
@classmethod
|
||||
def __getattr__(self, name):
|
||||
if name in ('__file__', '__path__'):
|
||||
return '/dev/null'
|
||||
elif name[0] == name[0].upper():
|
||||
return type(name, (), {})
|
||||
else:
|
||||
return Mock()
|
||||
|
||||
MOCK_MODULES = [
|
||||
'alsaaudio',
|
||||
'dbus',
|
||||
'dbus.mainloop',
|
||||
'dbus.mainloop.glib',
|
||||
'dbus.service',
|
||||
'glib',
|
||||
'gobject',
|
||||
'gst',
|
||||
'pygst',
|
||||
'pykka',
|
||||
'pykka.actor',
|
||||
'pykka.future',
|
||||
'pykka.registry',
|
||||
'pylast',
|
||||
'serial',
|
||||
]
|
||||
for mod_name in MOCK_MODULES:
|
||||
sys.modules[mod_name] = Mock()
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
|
||||
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/../'))
|
||||
|
||||
import mopidy
|
||||
# When RTD builds the project, it sets the READTHEDOCS environment variable to
|
||||
# the string True.
|
||||
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = ['sphinx.ext.autodoc',
|
||||
'sphinx.ext.graphviz', 'sphinx.ext.inheritance_diagram',
|
||||
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.graphviz',
|
||||
'sphinx.ext.extlinks', 'sphinx.ext.viewcode']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
@ -43,13 +80,14 @@ master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'Mopidy'
|
||||
copyright = u'2010-2011, Stein Magnus Jodal and contributors'
|
||||
copyright = u'2010-2012, Stein Magnus Jodal and contributors'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
import mopidy
|
||||
release = mopidy.get_version()
|
||||
# The short X.Y version.
|
||||
version = '.'.join(release.split('.')[:2])
|
||||
@ -97,7 +135,7 @@ modindex_common_prefix = ['mopidy.']
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. Major themes that come with
|
||||
# Sphinx are currently 'default' and 'sphinxdoc'.
|
||||
html_theme = 'nature'
|
||||
html_theme = 'default'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
@ -116,7 +154,8 @@ html_theme_path = ['_themes']
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
html_logo = '_static/mopidy.png'
|
||||
if on_rtd:
|
||||
html_logo = '_static/mopidy.png'
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
@ -130,7 +169,7 @@ html_static_path = ['_static']
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
html_last_updated_fmt = '%b %d, %Y'
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
@ -202,4 +241,4 @@ latex_documents = [
|
||||
|
||||
needs_sphinx = '1.0'
|
||||
|
||||
extlinks = {'issue': ('http://github.com/mopidy/mopidy/issues/%s', 'GH-')}
|
||||
extlinks = {'issue': ('https://github.com/mopidy/mopidy/issues/%s', 'GH-')}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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/>`_
|
||||
|
||||
@ -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
|
||||
|
||||
@ -18,36 +18,36 @@ Requirements
|
||||
gstreamer
|
||||
libspotify
|
||||
|
||||
If you install Mopidy from the APT archive, as described below, you can skip
|
||||
the dependency installation part.
|
||||
If you install Mopidy from the APT archive, as described below, APT will take
|
||||
care of all the dependencies for you. Otherwise, make sure you got the required
|
||||
dependencies installed.
|
||||
|
||||
Otherwise, make sure you got the required dependencies installed.
|
||||
- Hard dependencies:
|
||||
|
||||
- Python >= 2.6, < 3
|
||||
- Python >= 2.6, < 3
|
||||
|
||||
- `Pykka <http://jodal.github.com/pykka/>`_ >= 0.12.3
|
||||
- Pykka >= 0.12.3::
|
||||
|
||||
- GStreamer >= 0.10, with Python bindings. See :doc:`gstreamer`.
|
||||
sudo pip install -U pykka
|
||||
|
||||
- Mixer dependencies: The default mixer does not require any additional
|
||||
dependencies. If you use another mixer, see the mixer's docs for any
|
||||
additional requirements.
|
||||
|
||||
- Dependencies for at least one Mopidy backend:
|
||||
|
||||
- The default backend, :mod:`mopidy.backends.spotify`, requires libspotify
|
||||
and pyspotify. See :doc:`libspotify`.
|
||||
|
||||
- The local backend, :mod:`mopidy.backends.local`, requires no additional
|
||||
dependencies.
|
||||
- GStreamer 0.10.x, with Python bindings. See :doc:`gstreamer`.
|
||||
|
||||
- Optional dependencies:
|
||||
|
||||
- To use the Last.FM scrobbler, see :mod:`mopidy.frontends.lastfm` for
|
||||
additional requirements.
|
||||
- For Spotify support, you need libspotify and pyspotify. See
|
||||
:doc:`libspotify`.
|
||||
|
||||
- To use the MPRIS frontend, e.g. using the Ubuntu Sound Menu, see
|
||||
:mod:`mopidy.frontends.mpris` for additional requirements.
|
||||
- To scrobble your played tracks to Last.FM, you need pylast::
|
||||
|
||||
sudo pip install -U pylast
|
||||
|
||||
- To use MPRIS, e.g. for controlling Mopidy from the Ubuntu Sound Menu, you
|
||||
need some additional requirements::
|
||||
|
||||
sudo apt-get install python-dbus python-indicate
|
||||
|
||||
- Some custom mixers (but not the default one) require additional
|
||||
dependencies. See the docs for each mixer.
|
||||
|
||||
|
||||
Install latest stable release
|
||||
@ -100,8 +100,8 @@ install Mopidy from PyPI using Pip.
|
||||
|
||||
#. Then, you need to install Pip::
|
||||
|
||||
sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian
|
||||
sudo easy_install pip # On OS X
|
||||
sudo apt-get install python-setuptools python-pip # On Ubuntu/Debian
|
||||
sudo easy_install pip # On OS X
|
||||
|
||||
#. To install the currently latest stable release of Mopidy::
|
||||
|
||||
@ -112,8 +112,6 @@ install Mopidy from PyPI using Pip.
|
||||
#. Next, you need to set a couple of :doc:`settings </settings>`, and then
|
||||
you're ready to :doc:`run Mopidy </running>`.
|
||||
|
||||
If you for some reason can't use Pip, try ``easy_install`` instead.
|
||||
|
||||
|
||||
Install development version
|
||||
===========================
|
||||
@ -134,8 +132,8 @@ Mopidy's ``develop`` branch.
|
||||
|
||||
#. Then, you need to install Pip::
|
||||
|
||||
sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian
|
||||
sudo easy_install pip # On OS X
|
||||
sudo apt-get install python-setuptools python-pip # On Ubuntu/Debian
|
||||
sudo easy_install pip # On OS X
|
||||
|
||||
#. To install the latest snapshot of Mopidy, run::
|
||||
|
||||
@ -157,7 +155,7 @@ If you want to contribute to Mopidy, you should install Mopidy using Git.
|
||||
|
||||
#. Then install Git, if haven't already::
|
||||
|
||||
sudo aptitude install git-core # On Ubuntu/Debian
|
||||
sudo apt-get install git-core # On Ubuntu/Debian
|
||||
sudo brew install git # On OS X using Homebrew
|
||||
|
||||
#. Clone the official Mopidy repository, or your own fork of it::
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -2,8 +2,6 @@
|
||||
:mod:`mopidy.gstreamer` -- GStreamer adapter
|
||||
********************************************
|
||||
|
||||
.. inheritance-diagram:: mopidy.gstreamer
|
||||
|
||||
.. automodule:: mopidy.gstreamer
|
||||
:synopsis: GStreamer adapter
|
||||
:members:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -8,11 +8,11 @@ import os
|
||||
|
||||
from subprocess import PIPE, Popen
|
||||
|
||||
VERSION = (0, 6, 1)
|
||||
VERSION = (0, 7, 0)
|
||||
|
||||
DATA_PATH = os.path.join(glib.get_user_data_dir(), 'mopidy')
|
||||
CACHE_PATH = os.path.join(glib.get_user_cache_dir(), 'mopidy')
|
||||
SETTINGS_PATH = os.path.join(glib.get_user_config_dir(), 'mopidy')
|
||||
DATA_PATH = os.path.join(str(glib.get_user_data_dir()), 'mopidy')
|
||||
CACHE_PATH = os.path.join(str(glib.get_user_cache_dir()), 'mopidy')
|
||||
SETTINGS_PATH = os.path.join(str(glib.get_user_config_dir()), 'mopidy')
|
||||
SETTINGS_FILE = os.path.join(SETTINGS_PATH, 'settings.py')
|
||||
|
||||
def get_version():
|
||||
|
||||
@ -28,7 +28,7 @@ class CurrentPlaylistController(object):
|
||||
|
||||
Read-only.
|
||||
"""
|
||||
return [copy(ct) for ct in self._cp_tracks]
|
||||
return [copy(cp_track) for cp_track in self._cp_tracks]
|
||||
|
||||
@property
|
||||
def tracks(self):
|
||||
@ -37,7 +37,14 @@ class CurrentPlaylistController(object):
|
||||
|
||||
Read-only.
|
||||
"""
|
||||
return [ct[1] for ct in self._cp_tracks]
|
||||
return [cp_track.track for cp_track in self._cp_tracks]
|
||||
|
||||
@property
|
||||
def length(self):
|
||||
"""
|
||||
Length of the current playlist.
|
||||
"""
|
||||
return len(self._cp_tracks)
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
@ -116,9 +123,9 @@ class CurrentPlaylistController(object):
|
||||
matches = self._cp_tracks
|
||||
for (key, value) in criteria.iteritems():
|
||||
if key == 'cpid':
|
||||
matches = filter(lambda ct: ct[0] == value, matches)
|
||||
matches = filter(lambda ct: ct.cpid == value, matches)
|
||||
else:
|
||||
matches = filter(lambda ct: getattr(ct[1], key) == value,
|
||||
matches = filter(lambda ct: getattr(ct.track, key) == value,
|
||||
matches)
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
@ -129,6 +136,19 @@ class CurrentPlaylistController(object):
|
||||
else:
|
||||
raise LookupError(u'"%s" match multiple tracks' % criteria_string)
|
||||
|
||||
def index(self, cp_track):
|
||||
"""
|
||||
Get index of the given (CPID integer, :class:`mopidy.models.Track`)
|
||||
two-tuple in the current playlist.
|
||||
|
||||
Raises :exc:`ValueError` if not found.
|
||||
|
||||
:param cp_track: track to find the index of
|
||||
:type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`)
|
||||
:rtype: int
|
||||
"""
|
||||
return self._cp_tracks.index(cp_track)
|
||||
|
||||
def move(self, start, end, to_position):
|
||||
"""
|
||||
Move the tracks in the slice ``[start:end]`` to ``to_position``.
|
||||
@ -168,7 +188,6 @@ class CurrentPlaylistController(object):
|
||||
|
||||
:param criteria: on or more criteria to match by
|
||||
:type criteria: dict
|
||||
:type track: :class:`mopidy.models.Track`
|
||||
"""
|
||||
cp_track = self.get(**criteria)
|
||||
position = self._cp_tracks.index(cp_track)
|
||||
@ -204,6 +223,19 @@ class CurrentPlaylistController(object):
|
||||
self._cp_tracks = before + shuffled + after
|
||||
self.version += 1
|
||||
|
||||
def slice(self, start, end):
|
||||
"""
|
||||
Returns a slice of the current playlist, limited by the given
|
||||
start and end positions.
|
||||
|
||||
:param start: position of first track to include in slice
|
||||
:type start: int
|
||||
:param end: position after last track to include in slice
|
||||
:type end: int
|
||||
:rtype: two-tuple of (CPID integer, :class:`mopidy.models.Track`)
|
||||
"""
|
||||
return [copy(cp_track) for cp_track in self._cp_tracks[start:end]]
|
||||
|
||||
def _trigger_playlist_changed(self):
|
||||
logger.debug(u'Triggering playlist changed event')
|
||||
BackendListener.send('playlist_changed')
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import glib
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -37,6 +37,7 @@ class LastfmFrontend(ThreadingActor, BackendListener):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(LastfmFrontend, self).__init__()
|
||||
self.lastfm = None
|
||||
self.last_start_time = None
|
||||
|
||||
|
||||
@ -25,6 +25,7 @@ class MpdFrontend(actor.ThreadingActor, listeners.BackendListener):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(MpdFrontend, self).__init__()
|
||||
hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME)
|
||||
port = settings.MPD_SERVER_PORT
|
||||
|
||||
|
||||
@ -244,7 +244,8 @@ class MpdContext(object):
|
||||
"""
|
||||
if self._backend is None:
|
||||
backend_refs = ActorRegistry.get_by_class(Backend)
|
||||
assert len(backend_refs) == 1, 'Expected exactly one running backend.'
|
||||
assert len(backend_refs) == 1, \
|
||||
'Expected exactly one running backend.'
|
||||
self._backend = backend_refs[0].proxy()
|
||||
return self._backend
|
||||
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError,
|
||||
MpdNotImplemented)
|
||||
from mopidy.frontends.mpd.protocol import handle_request
|
||||
from mopidy.frontends.mpd.translator import tracks_to_mpd_format
|
||||
from mopidy.frontends.mpd.translator import (track_to_mpd_format,
|
||||
tracks_to_mpd_format)
|
||||
|
||||
@handle_request(r'^add "(?P<uri>[^"]*)"$')
|
||||
def add(context, uri):
|
||||
@ -74,8 +75,8 @@ def delete_range(context, start, end=None):
|
||||
if end is not None:
|
||||
end = int(end)
|
||||
else:
|
||||
end = len(context.backend.current_playlist.tracks.get())
|
||||
cp_tracks = context.backend.current_playlist.cp_tracks.get()[start:end]
|
||||
end = context.backend.current_playlist.length.get()
|
||||
cp_tracks = context.backend.current_playlist.slice(start, end).get()
|
||||
if not cp_tracks:
|
||||
raise MpdArgError(u'Bad song index', command=u'delete')
|
||||
for (cpid, _) in cp_tracks:
|
||||
@ -86,7 +87,8 @@ def delete_songpos(context, songpos):
|
||||
"""See :meth:`delete_range`"""
|
||||
try:
|
||||
songpos = int(songpos)
|
||||
(cpid, _) = context.backend.current_playlist.cp_tracks.get()[songpos]
|
||||
(cpid, _) = context.backend.current_playlist.slice(
|
||||
songpos, songpos + 1).get()[0]
|
||||
context.backend.current_playlist.remove(cpid=cpid)
|
||||
except IndexError:
|
||||
raise MpdArgError(u'Bad song index', command=u'delete')
|
||||
@ -157,8 +159,7 @@ def moveid(context, cpid, to):
|
||||
cpid = int(cpid)
|
||||
to = int(to)
|
||||
cp_track = context.backend.current_playlist.get(cpid=cpid).get()
|
||||
position = context.backend.current_playlist.cp_tracks.get().index(
|
||||
cp_track)
|
||||
position = context.backend.current_playlist.index(cp_track).get()
|
||||
context.backend.current_playlist.move(position, position + 1, to)
|
||||
|
||||
@handle_request(r'^playlist$')
|
||||
@ -193,10 +194,8 @@ def playlistfind(context, tag, needle):
|
||||
if tag == 'filename':
|
||||
try:
|
||||
cp_track = context.backend.current_playlist.get(uri=needle).get()
|
||||
(cpid, track) = cp_track
|
||||
position = context.backend.current_playlist.cp_tracks.get().index(
|
||||
cp_track)
|
||||
return track.mpd_format(cpid=cpid, position=position)
|
||||
position = context.backend.current_playlist.index(cp_track).get()
|
||||
return track_to_mpd_format(cp_track, position=position)
|
||||
except LookupError:
|
||||
return None
|
||||
raise MpdNotImplemented # TODO
|
||||
@ -215,18 +214,16 @@ def playlistid(context, cpid=None):
|
||||
try:
|
||||
cpid = int(cpid)
|
||||
cp_track = context.backend.current_playlist.get(cpid=cpid).get()
|
||||
position = context.backend.current_playlist.cp_tracks.get().index(
|
||||
cp_track)
|
||||
return cp_track.track.mpd_format(position=position, cpid=cpid)
|
||||
position = context.backend.current_playlist.index(cp_track).get()
|
||||
return track_to_mpd_format(cp_track, position=position)
|
||||
except LookupError:
|
||||
raise MpdNoExistError(u'No such song', command=u'playlistid')
|
||||
else:
|
||||
cpids = [ct[0] for ct in
|
||||
context.backend.current_playlist.cp_tracks.get()]
|
||||
return tracks_to_mpd_format(
|
||||
context.backend.current_playlist.tracks.get(), cpids=cpids)
|
||||
context.backend.current_playlist.cp_tracks.get())
|
||||
|
||||
@handle_request(r'^playlistinfo$')
|
||||
@handle_request(r'^playlistinfo "-1"$')
|
||||
@handle_request(r'^playlistinfo "(?P<songpos>-?\d+)"$')
|
||||
@handle_request(r'^playlistinfo "(?P<start>\d+):(?P<end>\d+)*"$')
|
||||
def playlistinfo(context, songpos=None,
|
||||
@ -245,36 +242,22 @@ def playlistinfo(context, songpos=None,
|
||||
- uses negative indexes, like ``playlistinfo "-1"``, to request
|
||||
the entire playlist
|
||||
"""
|
||||
if songpos == "-1":
|
||||
songpos = None
|
||||
|
||||
if songpos is not None:
|
||||
songpos = int(songpos)
|
||||
start = songpos
|
||||
end = songpos + 1
|
||||
if start == -1:
|
||||
end = None
|
||||
cpids = [ct[0] for ct in
|
||||
context.backend.current_playlist.cp_tracks.get()]
|
||||
return tracks_to_mpd_format(
|
||||
context.backend.current_playlist.tracks.get(),
|
||||
start, end, cpids=cpids)
|
||||
cp_track = context.backend.current_playlist.get(cpid=songpos).get()
|
||||
return track_to_mpd_format(cp_track, position=songpos)
|
||||
else:
|
||||
if start is None:
|
||||
start = 0
|
||||
start = int(start)
|
||||
if not (0 <= start <= len(
|
||||
context.backend.current_playlist.tracks.get())):
|
||||
if not (0 <= start <= context.backend.current_playlist.length.get()):
|
||||
raise MpdArgError(u'Bad song index', command=u'playlistinfo')
|
||||
if end is not None:
|
||||
end = int(end)
|
||||
if end > len(context.backend.current_playlist.tracks.get()):
|
||||
if end > context.backend.current_playlist.length.get():
|
||||
end = None
|
||||
cpids = [ct[0] for ct in
|
||||
context.backend.current_playlist.cp_tracks.get()]
|
||||
return tracks_to_mpd_format(
|
||||
context.backend.current_playlist.tracks.get(),
|
||||
start, end, cpids=cpids)
|
||||
cp_tracks = context.backend.current_playlist.cp_tracks.get()
|
||||
return tracks_to_mpd_format(cp_tracks, start, end)
|
||||
|
||||
@handle_request(r'^playlistsearch "(?P<tag>[^"]+)" "(?P<needle>[^"]+)"$')
|
||||
@handle_request(r'^playlistsearch (?P<tag>\S+) "(?P<needle>[^"]+)"$')
|
||||
@ -313,10 +296,8 @@ def plchanges(context, version):
|
||||
"""
|
||||
# XXX Naive implementation that returns all tracks as changed
|
||||
if int(version) < context.backend.current_playlist.version:
|
||||
cpids = [ct[0] for ct in
|
||||
context.backend.current_playlist.cp_tracks.get()]
|
||||
return tracks_to_mpd_format(
|
||||
context.backend.current_playlist.tracks.get(), cpids=cpids)
|
||||
context.backend.current_playlist.cp_tracks.get())
|
||||
|
||||
@handle_request(r'^plchangesposid "(?P<version>\d+)"$')
|
||||
def plchangesposid(context, version):
|
||||
@ -392,7 +373,6 @@ def swapid(context, cpid1, cpid2):
|
||||
cpid2 = int(cpid2)
|
||||
cp_track1 = context.backend.current_playlist.get(cpid=cpid1).get()
|
||||
cp_track2 = context.backend.current_playlist.get(cpid=cpid2).get()
|
||||
cp_tracks = context.backend.current_playlist.cp_tracks.get()
|
||||
position1 = cp_tracks.index(cp_track1)
|
||||
position2 = cp_tracks.index(cp_track2)
|
||||
position1 = context.backend.current_playlist.index(cp_track1).get()
|
||||
position2 = context.backend.current_playlist.index(cp_track2).get()
|
||||
swap(context, position1, position2)
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -57,6 +57,7 @@ class MprisFrontend(ThreadingActor, BackendListener):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(MprisFrontend, self).__init__()
|
||||
self.indicate_server = None
|
||||
self.mpris_object = None
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -13,15 +13,6 @@ from mopidy.backends.base import Backend
|
||||
|
||||
logger = logging.getLogger('mopidy.gstreamer')
|
||||
|
||||
default_caps = gst.Caps("""
|
||||
audio/x-raw-int,
|
||||
endianness=(int)1234,
|
||||
channels=(int)2,
|
||||
width=(int)16,
|
||||
depth=(int)16,
|
||||
signed=(boolean)true,
|
||||
rate=(int)44100""")
|
||||
|
||||
|
||||
class GStreamer(ThreadingActor):
|
||||
"""
|
||||
@ -34,6 +25,15 @@ class GStreamer(ThreadingActor):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(GStreamer, self).__init__()
|
||||
self._default_caps = gst.Caps("""
|
||||
audio/x-raw-int,
|
||||
endianness=(int)1234,
|
||||
channels=(int)2,
|
||||
width=(int)16,
|
||||
depth=(int)16,
|
||||
signed=(boolean)true,
|
||||
rate=(int)44100""")
|
||||
self._pipeline = None
|
||||
self._source = None
|
||||
self._tee = None
|
||||
@ -77,12 +77,14 @@ class GStreamer(ThreadingActor):
|
||||
def _on_new_source(self, element, pad):
|
||||
self._source = element.get_property('source')
|
||||
try:
|
||||
self._source.set_property('caps', default_caps)
|
||||
self._source.set_property('caps', self._default_caps)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
def _on_new_pad(self, source, pad, target_pad):
|
||||
if not pad.is_linked():
|
||||
if target_pad.is_linked():
|
||||
target_pad.get_peer().unlink(target_pad)
|
||||
pad.link(target_pad)
|
||||
|
||||
def _on_message(self, bus, message):
|
||||
@ -333,7 +335,8 @@ class GStreamer(ThreadingActor):
|
||||
self._tee.send_event(event)
|
||||
|
||||
def _handle_event_probe(self, teesrc, event):
|
||||
if event.type == gst.EVENT_CUSTOM_DOWNSTREAM and event.has_name('mopidy-unlink-tee'):
|
||||
if (event.type == gst.EVENT_CUSTOM_DOWNSTREAM
|
||||
and event.has_name('mopidy-unlink-tee')):
|
||||
data = self._get_structure_data(event.get_structure())
|
||||
|
||||
output = teesrc.get_peer().get_parent()
|
||||
|
||||
@ -23,6 +23,7 @@ class AlsaMixer(ThreadingActor, BaseMixer):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(AlsaMixer, self).__init__()
|
||||
self._mixer = None
|
||||
|
||||
def on_start(self):
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -297,6 +297,7 @@ class LineProtocol(ThreadingActor):
|
||||
encoding = 'utf-8'
|
||||
|
||||
def __init__(self, connection):
|
||||
super(LineProtocol, self).__init__()
|
||||
self.connection = connection
|
||||
self.prevent_timeout = False
|
||||
self.recv_buffer = ''
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
3
pylintrc
3
pylintrc
@ -18,6 +18,7 @@
|
||||
# R0921 - Abstract class not referenced
|
||||
# W0141 - Used builtin function '%s'
|
||||
# W0142 - Used * or ** magic
|
||||
# W0511 - TODO, FIXME and XXX in the code
|
||||
# W0613 - Unused argument %r
|
||||
#
|
||||
disable = C0103,C0111,E0102,E0202,E1101,R0201,R0801,R0903,R0904,R0921,W0141,W0142,W0613
|
||||
disable = C0103,C0111,E0102,E0202,E1101,R0201,R0801,R0903,R0904,R0921,W0141,W0142,W0511,W0613
|
||||
|
||||
@ -2,4 +2,5 @@ coverage
|
||||
mock >= 0.7
|
||||
nose
|
||||
tox
|
||||
unittest2
|
||||
yappi
|
||||
|
||||
@ -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([])
|
||||
|
||||
@ -271,11 +271,17 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase):
|
||||
|
||||
self.sendRequest(u'playlistinfo')
|
||||
self.assertInResponse(u'Title: a')
|
||||
self.assertInResponse(u'Pos: 0')
|
||||
self.assertInResponse(u'Title: b')
|
||||
self.assertInResponse(u'Pos: 1')
|
||||
self.assertInResponse(u'Title: c')
|
||||
self.assertInResponse(u'Pos: 2')
|
||||
self.assertInResponse(u'Title: d')
|
||||
self.assertInResponse(u'Pos: 3')
|
||||
self.assertInResponse(u'Title: e')
|
||||
self.assertInResponse(u'Pos: 4')
|
||||
self.assertInResponse(u'Title: f')
|
||||
self.assertInResponse(u'Pos: 5')
|
||||
self.assertInResponse(u'OK')
|
||||
|
||||
def test_playlistinfo_with_songpos(self):
|
||||
@ -286,11 +292,17 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase):
|
||||
|
||||
self.sendRequest(u'playlistinfo "4"')
|
||||
self.assertNotInResponse(u'Title: a')
|
||||
self.assertNotInResponse(u'Pos: 0')
|
||||
self.assertNotInResponse(u'Title: b')
|
||||
self.assertNotInResponse(u'Pos: 1')
|
||||
self.assertNotInResponse(u'Title: c')
|
||||
self.assertNotInResponse(u'Pos: 2')
|
||||
self.assertNotInResponse(u'Title: d')
|
||||
self.assertNotInResponse(u'Pos: 3')
|
||||
self.assertInResponse(u'Title: e')
|
||||
self.assertInResponse(u'Pos: 4')
|
||||
self.assertNotInResponse(u'Title: f')
|
||||
self.assertNotInResponse(u'Pos: 5')
|
||||
self.assertInResponse(u'OK')
|
||||
|
||||
def test_playlistinfo_with_negative_songpos_same_as_playlistinfo(self):
|
||||
@ -306,11 +318,17 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase):
|
||||
|
||||
self.sendRequest(u'playlistinfo "2:"')
|
||||
self.assertNotInResponse(u'Title: a')
|
||||
self.assertNotInResponse(u'Pos: 0')
|
||||
self.assertNotInResponse(u'Title: b')
|
||||
self.assertNotInResponse(u'Pos: 1')
|
||||
self.assertInResponse(u'Title: c')
|
||||
self.assertInResponse(u'Pos: 2')
|
||||
self.assertInResponse(u'Title: d')
|
||||
self.assertInResponse(u'Pos: 3')
|
||||
self.assertInResponse(u'Title: e')
|
||||
self.assertInResponse(u'Pos: 4')
|
||||
self.assertInResponse(u'Title: f')
|
||||
self.assertInResponse(u'Pos: 5')
|
||||
self.assertInResponse(u'OK')
|
||||
|
||||
def test_playlistinfo_with_closed_range(self):
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -23,8 +23,9 @@ class VersionTest(unittest.TestCase):
|
||||
self.assert_(SV('0.4.0') < SV('0.4.1'))
|
||||
self.assert_(SV('0.4.1') < SV('0.5.0'))
|
||||
self.assert_(SV('0.5.0') < SV('0.6.0'))
|
||||
self.assert_(SV('0.6.0') < SV(get_plain_version()))
|
||||
self.assert_(SV(get_plain_version()) < SV('0.7.0'))
|
||||
self.assert_(SV('0.6.0') < SV('0.6.1'))
|
||||
self.assert_(SV('0.6.1') < SV(get_plain_version()))
|
||||
self.assert_(SV(get_plain_version()) < SV('0.7.1'))
|
||||
|
||||
def test_get_platform_contains_platform(self):
|
||||
self.assert_(platform.platform() in get_platform())
|
||||
|
||||
Loading…
Reference in New Issue
Block a user