Merge branch 'develop' into feature/gstreamer-playlists

This commit is contained in:
Thomas Adamcik 2013-10-06 14:00:45 +02:00
commit 42a05458a0
79 changed files with 1390 additions and 863 deletions

View File

@ -5,3 +5,6 @@ Kristian Klette <klette@samfundet.no>
Johannes Knutsen <johannes@knutseninfo.no> <johannes@iterate.no>
Johannes Knutsen <johannes@knutseninfo.no> <johannes@barbarmaclin.(none)>
John Bäckstrand <sopues@gmail.com> <sandos@XBMCLive.(none)>
Alli Witheford <alzeih@gmail.com>
Alexandre Petitjean <alpetitjean@gmail.com>
Alexandre Petitjean <alpetitjean@gmail.com> <alexandre.petitjean@lne.fr>

View File

@ -1,15 +1,18 @@
language: python
install:
- "wget -q -O - http://apt.mopidy.com/mopidy.gpg | sudo apt-key add -"
- "sudo wget -q -O /etc/apt/sources.list.d/mopidy.list http://apt.mopidy.com/mopidy.list"
- "wget -O - http://apt.mopidy.com/mopidy.gpg | sudo apt-key add -"
- "sudo wget -O /etc/apt/sources.list.d/mopidy.list http://apt.mopidy.com/mopidy.list"
- "sudo apt-get update || true"
- "sudo apt-get install $(apt-cache depends mopidy | awk '$2 !~ /mopidy/ {print $2}')"
- "pip install flake8"
before_script:
- "rm $VIRTUAL_ENV/lib/python$TRAVIS_PYTHON_VERSION/no-global-site-packages.txt"
script: nosetests
script:
- "flake8 $(find . -iname '*.py')"
- "nosetests"
notifications:
irc:

View File

@ -19,3 +19,8 @@
- Nick Steel <kingosticks@gmail.com>
- Zan Dobersek <zandobersek@gmail.com>
- Thomas Refis <refis.thomas@gmail.com>
- Janez Troha <janez.troha@gmail.com>
- Tobias Sauerwein <cgtobi@gmail.com>
- Alli Witheford <alzeih@gmail.com>
- Alexandre Petitjean <alpetitjean@gmail.com>
- Pavol Babincak <scroolik@gmail.com>

View File

@ -25,4 +25,13 @@ To get started with Mopidy, check out `the docs <http://docs.mopidy.com/>`_.
- Mailing list: `mopidy@googlegroups.com <https://groups.google.com/forum/?fromgroups=#!forum/mopidy>`_
- Twitter: `@mopidy <https://twitter.com/mopidy/>`_
.. image:: https://secure.travis-ci.org/mopidy/mopidy.png?branch=develop
.. image:: https://travis-ci.org/mopidy/mopidy.png?branch=develop
:target: https://travis-ci.org/mopidy/mopidy
.. image:: https://pypip.in/v/Mopidy/badge.png
:target: https://crate.io/packages/Mopidy/
:alt: Latest PyPI version
.. image:: https://pypip.in/d/Mopidy/badge.png
:target: https://crate.io/packages/Mopidy/
:alt: Number of PyPI downloads

View File

@ -4,15 +4,16 @@ Changelog
This changelog is used to track all major changes to Mopidy.
v0.15.0 (UNRELEASED)
v0.15.0 (2013-09-19)
====================
(no description yet)
A release with a number of small and medium fixes, with no specific focus.
**Dependencies**
- Mopidy no longer supports Python 2.6. Currently, the only Python version
supported by Mopidy is Python 2.7. (Fixes: :issue:`344`)
supported by Mopidy is Python 2.7. We're continuously working towards running
Mopidy on Python 3. (Fixes: :issue:`344`)
**Command line options**
@ -20,7 +21,76 @@ v0.15.0 (UNRELEASED)
options.
- :option:`mopidy --show-config` will now take into consideration any
:option:`mopidy --option` arguments appearing later on the command line.
:option:`mopidy --option` arguments appearing later on the command line. This
helps you see the effective configuration for runs with the same
:option:`mopidy --options` arguments.
**Audio**
- Added support for audio visualization. :confval:`audio/visualizer` can now be
set to GStreamer visualizers.
- Properly encode localized mixer names before logging.
**Local backend**
- An album's number of discs and a track's disc number are now extracted when
scanning your music collection.
- The scanner now gives up scanning a file after a second, and continues with
the next file. This fixes some hangs on non-media files, like logs. (Fixes:
:issue:`476`, :issue:`483`)
- Added support for pluggable library updaters. This allows extension writers
to start providing their own custom libraries instead of being stuck with
just our tag cache as the only option.
- Converted local backend to use new ``local:playlist:path`` and
``local:track:path`` URI scheme. Also moves support of ``file://`` to
streaming backend.
**Spotify backend**
- Prepend playlist folder names to the playlist name, so that the playlist
hierarchy from your Spotify account is available in Mopidy. (Fixes:
:issue:`62`)
- Fix proxy config values that was broken with the config system change in
0.14. (Fixes: :issue:`472`)
**MPD frontend**
- Replace newline, carriage return and forward slash in playlist names. (Fixes:
:issue:`474`, :issue:`480`)
- Accept ``listall`` and ``listallinfo`` commands without the URI parameter.
The methods are still not implemented, but now the commands are accepted as
valid.
**HTTP frontend**
- Fix too broad truth test that caused :class:`mopidy.models.TlTrack`
objects with ``tlid`` set to ``0`` to be sent to the HTTP client without the
``tlid`` field. (Fixes: :issue:`501`)
- Upgrade Mopidy.js dependencies. This version has been released to NPM as
Mopidy.js v0.1.1.
**Extension support**
- :class:`mopidy.config.Secret` is now deserialized to unicode instead of
bytes. This may require modifications to extensions.
v0.14.2 (2013-07-01)
====================
This is a maintenance release to make Mopidy 0.14 work with pyspotify 1.11.
**Dependencies**
- pyspotify >= 1.9, < 2 is now required for Spotify support. In other words,
you're free to upgrade to pyspotify 1.11, but it isn't a requirement.
v0.14.1 (2013-04-28)

View File

@ -33,7 +33,10 @@ class Mock(object):
if name in ('__file__', '__path__'):
return '/dev/null'
elif (name[0] == name[0].upper()
and not name.startswith('MIXER_TRACK_')):
# gst.interfaces.MIXER_TRACK_*
and not name.startswith('MIXER_TRACK_')
# dbus.String()
and not name == 'String'):
return type(name, (), {})
else:
return Mock()
@ -98,7 +101,7 @@ master_doc = 'index'
# General information about the project.
project = 'Mopidy'
copyright = '2010-2013, Stein Magnus Jodal and contributors'
copyright = '2009-2013, 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

View File

@ -90,6 +90,16 @@ Core configuration values
``gst-inspect-0.10`` to see what output properties can be set on the sink.
For example: ``gst-inspect-0.10 shout2send``
.. confval:: audio/visualizer
Visualizer to use.
Can be left blank if no visualizer is desired. Otherwise this expects a
GStreamer visualizer. Typical values are ``monoscope``, ``goom``,
``goom2k1`` or one of the `libvisual`_ visualizers.
.. _libvisual: http://gstreamer.freedesktop.org/data/doc/gstreamer/head/gst-plugins-base-plugins/html/gst-plugins-base-plugins-plugin-libvisual.html
.. confval:: logging/console_format
The log format used for informational logging.

View File

@ -22,8 +22,8 @@ tested by Jenkins before it is merged into the ``develop`` branch, which is a
bit late, but good enough to get broad testing before new code is released.
In addition to running tests, the Jenkins CI server also gathers coverage
statistics and uses pylint to check for errors and possible improvements in our
code. So, if you're out of work, the code coverage and pylint data at the CI
statistics and uses flake8 to check for errors and possible improvements in our
code. So, if you're out of work, the code coverage and flake8 data at the CI
server should give you a place to start.

View File

@ -46,6 +46,22 @@ Issues:
https://github.com/dz0ny/mopidy-beets/issues
Mopidy-GMusic
-------------
Provides a backend for playing music from `Google Play Music
<https://play.google.com/music/>`_.
Author:
Ronald Hecht
PyPI:
`Mopidy-GMusic <https://pypi.python.org/pypi/Mopidy-GMusic>`_
GitHub:
`hechtus/mopidy-gmusic <https://github.com/hechtus/mopidy-gmusic>`_
Issues:
https://github.com/hechtus/mopidy-gmusic/issues
Mopidy-NAD
----------
@ -61,6 +77,22 @@ Issues:
https://github.com/mopidy/mopidy/issues
Mopidy-SomaFM
-------------
Provides a backend for playing music from the `SomaFM <http://somafm.com/>`_
service.
Author:
Alexandre Petitjean
PyPI:
`Mopidy-SomaFM <https://pypi.python.org/pypi/Mopidy-SomaFM>`_
GitHub:
`AlexandrePTJ/mopidy-somafm <https://github.com/AlexandrePTJ/mopidy-somafm/>`_
Issues:
https://github.com/AlexandrePTJ/mopidy-somafm/issues
Mopidy-SoundCloud
-----------------
@ -75,3 +107,19 @@ GitHub:
`dz0ny/mopidy-soundcloud <https://github.com/dz0ny/mopidy-soundcloud>`_
Issues:
https://github.com/dz0ny/mopidy-soundcloud/issues
Mopidy-Subsonic
---------------
Provides a backend for playing music from a `Subsonic Music Streamer
<http://www.subsonic.org/>`_ library.
Author:
Bradon Kanyid
PyPI:
`Mopidy-Subsonic <https://pypi.python.org/pypi/Mopidy-Subsonic>`_
GitHub:
`rattboi/mopidy-subsonic <https://github.com/rattboi/mopidy-subsonic>`_
Issues:
https://github.com/rattboi/mopidy-subsonic/issues

View File

@ -47,6 +47,11 @@ Configuration values
Path to tag cache for local media.
.. confval:: local/scan_timeout
Number of milliseconds before giving up scanning a file and moving on to
the next file.
Usage
=====

View File

@ -48,7 +48,7 @@ About
:maxdepth: 1
authors
licenses
license
changelog
versioning

View File

@ -16,12 +16,20 @@ distribution.
.. _raspi-wheezy:
How to for Debian 7 (Wheezy)
============================
How to for Raspbian "wheezy" and Debian "wheezy"
================================================
#. Download the latest wheezy disk image from
http://downloads.raspberrypi.org/images/debian/7/. I used the one dated
2012-08-08.
This guide applies for both:
- Raspian "wheezy" for armhf (hard-float), and
- Debian "wheezy" for armel (soft-float)
If you don't know which one to select, go for the armhf variant, as it'll give
you a lot better performance.
#. Download the latest "wheezy" disk image from
http://www.raspberrypi.org/downloads/. This was last tested with the images
from 2013-05-25 for armhf and 2013-05-29 for armel.
#. Flash the OS image to your SD card. See
http://elinux.org/RPi_Easy_SD_Card_Setup for help.
@ -82,10 +90,10 @@ card.
#. Ensure your system is up to date. On Debian based systems run::
sudo apt-get update
sudo apt-get full-upgrade
sudo apt-get dist-upgrade
#. Ensure you have a new enough firmware. On Debian based systems
`rpi-update <http://apt.mopidy.com://github.com/Hexxeh/rpi-update>`_
`rpi-update <https://github.com/Hexxeh/rpi-update>`_
can be used.
#. Update either ``~/.asoundrc`` or ``/etc/asound.conf`` to the

10
docs/license.rst Normal file
View File

@ -0,0 +1,10 @@
*******
License
*******
Mopidy is copyright 2009-2013 Stein Magnus Jodal and contributors. For a list
of contributors, see :doc:`authors`. For details on who have contributed what,
please refer to our git repository.
Mopidy is licensed under the `Apache License, Version 2.0
<http://www.apache.org/licenses/LICENSE-2.0>`_.

View File

@ -1,34 +0,0 @@
********
Licenses
********
For a list of contributors, see :doc:`authors`. For details on who have
contributed what, please refer to our git repository.
Source code license
===================
Copyright 2009-2013 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.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Documentation license
=====================
Copyright 2010-2013 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
http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative
Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA.

47
fabfile.py vendored
View File

@ -1,21 +1,62 @@
from fabric.api import local, settings
from fabric.api import execute, local, settings, task
@task
def docs():
local('make -C docs/ html')
@task
def autodocs():
auto(docs)
@task
def test(path=None):
path = path or 'tests/'
local('nosetests ' + path)
@task
def autotest(path=None):
auto(test, path=path)
@task
def coverage(path=None):
path = path or 'tests/'
local(
'nosetests --with-coverage --cover-package=mopidy '
'--cover-branches --cover-html ' + path)
@task
def autocoverage(path=None):
auto(coverage, path=path)
@task
def lint(path=None):
path = path or '.'
local('flake8 $(find %s -iname "*.py")' % path)
@task
def autolint(path=None):
auto(lint, path=path)
def auto(task, *args, **kwargs):
while True:
local('clear')
with settings(warn_only=True):
test(path)
execute(task, *args, **kwargs)
local(
'inotifywait -q -e create -e modify -e delete '
'--exclude ".*\.(pyc|sw.)" -r mopidy/ tests/')
'--exclude ".*\.(pyc|sw.)" -r docs/ mopidy/ tests/')
@task
def update_authors():
# Keep authors in the order of appearance and use awk to filter out dupes
local(

View File

@ -51,15 +51,15 @@ Building from source
1. Install [Node.js](http://nodejs.org/) and npm. There is a PPA if you're
running Ubuntu:
sudo apt-get install python-software-properties
sudo add-apt-repository ppa:chris-lea/node.js
sudo apt-get update
sudo apt-get install nodejs npm
sudo apt-get install python-software-properties
sudo add-apt-repository ppa:chris-lea/node.js
sudo apt-get update
sudo apt-get install nodejs
2. Enter the `js/` in Mopidy's Git repo dir and install all dependencies:
cd js/
npm install
cd js/
npm install
That's it.

View File

@ -3,10 +3,10 @@
*
* https://github.com/busterjs/bane
*
* @version 0.4.0
* @version 1.0.0
*/
((typeof define === "function" && define.amd && function (m) { define(m); }) ||
((typeof define === "function" && define.amd && function (m) { define("bane", m); }) ||
(typeof module === "object" && function (m) { module.exports = m(); }) ||
function (m) { this.bane = m(); }
)(function () {
@ -152,7 +152,7 @@
notifyListener(event, toNotify[i], args);
}
toNotify = listeners(this, event).slice()
toNotify = listeners(this, event).slice();
args = slice.call(arguments, 1);
for (i = 0, l = toNotify.length; i < l; ++i) {
notifyListener(event, toNotify[i], args);

View File

@ -9,27 +9,30 @@
*
* @author Brian Cavalier
* @author John Hann
* @version 2.0.0
* @version 2.4.0
*/
(function(define) { 'use strict';
define(function () {
(function(define, global) { 'use strict';
define(function (require) {
// Public API
when.defer = defer; // Create a deferred
when.promise = promise; // Create a pending promise
when.resolve = resolve; // Create a resolved promise
when.reject = reject; // Create a rejected promise
when.defer = defer; // Create a {promise, resolver} pair
when.join = join; // Join 2 or more promises
when.all = all; // Resolve a list of promises
when.map = map; // Array.map() for promises
when.reduce = reduce; // Array.reduce() for promises
when.settle = settle; // Settle a list of promises
when.any = any; // One-winner race
when.some = some; // Multi-winner race
when.isPromise = isPromise; // Determine if a thing is a promise
when.isPromise = isPromiseLike; // DEPRECATED: use isPromiseLike
when.isPromiseLike = isPromiseLike; // Is something promise-like, aka thenable
/**
* Register an observer for a promise or immediate value.
@ -57,13 +60,35 @@ define(function () {
* a trusted when.js promise. Any other duck-typed promise is considered
* untrusted.
* @constructor
* @param {function} sendMessage function to deliver messages to the promise's handler
* @param {function?} inspect function that reports the promise's state
* @name Promise
*/
function Promise(then) {
this.then = then;
function Promise(sendMessage, inspect) {
this._message = sendMessage;
this.inspect = inspect;
}
Promise.prototype = {
/**
* Register handlers for this promise.
* @param [onFulfilled] {Function} fulfillment handler
* @param [onRejected] {Function} rejection handler
* @param [onProgress] {Function} progress handler
* @return {Promise} new Promise
*/
then: function(onFulfilled, onRejected, onProgress) {
/*jshint unused:false*/
var args, sendMessage;
args = arguments;
sendMessage = this._message;
return _promise(function(resolve, reject, notify) {
sendMessage('when', args, resolve, notify);
}, this._status && this._status.observed());
},
/**
* Register a rejection handler. Shortcut for .then(undefined, onRejected)
* @param {function?} onRejected
@ -84,9 +109,7 @@ define(function () {
* @returns {Promise}
*/
ensure: function(onFulfilledOrRejected) {
var self = this;
return this.then(injectHandler, injectHandler).yield(self);
return this.then(injectHandler, injectHandler)['yield'](this);
function injectHandler() {
return resolve(onFulfilledOrRejected());
@ -107,6 +130,16 @@ define(function () {
});
},
/**
* Runs a side effect when this promise fulfills, without changing the
* fulfillment value.
* @param {function} onFulfilledSideEffect
* @returns {Promise}
*/
tap: function(onFulfilledSideEffect) {
return this.then(onFulfilledSideEffect)['yield'](this);
},
/**
* Assumes that this promise will fulfill with an array, and arranges
* for the onFulfilled to be called with the array as its argument list
@ -162,13 +195,16 @@ define(function () {
}
/**
* Creates a new Deferred with fully isolated resolver and promise parts,
* either or both of which may be given out safely to consumers.
* Creates a {promise, resolver} pair, either or both of which
* may be given out safely to consumers.
* The resolver has resolve, reject, and progress. The promise
* only has then.
* has then plus extended promise API.
*
* @return {{
* promise: Promise,
* resolve: function:Promise,
* reject: function:Promise,
* notify: function:Promise
* resolver: {
* resolve: function:Promise,
* reject: function:Promise,
@ -216,12 +252,26 @@ define(function () {
/**
* Creates a new promise whose fate is determined by resolver.
* @private (for now)
* @param {function} resolver function(resolve, reject, notify)
* @returns {Promise} promise whose fate is determine by resolver
*/
function promise(resolver) {
var value, handlers = [];
return _promise(resolver, monitorApi.PromiseStatus && monitorApi.PromiseStatus());
}
/**
* Creates a new promise, linked to parent, whose fate is determined
* by resolver.
* @param {function} resolver function(resolve, reject, notify)
* @param {Promise?} status promise from which the new promise is begotten
* @returns {Promise} promise whose fate is determine by resolver
* @private
*/
function _promise(resolver, status) {
var self, value, consumers = [];
self = new Promise(_message, inspect);
self._status = status;
// Call the provider resolver to seal the promise's fate
try {
@ -231,29 +281,34 @@ define(function () {
}
// Return the promise
return new Promise(then);
return self;
/**
* Register handlers for this promise.
* @param [onFulfilled] {Function} fulfillment handler
* @param [onRejected] {Function} rejection handler
* @param [onProgress] {Function} progress handler
* @return {Promise} new Promise
* Private message delivery. Queues and delivers messages to
* the promise's ultimate fulfillment value or rejection reason.
* @private
* @param {String} type
* @param {Array} args
* @param {Function} resolve
* @param {Function} notify
*/
function then(onFulfilled, onRejected, onProgress) {
return promise(function(resolve, reject, notify) {
handlers
// Call handlers later, after resolution
? handlers.push(function(value) {
value.then(onFulfilled, onRejected, onProgress)
.then(resolve, reject, notify);
})
// Call handlers soon, but not in the current stack
: enqueue(function() {
value.then(onFulfilled, onRejected, onProgress)
.then(resolve, reject, notify);
});
});
function _message(type, args, resolve, notify) {
consumers ? consumers.push(deliver) : enqueue(function() { deliver(value); });
function deliver(p) {
p._message(type, args, resolve, notify);
}
}
/**
* Returns a snapshot of the promise's state at the instant inspect()
* is called. The returned object is not live and will not update as
* the promise's state changes.
* @returns {{ state:String, value?:*, reason?:* }} status snapshot
* of the promise.
*/
function inspect() {
return value ? value.inspect() : toPendingState();
}
/**
@ -262,14 +317,17 @@ define(function () {
* @param {*|Promise} val resolution value
*/
function promiseResolve(val) {
if(!handlers) {
if(!consumers) {
return;
}
value = coerce(val);
scheduleHandlers(handlers, value);
scheduleConsumers(consumers, value);
consumers = undef;
handlers = undef;
if(status) {
updateStatus(value, status);
}
}
/**
@ -285,27 +343,90 @@ define(function () {
* @param {*} update progress event payload to pass to all listeners
*/
function promiseNotify(update) {
if(handlers) {
scheduleHandlers(handlers, progressing(update));
if(consumers) {
scheduleConsumers(consumers, progressed(update));
}
}
}
/**
* Creates a fulfilled, local promise as a proxy for a value
* NOTE: must never be exposed
* @param {*} value fulfillment value
* @returns {Promise}
*/
function fulfilled(value) {
return near(
new NearFulfilledProxy(value),
function() { return toFulfilledState(value); }
);
}
/**
* Creates a rejected, local promise with the supplied reason
* NOTE: must never be exposed
* @param {*} reason rejection reason
* @returns {Promise}
*/
function rejected(reason) {
return near(
new NearRejectedProxy(reason),
function() { return toRejectedState(reason); }
);
}
/**
* Creates a near promise using the provided proxy
* NOTE: must never be exposed
* @param {object} proxy proxy for the promise's ultimate value or reason
* @param {function} inspect function that returns a snapshot of the
* returned near promise's state
* @returns {Promise}
*/
function near(proxy, inspect) {
return new Promise(function (type, args, resolve) {
try {
resolve(proxy[type].apply(proxy, args));
} catch(e) {
resolve(rejected(e));
}
}, inspect);
}
/**
* Create a progress promise with the supplied update.
* @private
* @param {*} update
* @return {Promise} progress promise
*/
function progressed(update) {
return new Promise(function (type, args, _, notify) {
var onProgress = args[2];
try {
notify(typeof onProgress === 'function' ? onProgress(update) : update);
} catch(e) {
notify(e);
}
});
}
/**
* Coerces x to a trusted Promise
*
* @private
* @param {*} x thing to coerce
* @returns {Promise} Guaranteed to return a trusted Promise. If x
* @returns {*} Guaranteed to return a trusted Promise. If x
* is trusted, returns x, otherwise, returns a new, trusted, already-resolved
* Promise whose resolution value is:
* * the resolution value of x if it's a foreign promise, or
* * x if it's a value
*/
function coerce(x) {
if(x instanceof Promise) {
if (x instanceof Promise) {
return x;
} else if (x !== Object(x)) {
}
if (!(x === Object(x) && 'then' in x)) {
return fulfilled(x);
}
@ -332,61 +453,34 @@ define(function () {
}
/**
* Create an already-fulfilled promise for the supplied value
* @private
* Proxy for a near, fulfilled value
* @param {*} value
* @return {Promise} fulfilled promise
* @constructor
*/
function fulfilled(value) {
var self = new Promise(function (onFulfilled) {
try {
return typeof onFulfilled == 'function'
? coerce(onFulfilled(value)) : self;
} catch (e) {
return rejected(e);
}
});
return self;
function NearFulfilledProxy(value) {
this.value = value;
}
NearFulfilledProxy.prototype.when = function(onResult) {
return typeof onResult === 'function' ? onResult(this.value) : this.value;
};
/**
* Create an already-rejected promise with the supplied rejection reason.
* @private
* Proxy for a near rejection
* @param {*} reason
* @return {Promise} rejected promise
* @constructor
*/
function rejected(reason) {
var self = new Promise(function (_, onRejected) {
try {
return typeof onRejected == 'function'
? coerce(onRejected(reason)) : self;
} catch (e) {
return rejected(e);
}
});
return self;
function NearRejectedProxy(reason) {
this.reason = reason;
}
/**
* Create a progress promise with the supplied update.
* @private
* @param {*} update
* @return {Promise} progress promise
*/
function progressing(update) {
var self = new Promise(function (_, __, onProgress) {
try {
return typeof onProgress == 'function'
? progressing(onProgress(update)) : self;
} catch (e) {
return progressing(e);
}
});
return self;
}
NearRejectedProxy.prototype.when = function(_, onError) {
if(typeof onError === 'function') {
return onError(this.reason);
} else {
throw this.reason;
}
};
/**
* Schedule a task that will process a list of handlers
@ -395,7 +489,7 @@ define(function () {
* @param {Array} handlers queue of handlers to execute
* @param {*} value passed as the only arg to each handler
*/
function scheduleHandlers(handlers, value) {
function scheduleConsumers(handlers, value) {
enqueue(function() {
var handler, i = 0;
while (handler = handlers[i++]) {
@ -404,14 +498,23 @@ define(function () {
});
}
function updateStatus(value, status) {
value.then(statusFulfilled, statusRejected);
function statusFulfilled() { status.fulfilled(); }
function statusRejected(r) { status.rejected(r); }
}
/**
* Determines if promiseOrValue is a promise or not
*
* @param {*} promiseOrValue anything
* @returns {boolean} true if promiseOrValue is a {@link Promise}
* Determines if x is promise-like, i.e. a thenable object
* NOTE: Will return true for *any thenable object*, and isn't truly
* safe, since it may attempt to access the `then` property of x (i.e.
* clever/malicious getters may do weird things)
* @param {*} x anything
* @returns {boolean} true if x is promise-like
*/
function isPromise(promiseOrValue) {
return promiseOrValue && typeof promiseOrValue.then === 'function';
function isPromiseLike(x) {
return x && typeof x.then === 'function';
}
/**
@ -423,17 +526,15 @@ define(function () {
* @param {Array} promisesOrValues array of anything, may contain a mix
* of promises and values
* @param howMany {number} number of promisesOrValues to resolve
* @param {function?} [onFulfilled] resolution handler
* @param {function?} [onRejected] rejection handler
* @param {function?} [onProgress] progress handler
* @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then()
* @param {function?} [onRejected] DEPRECATED, use returnedPromise.then()
* @param {function?} [onProgress] DEPRECATED, use returnedPromise.then()
* @returns {Promise} promise that will resolve to an array of howMany values that
* resolved first, or will reject with an array of
* (promisesOrValues.length - howMany) + 1 rejection reasons.
*/
function some(promisesOrValues, howMany, onFulfilled, onRejected, onProgress) {
checkCallbacks(2, arguments);
return when(promisesOrValues, function(promisesOrValues) {
return promise(resolveSome).then(onFulfilled, onRejected, onProgress);
@ -457,7 +558,7 @@ define(function () {
rejectOne = function(reason) {
reasons.push(reason);
if(!--toReject) {
fulfillOne = rejectOne = noop;
fulfillOne = rejectOne = identity;
reject(reasons);
}
};
@ -466,7 +567,7 @@ define(function () {
// This orders the values based on promise resolution order
values.push(val);
if (!--toResolve) {
fulfillOne = rejectOne = noop;
fulfillOne = rejectOne = identity;
resolve(values);
}
};
@ -496,9 +597,9 @@ define(function () {
*
* @param {Array|Promise} promisesOrValues array of anything, may contain a mix
* of {@link Promise}s and values
* @param {function?} [onFulfilled] resolution handler
* @param {function?} [onRejected] rejection handler
* @param {function?} [onProgress] progress handler
* @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then()
* @param {function?} [onRejected] DEPRECATED, use returnedPromise.then()
* @param {function?} [onProgress] DEPRECATED, use returnedPromise.then()
* @returns {Promise} promise that will resolve to the value that resolved first, or
* will reject with an array of all rejected inputs.
*/
@ -519,14 +620,13 @@ define(function () {
*
* @param {Array|Promise} promisesOrValues array of anything, may contain a mix
* of {@link Promise}s and values
* @param {function?} [onFulfilled] resolution handler
* @param {function?} [onRejected] rejection handler
* @param {function?} [onProgress] progress handler
* @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then()
* @param {function?} [onRejected] DEPRECATED, use returnedPromise.then()
* @param {function?} [onProgress] DEPRECATED, use returnedPromise.then()
* @returns {Promise}
*/
function all(promisesOrValues, onFulfilled, onRejected, onProgress) {
checkCallbacks(1, arguments);
return map(promisesOrValues, identity).then(onFulfilled, onRejected, onProgress);
return _map(promisesOrValues, identity).then(onFulfilled, onRejected, onProgress);
}
/**
@ -535,28 +635,49 @@ define(function () {
* have fulfilled, or will reject when *any one* of the input promises rejects.
*/
function join(/* ...promises */) {
return map(arguments, identity);
return _map(arguments, identity);
}
/**
* Traditional map function, similar to `Array.prototype.map()`, but allows
* input to contain {@link Promise}s and/or values, and mapFunc may return
* either a value or a {@link Promise}
*
* @param {Array|Promise} array array of anything, may contain a mix
* of {@link Promise}s and values
* @param {function} mapFunc mapping function mapFunc(value) which may return
* either a {@link Promise} or value
* @returns {Promise} a {@link Promise} that will resolve to an array containing
* the mapped output values.
* Settles all input promises such that they are guaranteed not to
* be pending once the returned promise fulfills. The returned promise
* will always fulfill, except in the case where `array` is a promise
* that rejects.
* @param {Array|Promise} array or promise for array of promises to settle
* @returns {Promise} promise that always fulfills with an array of
* outcome snapshots for each input promise.
*/
function settle(array) {
return _map(array, toFulfilledState, toRejectedState);
}
/**
* Promise-aware array map function, similar to `Array.prototype.map()`,
* but input array may contain promises or values.
* @param {Array|Promise} array array of anything, may contain promises and values
* @param {function} mapFunc map function which may return a promise or value
* @returns {Promise} promise that will fulfill with an array of mapped values
* or reject if any input promise rejects.
*/
function map(array, mapFunc) {
return _map(array, mapFunc);
}
/**
* Internal map that allows a fallback to handle rejections
* @param {Array|Promise} array array of anything, may contain promises and values
* @param {function} mapFunc map function which may return a promise or value
* @param {function?} fallback function to handle rejected promises
* @returns {Promise} promise that will fulfill with an array of mapped values
* or reject if any input promise rejects.
*/
function _map(array, mapFunc, fallback) {
return when(array, function(array) {
return promise(resolveMap);
return _promise(resolveMap);
function resolveMap(resolve, reject, notify) {
var results, len, toResolve, resolveOne, i;
var results, len, toResolve, i;
// Since we know the resulting length, we can preallocate the results
// array to avoid array expansions.
@ -565,27 +686,28 @@ define(function () {
if(!toResolve) {
resolve(results);
} else {
return;
}
resolveOne = function(item, i) {
when(item, mapFunc).then(function(mapped) {
results[i] = mapped;
if(!--toResolve) {
resolve(results);
}
}, reject, notify);
};
// Since mapFunc may be async, get all invocations of it into flight
for(i = 0; i < len; i++) {
if(i in array) {
resolveOne(array[i], i);
} else {
--toResolve;
}
// Since mapFunc may be async, get all invocations of it into flight
for(i = 0; i < len; i++) {
if(i in array) {
resolveOne(array[i], i);
} else {
--toResolve;
}
}
function resolveOne(item, i) {
when(item, mapFunc, fallback).then(function(mapped) {
results[i] = mapped;
notify(mapped);
if(!--toResolve) {
resolve(results);
}
}, reject);
}
}
});
}
@ -625,12 +747,46 @@ define(function () {
});
}
// Snapshot states
/**
* Creates a fulfilled state snapshot
* @private
* @param {*} x any value
* @returns {{state:'fulfilled',value:*}}
*/
function toFulfilledState(x) {
return { state: 'fulfilled', value: x };
}
/**
* Creates a rejected state snapshot
* @private
* @param {*} x any reason
* @returns {{state:'rejected',reason:*}}
*/
function toRejectedState(x) {
return { state: 'rejected', reason: x };
}
/**
* Creates a pending state snapshot
* @private
* @returns {{state:'pending'}}
*/
function toPendingState() {
return { state: 'pending' };
}
//
// Utilities, etc.
// Internals, utilities, etc.
//
var reduceArray, slice, fcall, nextTick, handlerQueue,
timeout, funcProto, call, arrayProto, undef;
setTimeout, funcProto, call, arrayProto, monitorApi,
cjsRequire, undef;
cjsRequire = require;
//
// Shared handler queue processing
@ -648,20 +804,13 @@ define(function () {
*/
function enqueue(task) {
if(handlerQueue.push(task) === 1) {
scheduleDrainQueue();
nextTick(drainQueue);
}
}
/**
* Schedule the queue to be drained in the next tick.
*/
function scheduleDrainQueue() {
nextTick(drainQueue);
}
/**
* Drain the handler queue entirely or partially, being careful to allow
* the queue to be extended while it is being processed, and to continue
* Drain the handler queue entirely, being careful to allow the
* queue to be extended while it is being processed, and to continue
* processing until it is truly empty.
*/
function drainQueue() {
@ -674,20 +823,36 @@ define(function () {
handlerQueue = [];
}
//
// Capture function and array utils
//
/*global setImmediate:true*/
// capture setTimeout to avoid being caught by fake timers
// used in time based tests
setTimeout = global.setTimeout;
// capture setTimeout to avoid being caught by fake timers used in time based tests
timeout = setTimeout;
nextTick = typeof setImmediate === 'function'
? typeof window === 'undefined'
? setImmediate
: setImmediate.bind(window)
: typeof process === 'object'
? process.nextTick
: function(task) { timeout(task, 0); };
// Allow attaching the monitor to when() if env has no console
monitorApi = typeof console != 'undefined' ? console : when;
// Prefer setImmediate or MessageChannel, cascade to node,
// vertx and finally setTimeout
/*global setImmediate,MessageChannel,process*/
if (typeof setImmediate === 'function') {
nextTick = setImmediate.bind(global);
} else if(typeof MessageChannel !== 'undefined') {
var channel = new MessageChannel();
channel.port1.onmessage = drainQueue;
nextTick = function() { channel.port2.postMessage(0); };
} else if (typeof process === 'object' && process.nextTick) {
nextTick = process.nextTick;
} else {
try {
// vert.x 1.x || 2.x
nextTick = cjsRequire('vertx').runOnLoop || cjsRequire('vertx').runOnContext;
} catch(ignore) {
nextTick = function(t) { setTimeout(t, 0); };
}
}
//
// Capture/polyfill function and array utils
//
// Safe function calls
funcProto = Function.prototype;
@ -748,40 +913,10 @@ define(function () {
return reduced;
};
//
// Utility functions
//
/**
* Helper that checks arrayOfCallbacks to ensure that each element is either
* a function, or null or undefined.
* @private
* @param {number} start index at which to start checking items in arrayOfCallbacks
* @param {Array} arrayOfCallbacks array to check
* @throws {Error} if any element of arrayOfCallbacks is something other than
* a functions, null, or undefined.
*/
function checkCallbacks(start, arrayOfCallbacks) {
// TODO: Promises/A+ update type checking and docs
var arg, i = arrayOfCallbacks.length;
while(i > start) {
arg = arrayOfCallbacks[--i];
if (arg != null && typeof arg != 'function') {
throw new Error('arg '+i+' must be a function');
}
}
}
function noop() {}
function identity(x) {
return x;
}
return when;
});
})(
typeof define === 'function' && define.amd ? define : function (factory) { module.exports = factory(); }
);
})(typeof define === 'function' && define.amd ? define : function (factory) { module.exports = factory(require); }, this);

View File

@ -1,6 +1,6 @@
{
"name": "mopidy",
"version": "0.1.0",
"version": "0.1.1",
"description": "Client lib for controlling a Mopidy music server over a WebSocket",
"homepage": "http://www.mopidy.com/",
"author": {
@ -14,19 +14,19 @@
},
"main": "src/mopidy.js",
"dependencies": {
"bane": "~0.4.0",
"faye-websocket": "~0.4.4",
"when": "~2.0.0"
"bane": "~1.0.0",
"faye-websocket": "~0.7.0",
"when": "~2.4.0"
},
"devDependencies": {
"buster": "~0.6.12",
"buster": "~0.6.13",
"grunt": "~0.4.1",
"grunt-buster": "~0.2.1",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-jshint": "~0.4.3",
"grunt-contrib-uglify": "~0.2.0",
"grunt-contrib-watch": "~0.4.3",
"phantomjs": "~1.9.0"
"grunt-contrib-jshint": "~0.6.4",
"grunt-contrib-uglify": "~0.2.4",
"grunt-contrib-watch": "~0.5.3",
"phantomjs": "~1.9.2-0"
},
"scripts": {
"test": "grunt test",

View File

@ -1,8 +1,6 @@
from __future__ import unicode_literals
# pylint: disable = E0611,F0401
from distutils.version import StrictVersion as SV
# pylint: enable = E0611,F0401
import sys
import warnings
@ -23,4 +21,4 @@ if (isinstance(pykka.__version__, basestring)
warnings.filterwarnings('ignore', 'could not open display')
__version__ = '0.14.1'
__version__ = '0.15.0'

View File

@ -17,12 +17,6 @@ mopidy_args = sys.argv[1:]
sys.argv[1:] = []
# Add ../ to the path so we can run Mopidy from a Git checkout without
# installing it on the system.
sys.path.insert(
0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../')))
from mopidy import commands, ext
from mopidy.audio import Audio
from mopidy import config as config_lib
@ -42,15 +36,12 @@ def main():
if args.show_deps:
commands.show_deps()
loop = gobject.MainLoop()
enabled_extensions = [] # Make sure it is defined before the finally block
logging_initialized = False
# TODO: figure out a way to make the boilerplate in this file reusable in
# scanner and other places we need it.
try:
# Initial config without extensions to bootstrap logging.
logging_initialized = False
logging_config, _ = config_lib.load(
args.config_files, [], args.config_overrides)
@ -59,12 +50,16 @@ def main():
logging_config, args.verbosity_level, args.save_debug_log)
logging_initialized = True
create_file_structures()
check_old_locations()
installed_extensions = ext.load_extensions()
config, config_errors = config_lib.load(
args.config_files, installed_extensions, args.config_overrides)
# Filter out disabled extensions and remove any config errors for them.
enabled_extensions = []
for extension in installed_extensions:
enabled = config[extension.ext_name]['enabled']
if ext.validate_extension(extension) and enabled:
@ -79,31 +74,38 @@ def main():
proxied_config = config_lib.Proxy(config)
log.setup_log_levels(proxied_config)
create_file_structures()
check_old_locations()
ext.register_gstreamer_elements(enabled_extensions)
# Anything that wants to exit after this point must use
# mopidy.utils.process.exit_process as actors have been started.
audio = setup_audio(proxied_config)
backends = setup_backends(proxied_config, enabled_extensions, audio)
core = setup_core(audio, backends)
setup_frontends(proxied_config, enabled_extensions, core)
loop.run()
start(proxied_config, enabled_extensions)
except KeyboardInterrupt:
if logging_initialized:
logger.info('Interrupted. Exiting...')
pass
except Exception as ex:
if logging_initialized:
logger.exception(ex)
raise
finally:
loop.quit()
stop_frontends(enabled_extensions)
stop_core()
stop_backends(enabled_extensions)
stop_audio()
process.stop_remaining_actors()
def create_file_structures():
path.get_or_create_dir(b'$XDG_DATA_DIR/mopidy')
path.get_or_create_file(b'$XDG_CONFIG_DIR/mopidy/mopidy.conf')
def check_old_locations():
dot_mopidy_dir = path.expand_path(b'~/.mopidy')
if os.path.isdir(dot_mopidy_dir):
logger.warning(
'Old Mopidy dot dir found at %s. Please migrate your config to '
'the ini-file based config format. See release notes for further '
'instructions.', dot_mopidy_dir)
old_settings_file = path.expand_path(b'$XDG_CONFIG_DIR/mopidy/settings.py')
if os.path.isfile(old_settings_file):
logger.warning(
'Old Mopidy settings file found at %s. Please migrate your '
'config to the ini-file based config format. See release notes '
'for further instructions.', old_settings_file)
def log_extension_info(all_extensions, enabled_extensions):
@ -125,28 +127,27 @@ def check_config_errors(errors):
sys.exit(1)
def check_old_locations():
dot_mopidy_dir = path.expand_path(b'~/.mopidy')
if os.path.isdir(dot_mopidy_dir):
logger.warning(
'Old Mopidy dot dir found at %s. Please migrate your config to '
'the ini-file based config format. See release notes for further '
'instructions.', dot_mopidy_dir)
old_settings_file = path.expand_path(b'$XDG_CONFIG_DIR/mopidy/settings.py')
if os.path.isfile(old_settings_file):
logger.warning(
'Old Mopidy settings file found at %s. Please migrate your '
'config to the ini-file based config format. See release notes '
'for further instructions.', old_settings_file)
def start(config, extensions):
loop = gobject.MainLoop()
try:
audio = start_audio(config)
backends = start_backends(config, extensions, audio)
core = start_core(audio, backends)
start_frontends(config, extensions, core)
loop.run()
except KeyboardInterrupt:
logger.info('Interrupted. Exiting...')
return
finally:
loop.quit()
stop_frontends(extensions)
stop_core()
stop_backends(extensions)
stop_audio()
process.stop_remaining_actors()
def create_file_structures():
path.get_or_create_dir(b'$XDG_DATA_DIR/mopidy')
path.get_or_create_file(b'$XDG_CONFIG_DIR/mopidy/mopidy.conf')
def setup_audio(config):
def start_audio(config):
logger.info('Starting Mopidy audio')
return Audio.start(config=config).proxy()
@ -156,7 +157,7 @@ def stop_audio():
process.stop_actors_by_class(Audio)
def setup_backends(config, extensions, audio):
def start_backends(config, extensions, audio):
backend_classes = []
for extension in extensions:
backend_classes.extend(extension.get_backend_classes())
@ -180,7 +181,7 @@ def stop_backends(extensions):
process.stop_actors_by_class(backend_class)
def setup_core(audio, backends):
def start_core(audio, backends):
logger.info('Starting Mopidy core')
return Core.start(audio=audio, backends=backends).proxy()
@ -190,7 +191,7 @@ def stop_core():
process.stop_actors_by_class(Core)
def setup_frontends(config, extensions, core):
def start_frontends(config, extensions, core):
frontend_classes = []
for extension in extensions:
frontend_classes.extend(extension.get_frontend_classes())

View File

@ -25,6 +25,22 @@ playlists.register_elements()
MB = 1 << 20
# GST_PLAY_FLAG_VIDEO (1<<0)
# GST_PLAY_FLAG_AUDIO (1<<1)
# GST_PLAY_FLAG_TEXT (1<<2)
# GST_PLAY_FLAG_VIS (1<<3)
# GST_PLAY_FLAG_SOFT_VOLUME (1<<4)
# GST_PLAY_FLAG_NATIVE_AUDIO (1<<5)
# GST_PLAY_FLAG_NATIVE_VIDEO (1<<6)
# GST_PLAY_FLAG_DOWNLOAD (1<<7)
# GST_PLAY_FLAG_BUFFERING (1<<8)
# GST_PLAY_FLAG_DEINTERLACE (1<<9)
# GST_PLAY_FLAG_SOFT_COLORBALANCE (1<<10)
# Default flags to use for playbin: AUDIO, SOFT_VOLUME, DOWNLOAD
PLAYBIN_FLAGS = (1 << 1) | (1 << 4) | (1 << 7)
PLAYBIN_VIS_FLAGS = PLAYBIN_FLAGS | (1 << 3)
class Audio(pykka.ThreadingActor):
"""
@ -58,6 +74,7 @@ class Audio(pykka.ThreadingActor):
try:
self._setup_playbin()
self._setup_output()
self._setup_visualizer()
self._setup_mixer()
self._setup_message_processor()
except gobject.GError as ex:
@ -81,9 +98,7 @@ class Audio(pykka.ThreadingActor):
def _setup_playbin(self):
playbin = gst.element_factory_make('playbin2')
fakesink = gst.element_factory_make('fakesink')
playbin.set_property('video-sink', fakesink)
playbin.set_property('flags', PLAYBIN_FLAGS)
self._connect(playbin, 'about-to-finish', self._on_about_to_finish)
self._connect(playbin, 'notify::source', self._on_new_source)
@ -152,6 +167,20 @@ class Audio(pykka.ThreadingActor):
'Failed to create audio output "%s": %s', output_desc, ex)
process.exit_process()
def _setup_visualizer(self):
visualizer_element = self._config['audio']['visualizer']
if not visualizer_element:
return
try:
visualizer = gst.element_factory_make(visualizer_element)
self._playbin.set_property('vis-plugin', visualizer)
self._playbin.set_property('flags', PLAYBIN_VIS_FLAGS)
logger.info('Audio visualizer set to "%s"', visualizer_element)
except gobject.GError as ex:
logger.error(
'Failed to create audio visualizer "%s": %s',
visualizer_element, ex)
def _setup_mixer(self):
mixer_desc = self._config['audio']['mixer']
track_desc = self._config['audio']['mixer_track']
@ -196,7 +225,8 @@ class Audio(pykka.ThreadingActor):
self._mixer_track.min_volume, self._mixer_track.max_volume)
logger.info(
'Audio mixer set to "%s" using track "%s"',
mixer.get_factory().get_name(), track.label)
str(mixer.get_factory().get_name()).decode('utf-8'),
str(track.label).decode('utf-8'))
def _select_mixer_track(self, mixer, track_label):
# Ignore tracks without volumes, then look for track with

View File

@ -29,9 +29,7 @@ class AutoAudioMixer(gst.Bin):
gst.Bin.__init__(self)
mixer = self._find_mixer()
if mixer:
# pylint: disable=E1101
self.add(mixer)
# pylint: enable=E1101
logger.debug('AutoAudioMixer chose: %s', mixer.get_name())
else:
logger.debug('AutoAudioMixer did not find any usable mixers')

View File

@ -15,11 +15,6 @@ class Backend(object):
#: the backend doesn't provide a library.
library = None
#: The library update provider. An instance of
#: :class:`~mopidy.backends.base.BaseLibraryUpdateProvider`, or
#: :class:`None` if the backend doesn't provide a library.
updater = None
#: The playback provider. An instance of
#: :class:`~mopidy.backends.base.BasePlaybackProvider`, or :class:`None` if
#: the backend doesn't provide playback.
@ -40,9 +35,6 @@ class Backend(object):
def has_library(self):
return self.library is not None
def has_updater(self):
return self.updater is not None
def has_playback(self):
return self.playback is not None
@ -96,15 +88,7 @@ class BaseLibraryProvider(object):
class BaseLibraryUpdateProvider(object):
"""
:param backend: backend the controller is a part of
:type backend: :class:`mopidy.backends.base.Backend`
"""
pykka_traversable = True
def __init__(self, backend):
self.backend = backend
uri_schemes = []
def load(self):
"""Loads the library and returns all tracks in it.
@ -172,9 +156,22 @@ class BasePlaybackProvider(object):
:rtype: :class:`True` if successful, else :class:`False`
"""
self.audio.prepare_change()
self.audio.set_uri(track.uri).get()
self.change_track(track)
return self.audio.start_playback().get()
def change_track(self, track):
"""
Swith to provided track.
*MAY be reimplemented by subclass.*
:param track: the track to play
:type track: :class:`mopidy.models.Track`
:rtype: :class:`True` if successful, else :class:`False`
"""
self.audio.set_uri(track.uri).get()
return True
def resume(self):
"""
Resume playback at the same time position playback was paused.

View File

@ -21,6 +21,7 @@ class Extension(ext.Extension):
schema['media_dir'] = config.Path()
schema['playlists_dir'] = config.Path()
schema['tag_cache_file'] = config.Path()
schema['scan_timeout'] = config.Integer(minimum=0)
return schema
def validate_environment(self):
@ -29,3 +30,7 @@ class Extension(ext.Extension):
def get_backend_classes(self):
from .actor import LocalBackend
return [LocalBackend]
def get_library_updaters(self):
from .library import LocalLibraryUpdateProvider
return [LocalLibraryUpdateProvider]

View File

@ -8,8 +8,9 @@ import pykka
from mopidy.backends import base
from mopidy.utils import encoding, path
from .library import LocalLibraryProvider, LocalLibraryUpdateProvider
from .library import LocalLibraryProvider
from .playlists import LocalPlaylistsProvider
from .playback import LocalPlaybackProvider
logger = logging.getLogger('mopidy.backends.local')
@ -23,11 +24,10 @@ class LocalBackend(pykka.ThreadingActor, base.Backend):
self.check_dirs_and_files()
self.library = LocalLibraryProvider(backend=self)
self.updater = LocalLibraryUpdateProvider(backend=self)
self.playback = base.BasePlaybackProvider(audio=audio, backend=self)
self.playback = LocalPlaybackProvider(audio=audio, backend=self)
self.playlists = LocalPlaylistsProvider(backend=self)
self.uri_schemes = ['file']
self.uri_schemes = ['local']
def check_dirs_and_files(self):
if not os.path.isdir(self.config['local']['media_dir']):

View File

@ -3,3 +3,4 @@ enabled = true
media_dir = $XDG_MUSIC_DIR
playlists_dir = $XDG_DATA_DIR/mopidy/local/playlists
tag_cache_file = $XDG_DATA_DIR/mopidy/local/tag_cache
scan_timeout = 1000

View File

@ -81,7 +81,8 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
result_tracks = filter(any_filter, result_tracks)
else:
raise LookupError('Invalid lookup field: %s' % field)
return SearchResult(uri='file:search', tracks=result_tracks)
# TODO: add local:search:<query>
return SearchResult(uri='local:search', tracks=result_tracks)
def search(self, query=None, uris=None):
# TODO Only return results within URI roots given by ``uris``
@ -122,7 +123,8 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
result_tracks = filter(any_filter, result_tracks)
else:
raise LookupError('Invalid lookup field: %s' % field)
return SearchResult(uri='file:search', tracks=result_tracks)
# TODO: add local:search:<query>
return SearchResult(uri='local:search', tracks=result_tracks)
def _validate_query(self, query):
for (_, values) in query.iteritems():
@ -135,11 +137,12 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
# TODO: rename and move to tagcache extension.
class LocalLibraryUpdateProvider(base.BaseLibraryProvider):
def __init__(self, *args, **kwargs):
super(LocalLibraryUpdateProvider, self).__init__(*args, **kwargs)
uri_schemes = ['local']
def __init__(self, config):
self._tracks = {}
self._media_dir = self.backend.config['local']['media_dir']
self._tag_cache_file = self.backend.config['local']['tag_cache_file']
self._media_dir = config['local']['media_dir']
self._tag_cache_file = config['local']['tag_cache_file']
def load(self):
tracks = parse_mpd_tag_cache(self._tag_cache_file, self._media_dir)
@ -156,6 +159,8 @@ class LocalLibraryUpdateProvider(base.BaseLibraryProvider):
def commit(self):
directory, basename = os.path.split(self._tag_cache_file)
# TODO: cleanup directory/basename.* files.
tmp = tempfile.NamedTemporaryFile(
prefix=basename + '.', dir=directory, delete=False)

View File

@ -0,0 +1,19 @@
from __future__ import unicode_literals
import logging
import os
from mopidy.backends import base
from mopidy.utils import path
logger = logging.getLogger('mopidy.backends.local')
class LocalPlaybackProvider(base.BasePlaybackProvider):
def change_track(self, track):
media_dir = self.backend.config['local']['media_dir']
# TODO: check that type is correct.
file_path = path.uri_to_path(track.uri).split(':', 1)[1]
file_path = os.path.join(media_dir, file_path)
track = track.copy(uri=path.path_to_uri(file_path))
return super(LocalPlaybackProvider, self).change_track(track)

View File

@ -24,7 +24,7 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider):
def create(self, name):
name = formatting.slugify(name)
uri = path.path_to_uri(self._get_m3u_path(name))
uri = 'local:playlist:%s.m3u' % name
playlist = Playlist(uri=uri, name=name)
return self.save(playlist)
@ -37,6 +37,7 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider):
self._delete_m3u(playlist.uri)
def lookup(self, uri):
# TODO: store as {uri: playlist}?
for playlist in self._playlists:
if playlist.uri == uri:
return playlist
@ -45,8 +46,8 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider):
playlists = []
for m3u in glob.glob(os.path.join(self._playlists_dir, '*.m3u')):
uri = path.path_to_uri(m3u)
name = os.path.splitext(os.path.basename(m3u))[0]
uri = 'local:playlist:%s' % name
tracks = []
for track_uri in parse_m3u(m3u, self._media_dir):
@ -61,6 +62,7 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider):
playlists.append(playlist)
self.playlists = playlists
# TODO: send what scheme we loaded them for?
listener.BackendListener.send('playlists_loaded')
logger.info(
@ -86,38 +88,30 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider):
return playlist
def _get_m3u_path(self, name):
name = formatting.slugify(name)
file_path = os.path.join(self._playlists_dir, name + '.m3u')
def _m3u_uri_to_path(self, uri):
# TODO: create uri handling helpers for local uri types.
file_path = path.uri_to_path(uri).split(':', 1)[1]
file_path = os.path.join(self._playlists_dir, file_path)
path.check_file_path_is_inside_base_dir(file_path, self._playlists_dir)
return file_path
def _save_m3u(self, playlist):
file_path = path.uri_to_path(playlist.uri)
path.check_file_path_is_inside_base_dir(file_path, self._playlists_dir)
file_path = self._m3u_uri_to_path(playlist.uri)
with open(file_path, 'w') as file_handle:
for track in playlist.tracks:
if track.uri.startswith('file://'):
uri = path.uri_to_path(track.uri)
else:
uri = track.uri
file_handle.write(uri + '\n')
file_handle.write(track.uri + '\n')
def _delete_m3u(self, uri):
file_path = path.uri_to_path(uri)
path.check_file_path_is_inside_base_dir(file_path, self._playlists_dir)
file_path = self._m3u_uri_to_path(uri)
if os.path.exists(file_path):
os.remove(file_path)
def _rename_m3u(self, playlist):
src_file_path = path.uri_to_path(playlist.uri)
path.check_file_path_is_inside_base_dir(
src_file_path, self._playlists_dir)
dst_name = formatting.slugify(playlist.name)
dst_uri = 'local:playlist:%s.m3u' % dst_name
dst_file_path = self._get_m3u_path(playlist.name)
path.check_file_path_is_inside_base_dir(
dst_file_path, self._playlists_dir)
src_file_path = self._m3u_uri_to_path(playlist.uri)
dst_file_path = self._m3u_uri_to_path(dst_uri)
shutil.move(src_file_path, dst_file_path)
return playlist.copy(uri=path.path_to_uri(dst_file_path))
return playlist.copy(uri=dst_uri)

View File

@ -1,7 +1,8 @@
from __future__ import unicode_literals
import logging
import urllib
import os
import urlparse
from mopidy.models import Track, Artist, Album
from mopidy.utils.encoding import locale_decode
@ -30,7 +31,6 @@ def parse_m3u(file_path, media_dir):
- m3u files are latin-1.
- This function does not bother with Extended M3U directives.
"""
# TODO: uris as bytes
uris = []
try:
@ -46,16 +46,19 @@ def parse_m3u(file_path, media_dir):
if line.startswith('#'):
continue
# FIXME what about other URI types?
if line.startswith('file://'):
if urlparse.urlsplit(line).scheme:
uris.append(line)
elif os.path.normpath(line) == os.path.abspath(line):
path = path_to_uri(line)
uris.append(path)
else:
path = path_to_uri(media_dir, line)
path = path_to_uri(os.path.join(media_dir, line))
uris.append(path)
return uris
# TODO: remove music_dir from API
def parse_mpd_tag_cache(tag_cache, music_dir=''):
"""
Converts a MPD tag_cache into a lists of tracks, artists and albums.
@ -86,17 +89,17 @@ def parse_mpd_tag_cache(tag_cache, music_dir=''):
key, value = line.split(b': ', 1)
if key == b'key':
_convert_mpd_data(current, tracks, music_dir)
_convert_mpd_data(current, tracks)
current.clear()
current[key.lower()] = value.decode('utf-8')
_convert_mpd_data(current, tracks, music_dir)
_convert_mpd_data(current, tracks)
return tracks
def _convert_mpd_data(data, tracks, music_dir):
def _convert_mpd_data(data, tracks):
if not data:
return
@ -160,15 +163,8 @@ def _convert_mpd_data(data, tracks, music_dir):
path = data['file'][1:]
else:
path = data['file']
path = urllib.unquote(path.encode('utf-8'))
if isinstance(music_dir, unicode):
music_dir = music_dir.encode('utf-8')
# Make sure we only pass bytestrings to path_to_uri to avoid implicit
# decoding of bytestrings to unicode strings
track_kwargs['uri'] = path_to_uri(music_dir, path)
track_kwargs['uri'] = 'local:track:%s' % path
track_kwargs['length'] = int(data.get('time', 0)) * 1000
track = Track(**track_kwargs)

View File

@ -18,9 +18,6 @@ logger = logging.getLogger('mopidy.backends.spotify')
BITRATES = {96: 2, 160: 0, 320: 1}
# pylint: disable = R0901
# SpotifySessionManager: Too many ancestors (9/7)
class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
cache_location = None
@ -33,9 +30,17 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
self.cache_location = config['spotify']['cache_dir']
self.settings_location = config['spotify']['cache_dir']
full_proxy = ''
if config['proxy']['hostname']:
full_proxy = config['proxy']['hostname']
if config['proxy']['port']:
full_proxy += ':' + str(config['proxy']['port'])
if config['proxy']['scheme']:
full_proxy = config['proxy']['scheme'] + "://" + full_proxy
PyspotifySessionManager.__init__(
self, config['spotify']['username'], config['spotify']['password'],
proxy=config['proxy']['hostname'],
proxy=full_proxy,
proxy_username=config['proxy']['username'],
proxy_password=config['proxy']['password'])
@ -108,9 +113,6 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
def music_delivery(self, session, frames, frame_size, num_frames,
sample_type, sample_rate, channels):
"""Callback used by pyspotify"""
# pylint: disable = R0913
# Too many arguments (8/5)
if not self.push_audio_data:
return 0
@ -173,9 +175,14 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
logger.debug('Still getting data; skipped refresh of playlists')
return
playlists = []
folders = []
for spotify_playlist in self.session.playlist_container():
if spotify_playlist.type() == 'folder_start':
folders.append(spotify_playlist)
if spotify_playlist.type() == 'folder_end':
folders.pop()
playlists.append(translator.to_mopidy_playlist(
spotify_playlist,
spotify_playlist, folders=folders,
bitrate=self.bitrate, username=self.username))
playlists.append(translator.to_mopidy_playlist(
self.session.starred(),

View File

@ -67,7 +67,8 @@ def to_mopidy_track(spotify_track, bitrate=None):
return track_cache[uri]
def to_mopidy_playlist(spotify_playlist, bitrate=None, username=None):
def to_mopidy_playlist(
spotify_playlist, folders=None, bitrate=None, username=None):
if spotify_playlist is None or spotify_playlist.type() != 'playlist':
return
try:
@ -78,6 +79,9 @@ def to_mopidy_playlist(spotify_playlist, bitrate=None, username=None):
if not spotify_playlist.is_loaded():
return Playlist(uri=uri, name='[loading...]')
name = spotify_playlist.name()
if folders:
folder_names = '/'.join(folder.name() for folder in folders)
name = folder_names + '/' + name
tracks = [
to_mopidy_track(spotify_track, bitrate=bitrate)
for spotify_track in spotify_playlist

View File

@ -1,6 +1,7 @@
[stream]
enabled = true
protocols =
file
http
https
mms

View File

@ -24,9 +24,13 @@ _audio_schema = ConfigSchema('audio')
_audio_schema['mixer'] = String()
_audio_schema['mixer_track'] = String(optional=True)
_audio_schema['output'] = String()
_audio_schema['visualizer'] = String(optional=True)
_proxy_schema = ConfigSchema('proxy')
_proxy_schema['scheme'] = String(optional=True,
choices=['http', 'https', 'socks4', 'socks5'])
_proxy_schema['hostname'] = Hostname(optional=True)
_proxy_schema['port'] = Port(optional=True)
_proxy_schema['username'] = String(optional=True)
_proxy_schema['password'] = Secret(optional=True)

View File

@ -39,6 +39,7 @@ def convert(settings):
helper('audio/output', 'OUTPUT')
helper('proxy/hostname', 'SPOTIFY_PROXY_HOST')
helper('proxy/port', 'SPOTIFY_PROXY_PORT')
helper('proxy/username', 'SPOTIFY_PROXY_USERNAME')
helper('proxy/password', 'SPOTIFY_PROXY_PASSWORD')

View File

@ -11,8 +11,11 @@ pykka = info
mixer = autoaudiomixer
mixer_track =
output = autoaudiosink
visualizer =
[proxy]
scheme =
hostname =
port =
username =
password =

View File

@ -3,7 +3,6 @@ from __future__ import unicode_literals
import logging
import re
import socket
import sys
from mopidy.utils import path
from mopidy.config import validators
@ -72,9 +71,9 @@ class String(ConfigValue):
def deserialize(self, value):
value = decode(value).strip()
validators.validate_required(value, self._required)
validators.validate_choice(value, self._choices)
if not value:
return None
validators.validate_choice(value, self._choices)
return value
def serialize(self, value, display=False):
@ -83,41 +82,38 @@ class String(ConfigValue):
return encode(value)
class Secret(ConfigValue):
"""Secret value.
class Secret(String):
"""Secret string value.
Should be used for passwords, auth tokens etc. Deserializing will not
convert to unicode. Will mask value when being displayed.
Is decoded as utf-8 and \\n \\t escapes should work and be preserved.
Should be used for passwords, auth tokens etc. Will mask value when being
displayed.
"""
def __init__(self, optional=False, choices=None):
self._required = not optional
def deserialize(self, value):
value = value.strip()
validators.validate_required(value, self._required)
if not value:
return None
return value
self._choices = None # Choices doesn't make sense for secrets
def serialize(self, value, display=False):
if isinstance(value, unicode):
value = value.encode('utf-8')
if value is None:
return b''
elif display:
if value is not None and display:
return b'********'
return value
return super(Secret, self).serialize(value, display)
class Integer(ConfigValue):
"""Integer value."""
def __init__(self, minimum=None, maximum=None, choices=None):
def __init__(
self, minimum=None, maximum=None, choices=None, optional=False):
self._required = not optional
self._minimum = minimum
self._maximum = maximum
self._choices = choices
def deserialize(self, value):
validators.validate_required(value, self._required)
if not value:
return None
value = int(value)
validators.validate_choice(value, self._choices)
validators.validate_minimum(value, self._minimum)
@ -223,8 +219,9 @@ class Port(Integer):
allocate a port for us.
"""
# TODO: consider probing if port is free or not?
def __init__(self, choices=None):
super(Port, self).__init__(minimum=0, maximum=2**16-1, choices=choices)
def __init__(self, choices=None, optional=False):
super(Port, self).__init__(
minimum=0, maximum=2 ** 16 - 1, choices=choices, optional=optional)
class Path(ConfigValue):
@ -256,7 +253,7 @@ class Path(ConfigValue):
def serialize(self, value, display=False):
if isinstance(value, unicode):
value = value.encode(sys.getfilesystemencoding())
raise ValueError('paths should always be bytes')
if isinstance(value, ExpandedPath):
return value.original
return value

View File

@ -69,8 +69,9 @@ class LibraryController(object):
"""
query = query or kwargs
futures = [
backend.library.find_exact(query=query, uris=uris)
for (backend, uris) in self._get_backends_to_uris(uris).items()]
backend.library.find_exact(query=query, uris=backend_uris)
for (backend, backend_uris)
in self._get_backends_to_uris(uris).items()]
return [result for result in pykka.get_all(futures) if result]
def lookup(self, uri):
@ -145,6 +146,7 @@ class LibraryController(object):
"""
query = query or kwargs
futures = [
backend.library.search(query=query, uris=uris)
for (backend, uris) in self._get_backends_to_uris(uris).items()]
backend.library.search(query=query, uris=backend_uris)
for (backend, backend_uris)
in self._get_backends_to_uris(uris).items()]
return [result for result in pykka.get_all(futures) if result]

View File

@ -13,9 +13,6 @@ logger = logging.getLogger('mopidy.core')
class PlaybackController(object):
# pylint: disable = R0902
# Too many instance attributes
pykka_traversable = True
def __init__(self, audio, backends, core):
@ -175,9 +172,6 @@ class PlaybackController(object):
"""
def get_tl_track_at_eot(self):
# pylint: disable = R0911
# Too many return statements
tl_tracks = self.core.tracklist.tl_tracks
if not tl_tracks:
@ -401,6 +395,7 @@ class PlaybackController(object):
if self.random and self._shuffled:
self._shuffled.remove(tl_track)
if on_error_step == 1:
# TODO: can cause an endless loop for single track repeat.
self.next()
elif on_error_step == -1:
self.previous()

View File

@ -79,6 +79,15 @@ class Extension(object):
"""
return []
def get_library_updaters(self):
"""List of library updater classes
:returns: list of
:class:`~mopidy.backends.base.BaseLibraryUpdateProvider`
subclasses
"""
return []
def register_gstreamer_elements(self):
"""Hook for registering custom GStreamer elements

View File

@ -1,8 +1,8 @@
/*! Mopidy.js - built 2013-03-31
/*! Mopidy.js - built 2013-09-17
* http://www.mopidy.com/
* Copyright (c) 2013 Stein Magnus Jodal and contributors
* Licensed under the Apache License, Version 2.0 */
((typeof define === "function" && define.amd && function (m) { define(m); }) ||
((typeof define === "function" && define.amd && function (m) { define("bane", m); }) ||
(typeof module === "object" && function (m) { module.exports = m(); }) ||
function (m) { this.bane = m(); }
)(function () {
@ -148,7 +148,7 @@
notifyListener(event, toNotify[i], args);
}
toNotify = listeners(this, event).slice()
toNotify = listeners(this, event).slice();
args = slice.call(arguments, 1);
for (i = 0, l = toNotify.length; i < l; ++i) {
notifyListener(event, toNotify[i], args);
@ -187,27 +187,30 @@ if (typeof window !== "undefined") {
*
* @author Brian Cavalier
* @author John Hann
* @version 2.0.0
* @version 2.4.0
*/
(function(define) { 'use strict';
define(function () {
(function(define, global) { 'use strict';
define(function (require) {
// Public API
when.defer = defer; // Create a deferred
when.promise = promise; // Create a pending promise
when.resolve = resolve; // Create a resolved promise
when.reject = reject; // Create a rejected promise
when.defer = defer; // Create a {promise, resolver} pair
when.join = join; // Join 2 or more promises
when.all = all; // Resolve a list of promises
when.map = map; // Array.map() for promises
when.reduce = reduce; // Array.reduce() for promises
when.settle = settle; // Settle a list of promises
when.any = any; // One-winner race
when.some = some; // Multi-winner race
when.isPromise = isPromise; // Determine if a thing is a promise
when.isPromise = isPromiseLike; // DEPRECATED: use isPromiseLike
when.isPromiseLike = isPromiseLike; // Is something promise-like, aka thenable
/**
* Register an observer for a promise or immediate value.
@ -235,13 +238,35 @@ define(function () {
* a trusted when.js promise. Any other duck-typed promise is considered
* untrusted.
* @constructor
* @param {function} sendMessage function to deliver messages to the promise's handler
* @param {function?} inspect function that reports the promise's state
* @name Promise
*/
function Promise(then) {
this.then = then;
function Promise(sendMessage, inspect) {
this._message = sendMessage;
this.inspect = inspect;
}
Promise.prototype = {
/**
* Register handlers for this promise.
* @param [onFulfilled] {Function} fulfillment handler
* @param [onRejected] {Function} rejection handler
* @param [onProgress] {Function} progress handler
* @return {Promise} new Promise
*/
then: function(onFulfilled, onRejected, onProgress) {
/*jshint unused:false*/
var args, sendMessage;
args = arguments;
sendMessage = this._message;
return _promise(function(resolve, reject, notify) {
sendMessage('when', args, resolve, notify);
}, this._status && this._status.observed());
},
/**
* Register a rejection handler. Shortcut for .then(undefined, onRejected)
* @param {function?} onRejected
@ -262,9 +287,7 @@ define(function () {
* @returns {Promise}
*/
ensure: function(onFulfilledOrRejected) {
var self = this;
return this.then(injectHandler, injectHandler).yield(self);
return this.then(injectHandler, injectHandler)['yield'](this);
function injectHandler() {
return resolve(onFulfilledOrRejected());
@ -285,6 +308,16 @@ define(function () {
});
},
/**
* Runs a side effect when this promise fulfills, without changing the
* fulfillment value.
* @param {function} onFulfilledSideEffect
* @returns {Promise}
*/
tap: function(onFulfilledSideEffect) {
return this.then(onFulfilledSideEffect)['yield'](this);
},
/**
* Assumes that this promise will fulfill with an array, and arranges
* for the onFulfilled to be called with the array as its argument list
@ -340,13 +373,16 @@ define(function () {
}
/**
* Creates a new Deferred with fully isolated resolver and promise parts,
* either or both of which may be given out safely to consumers.
* Creates a {promise, resolver} pair, either or both of which
* may be given out safely to consumers.
* The resolver has resolve, reject, and progress. The promise
* only has then.
* has then plus extended promise API.
*
* @return {{
* promise: Promise,
* resolve: function:Promise,
* reject: function:Promise,
* notify: function:Promise
* resolver: {
* resolve: function:Promise,
* reject: function:Promise,
@ -394,12 +430,26 @@ define(function () {
/**
* Creates a new promise whose fate is determined by resolver.
* @private (for now)
* @param {function} resolver function(resolve, reject, notify)
* @returns {Promise} promise whose fate is determine by resolver
*/
function promise(resolver) {
var value, handlers = [];
return _promise(resolver, monitorApi.PromiseStatus && monitorApi.PromiseStatus());
}
/**
* Creates a new promise, linked to parent, whose fate is determined
* by resolver.
* @param {function} resolver function(resolve, reject, notify)
* @param {Promise?} status promise from which the new promise is begotten
* @returns {Promise} promise whose fate is determine by resolver
* @private
*/
function _promise(resolver, status) {
var self, value, consumers = [];
self = new Promise(_message, inspect);
self._status = status;
// Call the provider resolver to seal the promise's fate
try {
@ -409,29 +459,34 @@ define(function () {
}
// Return the promise
return new Promise(then);
return self;
/**
* Register handlers for this promise.
* @param [onFulfilled] {Function} fulfillment handler
* @param [onRejected] {Function} rejection handler
* @param [onProgress] {Function} progress handler
* @return {Promise} new Promise
* Private message delivery. Queues and delivers messages to
* the promise's ultimate fulfillment value or rejection reason.
* @private
* @param {String} type
* @param {Array} args
* @param {Function} resolve
* @param {Function} notify
*/
function then(onFulfilled, onRejected, onProgress) {
return promise(function(resolve, reject, notify) {
handlers
// Call handlers later, after resolution
? handlers.push(function(value) {
value.then(onFulfilled, onRejected, onProgress)
.then(resolve, reject, notify);
})
// Call handlers soon, but not in the current stack
: enqueue(function() {
value.then(onFulfilled, onRejected, onProgress)
.then(resolve, reject, notify);
});
});
function _message(type, args, resolve, notify) {
consumers ? consumers.push(deliver) : enqueue(function() { deliver(value); });
function deliver(p) {
p._message(type, args, resolve, notify);
}
}
/**
* Returns a snapshot of the promise's state at the instant inspect()
* is called. The returned object is not live and will not update as
* the promise's state changes.
* @returns {{ state:String, value?:*, reason?:* }} status snapshot
* of the promise.
*/
function inspect() {
return value ? value.inspect() : toPendingState();
}
/**
@ -440,14 +495,17 @@ define(function () {
* @param {*|Promise} val resolution value
*/
function promiseResolve(val) {
if(!handlers) {
if(!consumers) {
return;
}
value = coerce(val);
scheduleHandlers(handlers, value);
scheduleConsumers(consumers, value);
consumers = undef;
handlers = undef;
if(status) {
updateStatus(value, status);
}
}
/**
@ -463,27 +521,90 @@ define(function () {
* @param {*} update progress event payload to pass to all listeners
*/
function promiseNotify(update) {
if(handlers) {
scheduleHandlers(handlers, progressing(update));
if(consumers) {
scheduleConsumers(consumers, progressed(update));
}
}
}
/**
* Creates a fulfilled, local promise as a proxy for a value
* NOTE: must never be exposed
* @param {*} value fulfillment value
* @returns {Promise}
*/
function fulfilled(value) {
return near(
new NearFulfilledProxy(value),
function() { return toFulfilledState(value); }
);
}
/**
* Creates a rejected, local promise with the supplied reason
* NOTE: must never be exposed
* @param {*} reason rejection reason
* @returns {Promise}
*/
function rejected(reason) {
return near(
new NearRejectedProxy(reason),
function() { return toRejectedState(reason); }
);
}
/**
* Creates a near promise using the provided proxy
* NOTE: must never be exposed
* @param {object} proxy proxy for the promise's ultimate value or reason
* @param {function} inspect function that returns a snapshot of the
* returned near promise's state
* @returns {Promise}
*/
function near(proxy, inspect) {
return new Promise(function (type, args, resolve) {
try {
resolve(proxy[type].apply(proxy, args));
} catch(e) {
resolve(rejected(e));
}
}, inspect);
}
/**
* Create a progress promise with the supplied update.
* @private
* @param {*} update
* @return {Promise} progress promise
*/
function progressed(update) {
return new Promise(function (type, args, _, notify) {
var onProgress = args[2];
try {
notify(typeof onProgress === 'function' ? onProgress(update) : update);
} catch(e) {
notify(e);
}
});
}
/**
* Coerces x to a trusted Promise
*
* @private
* @param {*} x thing to coerce
* @returns {Promise} Guaranteed to return a trusted Promise. If x
* @returns {*} Guaranteed to return a trusted Promise. If x
* is trusted, returns x, otherwise, returns a new, trusted, already-resolved
* Promise whose resolution value is:
* * the resolution value of x if it's a foreign promise, or
* * x if it's a value
*/
function coerce(x) {
if(x instanceof Promise) {
if (x instanceof Promise) {
return x;
} else if (x !== Object(x)) {
}
if (!(x === Object(x) && 'then' in x)) {
return fulfilled(x);
}
@ -510,61 +631,34 @@ define(function () {
}
/**
* Create an already-fulfilled promise for the supplied value
* @private
* Proxy for a near, fulfilled value
* @param {*} value
* @return {Promise} fulfilled promise
* @constructor
*/
function fulfilled(value) {
var self = new Promise(function (onFulfilled) {
try {
return typeof onFulfilled == 'function'
? coerce(onFulfilled(value)) : self;
} catch (e) {
return rejected(e);
}
});
return self;
function NearFulfilledProxy(value) {
this.value = value;
}
NearFulfilledProxy.prototype.when = function(onResult) {
return typeof onResult === 'function' ? onResult(this.value) : this.value;
};
/**
* Create an already-rejected promise with the supplied rejection reason.
* @private
* Proxy for a near rejection
* @param {*} reason
* @return {Promise} rejected promise
* @constructor
*/
function rejected(reason) {
var self = new Promise(function (_, onRejected) {
try {
return typeof onRejected == 'function'
? coerce(onRejected(reason)) : self;
} catch (e) {
return rejected(e);
}
});
return self;
function NearRejectedProxy(reason) {
this.reason = reason;
}
/**
* Create a progress promise with the supplied update.
* @private
* @param {*} update
* @return {Promise} progress promise
*/
function progressing(update) {
var self = new Promise(function (_, __, onProgress) {
try {
return typeof onProgress == 'function'
? progressing(onProgress(update)) : self;
} catch (e) {
return progressing(e);
}
});
return self;
}
NearRejectedProxy.prototype.when = function(_, onError) {
if(typeof onError === 'function') {
return onError(this.reason);
} else {
throw this.reason;
}
};
/**
* Schedule a task that will process a list of handlers
@ -573,7 +667,7 @@ define(function () {
* @param {Array} handlers queue of handlers to execute
* @param {*} value passed as the only arg to each handler
*/
function scheduleHandlers(handlers, value) {
function scheduleConsumers(handlers, value) {
enqueue(function() {
var handler, i = 0;
while (handler = handlers[i++]) {
@ -582,14 +676,23 @@ define(function () {
});
}
function updateStatus(value, status) {
value.then(statusFulfilled, statusRejected);
function statusFulfilled() { status.fulfilled(); }
function statusRejected(r) { status.rejected(r); }
}
/**
* Determines if promiseOrValue is a promise or not
*
* @param {*} promiseOrValue anything
* @returns {boolean} true if promiseOrValue is a {@link Promise}
* Determines if x is promise-like, i.e. a thenable object
* NOTE: Will return true for *any thenable object*, and isn't truly
* safe, since it may attempt to access the `then` property of x (i.e.
* clever/malicious getters may do weird things)
* @param {*} x anything
* @returns {boolean} true if x is promise-like
*/
function isPromise(promiseOrValue) {
return promiseOrValue && typeof promiseOrValue.then === 'function';
function isPromiseLike(x) {
return x && typeof x.then === 'function';
}
/**
@ -601,17 +704,15 @@ define(function () {
* @param {Array} promisesOrValues array of anything, may contain a mix
* of promises and values
* @param howMany {number} number of promisesOrValues to resolve
* @param {function?} [onFulfilled] resolution handler
* @param {function?} [onRejected] rejection handler
* @param {function?} [onProgress] progress handler
* @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then()
* @param {function?} [onRejected] DEPRECATED, use returnedPromise.then()
* @param {function?} [onProgress] DEPRECATED, use returnedPromise.then()
* @returns {Promise} promise that will resolve to an array of howMany values that
* resolved first, or will reject with an array of
* (promisesOrValues.length - howMany) + 1 rejection reasons.
*/
function some(promisesOrValues, howMany, onFulfilled, onRejected, onProgress) {
checkCallbacks(2, arguments);
return when(promisesOrValues, function(promisesOrValues) {
return promise(resolveSome).then(onFulfilled, onRejected, onProgress);
@ -635,7 +736,7 @@ define(function () {
rejectOne = function(reason) {
reasons.push(reason);
if(!--toReject) {
fulfillOne = rejectOne = noop;
fulfillOne = rejectOne = identity;
reject(reasons);
}
};
@ -644,7 +745,7 @@ define(function () {
// This orders the values based on promise resolution order
values.push(val);
if (!--toResolve) {
fulfillOne = rejectOne = noop;
fulfillOne = rejectOne = identity;
resolve(values);
}
};
@ -674,9 +775,9 @@ define(function () {
*
* @param {Array|Promise} promisesOrValues array of anything, may contain a mix
* of {@link Promise}s and values
* @param {function?} [onFulfilled] resolution handler
* @param {function?} [onRejected] rejection handler
* @param {function?} [onProgress] progress handler
* @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then()
* @param {function?} [onRejected] DEPRECATED, use returnedPromise.then()
* @param {function?} [onProgress] DEPRECATED, use returnedPromise.then()
* @returns {Promise} promise that will resolve to the value that resolved first, or
* will reject with an array of all rejected inputs.
*/
@ -697,14 +798,13 @@ define(function () {
*
* @param {Array|Promise} promisesOrValues array of anything, may contain a mix
* of {@link Promise}s and values
* @param {function?} [onFulfilled] resolution handler
* @param {function?} [onRejected] rejection handler
* @param {function?} [onProgress] progress handler
* @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then()
* @param {function?} [onRejected] DEPRECATED, use returnedPromise.then()
* @param {function?} [onProgress] DEPRECATED, use returnedPromise.then()
* @returns {Promise}
*/
function all(promisesOrValues, onFulfilled, onRejected, onProgress) {
checkCallbacks(1, arguments);
return map(promisesOrValues, identity).then(onFulfilled, onRejected, onProgress);
return _map(promisesOrValues, identity).then(onFulfilled, onRejected, onProgress);
}
/**
@ -713,28 +813,49 @@ define(function () {
* have fulfilled, or will reject when *any one* of the input promises rejects.
*/
function join(/* ...promises */) {
return map(arguments, identity);
return _map(arguments, identity);
}
/**
* Traditional map function, similar to `Array.prototype.map()`, but allows
* input to contain {@link Promise}s and/or values, and mapFunc may return
* either a value or a {@link Promise}
*
* @param {Array|Promise} array array of anything, may contain a mix
* of {@link Promise}s and values
* @param {function} mapFunc mapping function mapFunc(value) which may return
* either a {@link Promise} or value
* @returns {Promise} a {@link Promise} that will resolve to an array containing
* the mapped output values.
* Settles all input promises such that they are guaranteed not to
* be pending once the returned promise fulfills. The returned promise
* will always fulfill, except in the case where `array` is a promise
* that rejects.
* @param {Array|Promise} array or promise for array of promises to settle
* @returns {Promise} promise that always fulfills with an array of
* outcome snapshots for each input promise.
*/
function settle(array) {
return _map(array, toFulfilledState, toRejectedState);
}
/**
* Promise-aware array map function, similar to `Array.prototype.map()`,
* but input array may contain promises or values.
* @param {Array|Promise} array array of anything, may contain promises and values
* @param {function} mapFunc map function which may return a promise or value
* @returns {Promise} promise that will fulfill with an array of mapped values
* or reject if any input promise rejects.
*/
function map(array, mapFunc) {
return _map(array, mapFunc);
}
/**
* Internal map that allows a fallback to handle rejections
* @param {Array|Promise} array array of anything, may contain promises and values
* @param {function} mapFunc map function which may return a promise or value
* @param {function?} fallback function to handle rejected promises
* @returns {Promise} promise that will fulfill with an array of mapped values
* or reject if any input promise rejects.
*/
function _map(array, mapFunc, fallback) {
return when(array, function(array) {
return promise(resolveMap);
return _promise(resolveMap);
function resolveMap(resolve, reject, notify) {
var results, len, toResolve, resolveOne, i;
var results, len, toResolve, i;
// Since we know the resulting length, we can preallocate the results
// array to avoid array expansions.
@ -743,27 +864,28 @@ define(function () {
if(!toResolve) {
resolve(results);
} else {
return;
}
resolveOne = function(item, i) {
when(item, mapFunc).then(function(mapped) {
results[i] = mapped;
if(!--toResolve) {
resolve(results);
}
}, reject, notify);
};
// Since mapFunc may be async, get all invocations of it into flight
for(i = 0; i < len; i++) {
if(i in array) {
resolveOne(array[i], i);
} else {
--toResolve;
}
// Since mapFunc may be async, get all invocations of it into flight
for(i = 0; i < len; i++) {
if(i in array) {
resolveOne(array[i], i);
} else {
--toResolve;
}
}
function resolveOne(item, i) {
when(item, mapFunc, fallback).then(function(mapped) {
results[i] = mapped;
notify(mapped);
if(!--toResolve) {
resolve(results);
}
}, reject);
}
}
});
}
@ -803,12 +925,46 @@ define(function () {
});
}
// Snapshot states
/**
* Creates a fulfilled state snapshot
* @private
* @param {*} x any value
* @returns {{state:'fulfilled',value:*}}
*/
function toFulfilledState(x) {
return { state: 'fulfilled', value: x };
}
/**
* Creates a rejected state snapshot
* @private
* @param {*} x any reason
* @returns {{state:'rejected',reason:*}}
*/
function toRejectedState(x) {
return { state: 'rejected', reason: x };
}
/**
* Creates a pending state snapshot
* @private
* @returns {{state:'pending'}}
*/
function toPendingState() {
return { state: 'pending' };
}
//
// Utilities, etc.
// Internals, utilities, etc.
//
var reduceArray, slice, fcall, nextTick, handlerQueue,
timeout, funcProto, call, arrayProto, undef;
setTimeout, funcProto, call, arrayProto, monitorApi,
cjsRequire, undef;
cjsRequire = require;
//
// Shared handler queue processing
@ -826,20 +982,13 @@ define(function () {
*/
function enqueue(task) {
if(handlerQueue.push(task) === 1) {
scheduleDrainQueue();
nextTick(drainQueue);
}
}
/**
* Schedule the queue to be drained in the next tick.
*/
function scheduleDrainQueue() {
nextTick(drainQueue);
}
/**
* Drain the handler queue entirely or partially, being careful to allow
* the queue to be extended while it is being processed, and to continue
* Drain the handler queue entirely, being careful to allow the
* queue to be extended while it is being processed, and to continue
* processing until it is truly empty.
*/
function drainQueue() {
@ -852,20 +1001,36 @@ define(function () {
handlerQueue = [];
}
//
// Capture function and array utils
//
/*global setImmediate:true*/
// capture setTimeout to avoid being caught by fake timers
// used in time based tests
setTimeout = global.setTimeout;
// capture setTimeout to avoid being caught by fake timers used in time based tests
timeout = setTimeout;
nextTick = typeof setImmediate === 'function'
? typeof window === 'undefined'
? setImmediate
: setImmediate.bind(window)
: typeof process === 'object'
? process.nextTick
: function(task) { timeout(task, 0); };
// Allow attaching the monitor to when() if env has no console
monitorApi = typeof console != 'undefined' ? console : when;
// Prefer setImmediate or MessageChannel, cascade to node,
// vertx and finally setTimeout
/*global setImmediate,MessageChannel,process*/
if (typeof setImmediate === 'function') {
nextTick = setImmediate.bind(global);
} else if(typeof MessageChannel !== 'undefined') {
var channel = new MessageChannel();
channel.port1.onmessage = drainQueue;
nextTick = function() { channel.port2.postMessage(0); };
} else if (typeof process === 'object' && process.nextTick) {
nextTick = process.nextTick;
} else {
try {
// vert.x 1.x || 2.x
nextTick = cjsRequire('vertx').runOnLoop || cjsRequire('vertx').runOnContext;
} catch(ignore) {
nextTick = function(t) { setTimeout(t, 0); };
}
}
//
// Capture/polyfill function and array utils
//
// Safe function calls
funcProto = Function.prototype;
@ -926,43 +1091,13 @@ define(function () {
return reduced;
};
//
// Utility functions
//
/**
* Helper that checks arrayOfCallbacks to ensure that each element is either
* a function, or null or undefined.
* @private
* @param {number} start index at which to start checking items in arrayOfCallbacks
* @param {Array} arrayOfCallbacks array to check
* @throws {Error} if any element of arrayOfCallbacks is something other than
* a functions, null, or undefined.
*/
function checkCallbacks(start, arrayOfCallbacks) {
// TODO: Promises/A+ update type checking and docs
var arg, i = arrayOfCallbacks.length;
while(i > start) {
arg = arrayOfCallbacks[--i];
if (arg != null && typeof arg != 'function') {
throw new Error('arg '+i+' must be a function');
}
}
}
function noop() {}
function identity(x) {
return x;
}
return when;
});
})(
typeof define === 'function' && define.amd ? define : function (factory) { module.exports = factory(); }
);
})(typeof define === 'function' && define.amd ? define : function (factory) { module.exports = factory(require); }, this);
if (typeof module === "object" && typeof require === "function") {
var bane = require("bane");

File diff suppressed because one or more lines are too long

View File

@ -236,6 +236,8 @@ class MpdContext(object):
#: The subsytems that we want to be notified about in idle mode.
subscriptions = None
_invalid_playlist_chars = re.compile(r'[\n\r/]')
def __init__(self, dispatcher, session=None, config=None, core=None):
self.dispatcher = dispatcher
self.session = session
@ -248,10 +250,11 @@ class MpdContext(object):
self.refresh_playlists_mapping()
def create_unique_name(self, playlist_name):
name = playlist_name
stripped_name = self._invalid_playlist_chars.sub(' ', playlist_name)
name = stripped_name
i = 2
while name in self._playlist_uri_from_name:
name = '%s [%d]' % (playlist_name, i)
name = '%s [%d]' % (stripped_name, i)
i += 1
return name
@ -266,6 +269,7 @@ class MpdContext(object):
for playlist in self.core.playlists.playlists.get():
if not playlist.name:
continue
# TODO: add scheme to name perhaps 'foo (spotify)' etc.
name = self.create_unique_name(playlist.name)
self._playlist_uri_from_name[name] = playlist.uri
self._playlist_name_from_uri[playlist.uri] = name

View File

@ -72,9 +72,7 @@ def load_protocol_modules():
The protocol modules must be imported to get them registered in
:attr:`request_handlers` and :attr:`mpd_commands`.
"""
# pylint: disable = W0612
from . import ( # noqa
audio_output, channels, command_list, connection, current_playlist,
empty, music_db, playback, reflection, status, stickers,
stored_playlists)
# pylint: enable = W0612

View File

@ -101,9 +101,6 @@ def deleteid(context, tlid):
Deletes the song ``SONGID`` from the playlist
"""
tlid = int(tlid)
tl_track = context.core.playback.current_tl_track.get()
if tl_track and tl_track.tlid == tlid:
context.core.playback.next()
tl_tracks = context.core.tracklist.remove(tlid=tlid).get()
if not tl_tracks:
raise MpdNoExistError('No such song', command='deleteid')

View File

@ -39,8 +39,8 @@ def _artist_as_track(artist):
artists=[artist])
@handle_request(r'^count "(?P<tag>[^"]+)" "(?P<needle>[^"]*)"$')
def count(context, tag, needle):
@handle_request(r'^count ' + QUERY_RE)
def count(context, mpd_query):
"""
*musicpd.org, music database section:*
@ -48,6 +48,11 @@ def count(context, tag, needle):
Counts the number of songs and their total playtime in the db
matching ``TAG`` exactly.
*GMPC:*
- does not add quotes around the tag argument.
- use multiple tag-needle pairs to make more specific searches.
"""
return [('songs', 0), ('playtime', 0)] # TODO
@ -240,8 +245,9 @@ def _list_date(context, query):
return dates
@handle_request(r'^listall "(?P<uri>[^"]+)"')
def listall(context, uri):
@handle_request(r'^listall$')
@handle_request(r'^listall "(?P<uri>[^"]+)"$')
def listall(context, uri=None):
"""
*musicpd.org, music database section:*
@ -252,8 +258,9 @@ def listall(context, uri):
raise MpdNotImplemented # TODO
@handle_request(r'^listallinfo "(?P<uri>[^"]+)"')
def listallinfo(context, uri):
@handle_request(r'^listallinfo$')
@handle_request(r'^listallinfo "(?P<uri>[^"]+)"$')
def listallinfo(context, uri=None):
"""
*musicpd.org, music database section:*

View File

@ -156,14 +156,14 @@ def query_from_mpd_list_format(field, mpd_query):
if field == 'album':
if not tokens[0]:
raise ValueError
return {'artist': [tokens[0]]} # See above NOTE
return {'artist': [tokens[0]]}
else:
raise MpdArgError(
'should be "Album" for 3 arguments', command='list')
elif len(tokens) % 2 == 0:
query = {}
while tokens:
key = str(tokens[0].lower()) # See above NOTE
key = tokens[0].lower()
value = tokens[1]
tokens = tokens[2:]
if key not in ('artist', 'album', 'date', 'genre'):

View File

@ -34,7 +34,7 @@ class MprisFrontend(pykka.ThreadingActor, CoreListener):
self.mpris_object = objects.MprisObject(self.config, self.core)
self._send_startup_notification()
except Exception as e:
logger.error('MPRIS frontend setup failed (%s)', e)
logger.warning('MPRIS frontend setup failed (%s)', e)
self.stop()
def on_stop(self):

View File

@ -90,7 +90,7 @@ class ImmutableObject(object):
for v in value]
elif isinstance(value, ImmutableObject):
value = value.serialize()
if value:
if not (isinstance(value, list) and len(value) == 0):
data[public_key] = value
return data

View File

@ -27,7 +27,6 @@ pygst.require('0.10')
import gst
from mopidy import config as config_lib, ext
from mopidy.audio import dummy as dummy_audio
from mopidy.models import Track, Artist, Album
from mopidy.utils import log, path, versioning
@ -45,21 +44,36 @@ def main():
log.setup_root_logger()
log.setup_console_logging(logging_config, args.verbosity_level)
extensions = dict((e.ext_name, e) for e in ext.load_extensions())
extensions = ext.load_extensions()
config, errors = config_lib.load(
config_files, extensions.values(), config_overrides)
config_files, extensions, config_overrides)
log.setup_log_levels(config)
if not config['local']['media_dir']:
logging.warning('Config value local/media_dir is not set.')
return
if not config['local']['scan_timeout']:
logging.warning('Config value local/scan_timeout is not set.')
return
# TODO: missing config error checking and other default setup code.
audio = dummy_audio.DummyAudio()
local_backend_classes = extensions['local'].get_backend_classes()
local_backend = local_backend_classes[0](config, audio)
local_updater = local_backend.updater
updaters = {}
for e in extensions:
for updater_class in e.get_library_updaters():
if updater_class and 'local' in updater_class.uri_schemes:
updaters[e.ext_name] = updater_class
if not updaters:
logging.error('No usable library updaters found.')
return
elif len(updaters) > 1:
logging.error('More than one library updater found. '
'Provided by: %s', ', '.join(updaters.keys()))
return
local_updater = updaters.values()[0](config) # TODO: switch to actor?
media_dir = config['local']['media_dir']
@ -97,9 +111,11 @@ def main():
logging.warning('Failed %s: %s', uri, error)
logging.debug('Debug info for %s: %s', uri, debug)
scan_timeout = config['local']['scan_timeout']
logging.info('Scanning new and modified tracks.')
# TODO: just pass the library in instead?
scanner = Scanner(uris_update, store, debug)
scanner = Scanner(uris_update, store, debug, scan_timeout)
try:
scanner.start()
except KeyboardInterrupt:
@ -139,6 +155,7 @@ def translator(data):
_retrieve(gst.TAG_ALBUM, 'name', album_kwargs)
_retrieve(gst.TAG_TRACK_COUNT, 'num_tracks', album_kwargs)
_retrieve(gst.TAG_ALBUM_VOLUME_COUNT, 'num_discs', album_kwargs)
_retrieve(gst.TAG_ARTIST, 'name', artist_kwargs)
if gst.TAG_DATE in data and data[gst.TAG_DATE]:
@ -152,6 +169,7 @@ def translator(data):
_retrieve(gst.TAG_TITLE, 'name', track_kwargs)
_retrieve(gst.TAG_TRACK_NUMBER, 'track_no', track_kwargs)
_retrieve(gst.TAG_ALBUM_VOLUME_NUMBER, 'disc_no', track_kwargs)
# Following keys don't seem to have TAG_* constant.
_retrieve('album-artist', 'name', albumartist_kwargs)
@ -174,12 +192,15 @@ def translator(data):
class Scanner(object):
def __init__(self, uris, data_callback, error_callback=None):
def __init__(
self, uris, data_callback, error_callback=None, scan_timeout=1000):
self.data = {}
self.uris = iter(uris)
self.data_callback = data_callback
self.error_callback = error_callback
self.scan_timeout = scan_timeout
self.loop = gobject.MainLoop()
self.timeout_id = None
self.fakesink = gst.element_factory_make('fakesink')
self.fakesink.set_property('signal-handoffs', True)
@ -250,6 +271,14 @@ class Scanner(object):
self.error_callback(uri, error, debug)
self.next_uri()
def process_timeout(self):
if self.error_callback:
uri = self.uribin.get_property('uri')
self.error_callback(
uri, 'Scan timed out after %d ms' % self.scan_timeout, None)
self.next_uri()
return False
def get_duration(self):
self.pipe.get_state() # Block until state change is done.
try:
@ -260,6 +289,9 @@ class Scanner(object):
def next_uri(self):
self.data = {}
if self.timeout_id:
gobject.source_remove(self.timeout_id)
self.timeout_id = None
try:
uri = next(self.uris)
except StopIteration:
@ -267,6 +299,8 @@ class Scanner(object):
return False
self.pipe.set_state(gst.STATE_NULL)
self.uribin.set_property('uri', uri)
self.timeout_id = gobject.timeout_add(
self.scan_timeout, self.process_timeout)
self.pipe.set_state(gst.STATE_PLAYING)
return True

View File

@ -2,12 +2,9 @@ from __future__ import unicode_literals
import logging
import os
import re
# pylint: disable = W0402
import string
# pylint: enable = W0402
import sys
import urllib
import urlparse
import glib
@ -51,7 +48,7 @@ def get_or_create_file(file_path):
return file_path
def path_to_uri(*paths):
def path_to_uri(path):
"""
Convert OS specific path to file:// URI.
@ -61,17 +58,15 @@ def path_to_uri(*paths):
Returns a file:// URI as an unicode string.
"""
path = os.path.join(*paths)
if isinstance(path, unicode):
path = path.encode('utf-8')
if sys.platform == 'win32':
return 'file:' + urllib.quote(path)
return 'file://' + urllib.quote(path)
path = urllib.quote(path)
return urlparse.urlunsplit((b'file', b'', path, b'', b''))
def uri_to_path(uri):
"""
Convert the file:// to a OS specific path.
Convert an URI to a OS specific path.
Returns a bytestring, since the file path can contain chars with other
encoding than UTF-8.
@ -82,10 +77,7 @@ def uri_to_path(uri):
"""
if isinstance(uri, unicode):
uri = uri.encode('utf-8')
if sys.platform == 'win32':
return urllib.unquote(re.sub(b'^file:', b'', uri))
else:
return urllib.unquote(re.sub(b'^file://', b'', uri))
return urllib.unquote(urlparse.urlsplit(uri).path)
def split_path(path):

View File

@ -14,11 +14,9 @@ def get_version():
def get_git_version():
process = Popen(['git', 'describe'], stdout=PIPE, stderr=PIPE)
# pylint: disable = E1101
if process.wait() != 0:
raise EnvironmentError('Execution of "git describe" failed')
version = process.stdout.read().strip()
# pylint: enable = E1101
if version.startswith('v'):
version = version[1:]
return version

View File

@ -1,21 +0,0 @@
[MESSAGES CONTROL]
#
# Disabled messages
# -----------------
#
# C0103 - Invalid name "%s" (should match %s)
# C0111 - Missing docstring
# R0201 - Method could be a function
# R0801 - Similar lines in %s files
# R0902 - Too many instance attributes (%s/%s)
# R0903 - Too few public methods (%s/%s)
# R0904 - Too many public methods (%s/%s)
# R0912 - Too many branches (%s/%s)
# R0913 - Too many arguments (%s/%s)
# 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,R0201,R0801,R0902,R0903,R0904,R0912,R0913,R0921,W0141,W0142,W0511,W0613

View File

@ -1,2 +0,0 @@
pyserial
# Available as python-serial in Debian/Ubuntu

View File

@ -2,4 +2,5 @@ cherrypy >= 3.2.2
# Available as python-cherrypy3 in Debian/Ubuntu
ws4py >= 0.2.3
# Available as python-ws4py from apt.mopidy.com
# Available as python-ws4py in newer Debian/Ubuntu and from apt.mopidy.com for
# older releases of Debian/Ubuntu

View File

@ -1,4 +1,4 @@
pyspotify >= 1.9, < 1.11
pyspotify >= 1.9, < 2
# The libspotify Python wrapper
# Available as the python-spotify package from apt.mopidy.com

View File

@ -2,4 +2,3 @@ coverage
flake8
mock >= 1.0
nose
pylint

View File

@ -1,6 +0,0 @@
[nosetests]
verbosity = 1
#with-coverage = 1
cover-package = mopidy
cover-inclusive = 1
cover-html = 1

View File

@ -28,7 +28,7 @@ setup(
'Pykka >= 1.1',
],
extras_require={
'spotify': ['pyspotify >= 1.9, < 1.11'],
'spotify': ['pyspotify >= 1.9, < 2'],
'scrobbler': ['pylast >= 0.5.7'],
'http': ['cherrypy >= 3.2.2', 'ws4py >= 0.2.3'],
},
@ -36,7 +36,6 @@ setup(
tests_require=[
'nose',
'mock >= 1.0',
'unittest2',
],
entry_points={
'console_scripts': [
@ -61,7 +60,6 @@ setup(
'License :: OSI Approved :: Apache Software License',
'Operating System :: MacOS :: MacOS X',
'Operating System :: POSIX :: Linux',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Topic :: Multimedia :: Sound/Audio :: Players',
],

View File

@ -1,12 +1,11 @@
from __future__ import unicode_literals
import os
import sys
def path_to_data_dir(name):
if not isinstance(name, bytes):
name = name.encode(sys.getfilesystemencoding())
name = name.encode('utf-8')
path = os.path.dirname(__file__)
path = os.path.join(path, b'data')
path = os.path.abspath(path)

View File

@ -21,6 +21,7 @@ class AudioTest(unittest.TestCase):
'mixer': 'fakemixer track_max_volume=65536',
'mixer_track': None,
'output': 'fakesink',
'visualizer': None,
}
}
self.song_uri = path_to_uri(path_to_data_dir('song1.wav'))
@ -70,6 +71,7 @@ class AudioTest(unittest.TestCase):
'mixer': 'fakemixer track_max_volume=40',
'mixer_track': None,
'output': 'fakesink',
'visualizer': None,
}
}
self.audio = audio.Audio.start(config=config).proxy()

View File

@ -7,8 +7,6 @@ import pykka
from mopidy import core
from mopidy.models import Track, Album, Artist
from tests import path_to_data_dir
class LibraryControllerTest(object):
artists = [Artist(name='artist1'), Artist(name='artist2'), Artist()]
@ -17,13 +15,10 @@ class LibraryControllerTest(object):
Album(name='album2', artists=artists[1:2]),
Album()]
tracks = [
Track(
uri='file://' + path_to_data_dir('uri1'), name='track1',
artists=artists[:1], album=albums[0], date='2001-02-03',
length=4000),
Track(
uri='file://' + path_to_data_dir('uri2'), name='track2',
artists=artists[1:2], album=albums[1], date='2002', length=4000),
Track(uri='local:track:path1', name='track1', artists=artists[:1],
album=albums[0], date='2001-02-03', length=4000),
Track(uri='local:track:path2', name='track2', artists=artists[1:2],
album=albums[1], date='2002', length=4000),
Track()]
config = {}
@ -66,11 +61,11 @@ class LibraryControllerTest(object):
self.assertEqual(list(result[0].tracks), [])
def test_find_exact_uri(self):
track_1_uri = 'file://' + path_to_data_dir('uri1')
track_1_uri = 'local:track:path1'
result = self.library.find_exact(uri=track_1_uri)
self.assertEqual(list(result[0].tracks), self.tracks[:1])
track_2_uri = 'file://' + path_to_data_dir('uri2')
track_2_uri = 'local:track:path2'
result = self.library.find_exact(uri=track_2_uri)
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
@ -136,10 +131,10 @@ class LibraryControllerTest(object):
self.assertEqual(list(result[0].tracks), [])
def test_search_uri(self):
result = self.library.search(uri=['RI1'])
result = self.library.search(uri=['TH1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(uri=['RI2'])
result = self.library.search(uri=['TH2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_track(self):
@ -183,7 +178,7 @@ class LibraryControllerTest(object):
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(any=['Bum1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(any=['RI1'])
result = self.library.search(any=['TH1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
def test_search_wrong_type(self):

View File

@ -1,9 +1,4 @@
from __future__ import unicode_literals
from mopidy.utils.path import path_to_uri
from tests import path_to_data_dir
song = path_to_data_dir('song%s.wav')
generate_song = lambda i: path_to_uri(song % i)
generate_song = lambda i: 'local:track:song%s.wav' % i

View File

@ -5,7 +5,6 @@ import unittest
from mopidy.backends.local import actor
from mopidy.core import PlaybackState
from mopidy.models import Track
from mopidy.utils.path import path_to_uri
from tests import path_to_data_dir
from tests.backends.base.playback import PlaybackControllerTest
@ -24,25 +23,25 @@ class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase):
tracks = [
Track(uri=generate_song(i), length=4464) for i in range(1, 4)]
def add_track(self, path):
uri = path_to_uri(path_to_data_dir(path))
def add_track(self, uri):
track = Track(uri=uri, length=4464)
self.tracklist.add([track])
def test_uri_scheme(self):
self.assertIn('file', self.core.uri_schemes)
self.assertNotIn('file', self.core.uri_schemes)
self.assertIn('local', self.core.uri_schemes)
def test_play_mp3(self):
self.add_track('blank.mp3')
self.add_track('local:track:blank.mp3')
self.playback.play()
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
def test_play_ogg(self):
self.add_track('blank.ogg')
self.add_track('local:track:blank.ogg')
self.playback.play()
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
def test_play_flac(self):
self.add_track('blank.flac')
self.add_track('local:track:blank.flac')
self.playback.play()
self.assertEqual(self.playback.state, PlaybackState.PLAYING)

View File

@ -7,7 +7,6 @@ import unittest
from mopidy.backends.local import actor
from mopidy.models import Track
from mopidy.utils.path import path_to_uri
from tests import path_to_data_dir
from tests.backends.base.playlists import (
@ -89,21 +88,18 @@ class LocalPlaylistsControllerTest(
def test_playlist_contents_is_written_to_disk(self):
track = Track(uri=generate_song(1))
track_path = track.uri[len('file://'):]
playlist = self.core.playlists.create('test')
playlist_path = playlist.uri[len('file://'):]
playlist_path = os.path.join(self.playlists_dir, 'test.m3u')
playlist = playlist.copy(tracks=[track])
playlist = self.core.playlists.save(playlist)
with open(playlist_path) as playlist_file:
contents = playlist_file.read()
self.assertEqual(track_path, contents.strip())
self.assertEqual(track.uri, contents.strip())
def test_playlists_are_loaded_at_startup(self):
playlist_path = os.path.join(self.playlists_dir, 'test.m3u')
track = Track(uri=path_to_uri(path_to_data_dir('uri2')))
track = Track(uri='local:track:path2')
playlist = self.core.playlists.create('test')
playlist = playlist.copy(tracks=[track])
playlist = self.core.playlists.save(playlist)
@ -112,8 +108,7 @@ class LocalPlaylistsControllerTest(
self.assert_(backend.playlists.playlists)
self.assertEqual(
path_to_uri(playlist_path),
backend.playlists.playlists[0].uri)
'local:playlist:test', backend.playlists.playlists[0].uri)
self.assertEqual(
playlist.name, backend.playlists.playlists[0].name)
self.assertEqual(

View File

@ -98,7 +98,7 @@ expected_tracks = []
def generate_track(path, ident):
uri = path_to_uri(path_to_data_dir(path))
uri = 'local:track:%s' % path
track = Track(
uri=uri, name='trackname', artists=expected_artists,
album=expected_albums[0], track_no=1, date='2006', length=4000,
@ -126,11 +126,10 @@ class MPDTagCacheToTracksTest(unittest.TestCase):
def test_simple_cache(self):
tracks = parse_mpd_tag_cache(
path_to_data_dir('simple_tag_cache'), path_to_data_dir(''))
uri = path_to_uri(path_to_data_dir('song1.mp3'))
track = Track(
uri=uri, name='trackname', artists=expected_artists, track_no=1,
album=expected_albums[0], date='2006', length=4000,
last_modified=1272319626)
uri='local:track:song1.mp3', name='trackname',
artists=expected_artists, track_no=1, album=expected_albums[0],
date='2006', length=4000, last_modified=1272319626)
self.assertEqual(set([track]), tracks)
def test_advanced_cache(self):
@ -142,12 +141,11 @@ class MPDTagCacheToTracksTest(unittest.TestCase):
tracks = parse_mpd_tag_cache(
path_to_data_dir('utf8_tag_cache'), path_to_data_dir(''))
uri = path_to_uri(path_to_data_dir('song1.mp3'))
artists = [Artist(name='æøå')]
album = Album(name='æøå', artists=artists)
track = Track(
uri=uri, name='æøå', artists=artists, album=album, length=4000,
last_modified=1272319626)
uri='local:track:song1.mp3', name='æøå', artists=artists,
album=album, length=4000, last_modified=1272319626)
self.assertEqual(track, list(tracks)[0])
@ -159,8 +157,8 @@ class MPDTagCacheToTracksTest(unittest.TestCase):
def test_cache_with_blank_track_info(self):
tracks = parse_mpd_tag_cache(
path_to_data_dir('blank_tag_cache'), path_to_data_dir(''))
uri = path_to_uri(path_to_data_dir('song1.mp3'))
expected = Track(uri=uri, length=4000, last_modified=1272319626)
expected = Track(
uri='local:track:song1.mp3', length=4000, last_modified=1272319626)
self.assertEqual(set([expected]), tracks)
def test_musicbrainz_tagcache(self):
@ -183,10 +181,10 @@ class MPDTagCacheToTracksTest(unittest.TestCase):
def test_albumartist_tag_cache(self):
tracks = parse_mpd_tag_cache(
path_to_data_dir('albumartist_tag_cache'), path_to_data_dir(''))
uri = path_to_uri(path_to_data_dir('song1.mp3'))
artist = Artist(name='albumartistname')
album = expected_albums[0].copy(artists=[artist])
track = Track(
uri=uri, name='trackname', artists=expected_artists, track_no=1,
album=album, date='2006', length=4000, last_modified=1272319626)
uri='local:track:song1.mp3', name='trackname',
artists=expected_artists, track_no=1, album=album, date='2006',
length=4000, last_modified=1272319626)
self.assertEqual(track, list(tracks)[0])

View File

@ -81,7 +81,8 @@ class ConfigSchemaTest(unittest.TestCase):
class LogLevelConfigSchemaTest(unittest.TestCase):
def test_conversion(self):
schema = schemas.LogLevelConfigSchema('test')
result, errors = schema.deserialize({'foo.bar': 'DEBUG', 'baz': 'INFO'})
result, errors = schema.deserialize(
{'foo.bar': 'DEBUG', 'baz': 'INFO'})
self.assertEqual(logging.DEBUG, result['foo.bar'])
self.assertEqual(logging.INFO, result['baz'])

View File

@ -5,7 +5,6 @@ from __future__ import unicode_literals
import logging
import mock
import socket
import sys
import unittest
from mopidy.config import types
@ -99,13 +98,18 @@ class StringTest(unittest.TestCase):
self.assertIsInstance(result, bytes)
self.assertEqual(b'', result)
def test_deserialize_enforces_choices_optional(self):
value = types.String(optional=True, choices=['foo', 'bar', 'baz'])
self.assertEqual(None, value.deserialize(b''))
self.assertRaises(ValueError, value.deserialize, b'foobar')
class SecretTest(unittest.TestCase):
def test_deserialize_passes_through(self):
def test_deserialize_decodes_utf8(self):
value = types.Secret()
result = value.deserialize(b'foo')
self.assertIsInstance(result, bytes)
self.assertEqual(b'foo', result)
result = value.deserialize('æøå'.encode('utf-8'))
self.assertIsInstance(result, unicode)
self.assertEqual('æøå', result)
def test_deserialize_enforces_required(self):
value = types.Secret()
@ -164,6 +168,10 @@ class IntegerTest(unittest.TestCase):
self.assertEqual(5, value.deserialize('5'))
self.assertRaises(ValueError, value.deserialize, '15')
def test_deserialize_respects_optional(self):
value = types.Integer(optional=True)
self.assertEqual(None, value.deserialize(''))
class BooleanTest(unittest.TestCase):
def test_deserialize_conversion_success(self):
@ -367,7 +375,4 @@ class PathTest(unittest.TestCase):
def test_serialize_unicode_string(self):
value = types.Path()
expected = 'æøå'.encode(sys.getfilesystemencoding())
result = value.serialize('æøå')
self.assertEqual(expected, result)
self.assertIsInstance(result, bytes)
self.assertRaises(ValueError, value.serialize, 'æøå')

View File

@ -3,22 +3,22 @@ mpd_version: 0.14.2
fs_charset: UTF-8
info_end
songList begin
key: uri1
file: /uri1
key: key1
file: /path1
Artist: artist1
Title: track1
Album: album1
Date: 2001-02-03
Time: 4
key: uri2
file: /uri2
key: key1
file: /path2
Artist: artist2
Title: track2
Album: album2
Date: 2002
Time: 4
key: uri3
file: /uri3
key: key3
file: /path3
Artist: artist3
Title: track3
Album: album3

Binary file not shown.

Binary file not shown.

View File

@ -7,7 +7,19 @@ from tests.frontends.mpd import protocol
class MusicDatabaseHandlerTest(protocol.BaseTestCase):
def test_count(self):
self.sendRequest('count "tag" "needle"')
self.sendRequest('count "artist" "needle"')
self.assertInResponse('songs: 0')
self.assertInResponse('playtime: 0')
self.assertInResponse('OK')
def test_count_without_quotes(self):
self.sendRequest('count artist "needle"')
self.assertInResponse('songs: 0')
self.assertInResponse('playtime: 0')
self.assertInResponse('OK')
def test_count_with_multiple_pairs(self):
self.sendRequest('count "artist" "foo" "album" "bar"')
self.assertInResponse('songs: 0')
self.assertInResponse('playtime: 0')
self.assertInResponse('OK')
@ -70,11 +82,19 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase):
self.assertEqual(playlists[0].tracks[0].uri, 'dummy:a')
self.assertInResponse('OK')
def test_listall(self):
def test_listall_without_uri(self):
self.sendRequest('listall')
self.assertEqualResponse('ACK [0@0] {} Not implemented')
def test_listall_with_uri(self):
self.sendRequest('listall "file:///dev/urandom"')
self.assertEqualResponse('ACK [0@0] {} Not implemented')
def test_listallinfo(self):
def test_listallinfo_without_uri(self):
self.sendRequest('listallinfo')
self.assertEqualResponse('ACK [0@0] {} Not implemented')
def test_listallinfo_with_uri(self):
self.sendRequest('listallinfo "file:///dev/urandom"')
self.assertEqualResponse('ACK [0@0] {} Not implemented')

View File

@ -10,8 +10,8 @@ from tests.frontends.mpd import protocol
class PlaylistsHandlerTest(protocol.BaseTestCase):
def test_listplaylist(self):
self.backend.playlists.playlists = [
Playlist(name='name', uri='dummy:name',
tracks=[Track(uri='dummy:a')])]
Playlist(
name='name', uri='dummy:name', tracks=[Track(uri='dummy:a')])]
self.sendRequest('listplaylist "name"')
self.assertInResponse('file: dummy:a')
@ -19,8 +19,8 @@ class PlaylistsHandlerTest(protocol.BaseTestCase):
def test_listplaylist_without_quotes(self):
self.backend.playlists.playlists = [
Playlist(name='name', uri='dummy:name',
tracks=[Track(uri='dummy:a')])]
Playlist(
name='name', uri='dummy:name', tracks=[Track(uri='dummy:a')])]
self.sendRequest('listplaylist name')
self.assertInResponse('file: dummy:a')
@ -41,8 +41,8 @@ class PlaylistsHandlerTest(protocol.BaseTestCase):
def test_listplaylistinfo(self):
self.backend.playlists.playlists = [
Playlist(name='name', uri='dummy:name',
tracks=[Track(uri='dummy:a')])]
Playlist(
name='name', uri='dummy:name', tracks=[Track(uri='dummy:a')])]
self.sendRequest('listplaylistinfo "name"')
self.assertInResponse('file: dummy:a')
@ -52,8 +52,8 @@ class PlaylistsHandlerTest(protocol.BaseTestCase):
def test_listplaylistinfo_without_quotes(self):
self.backend.playlists.playlists = [
Playlist(name='name', uri='dummy:name',
tracks=[Track(uri='dummy:a')])]
Playlist(
name='name', uri='dummy:name', tracks=[Track(uri='dummy:a')])]
self.sendRequest('listplaylistinfo name')
self.assertInResponse('file: dummy:a')
@ -107,6 +107,30 @@ class PlaylistsHandlerTest(protocol.BaseTestCase):
self.assertNotInResponse('playlist: ')
self.assertInResponse('OK')
def test_listplaylists_replaces_newline_with_space(self):
self.backend.playlists.playlists = [
Playlist(name='a\n', uri='dummy:')]
self.sendRequest('listplaylists')
self.assertInResponse('playlist: a ')
self.assertNotInResponse('playlist: a\n')
self.assertInResponse('OK')
def test_listplaylists_replaces_carriage_return_with_space(self):
self.backend.playlists.playlists = [
Playlist(name='a\r', uri='dummy:')]
self.sendRequest('listplaylists')
self.assertInResponse('playlist: a ')
self.assertNotInResponse('playlist: a\r')
self.assertInResponse('OK')
def test_listplaylists_replaces_forward_slash_with_space(self):
self.backend.playlists.playlists = [
Playlist(name='a/', uri='dummy:')]
self.sendRequest('listplaylists')
self.assertInResponse('playlist: a ')
self.assertNotInResponse('playlist: a/')
self.assertInResponse('OK')
def test_load_appends_to_tracklist(self):
self.core.tracklist.add([Track(uri='a'), Track(uri='b')])
self.assertEqual(len(self.core.tracklist.tracks.get()), 2)

View File

@ -12,7 +12,10 @@ class HelpTest(unittest.TestCase):
def test_help_has_mopidy_options(self):
mopidy_dir = os.path.dirname(mopidy.__file__)
args = [sys.executable, mopidy_dir, '--help']
process = subprocess.Popen(args, stdout=subprocess.PIPE)
process = subprocess.Popen(
args,
env={'PYTHONPATH': os.path.join(mopidy_dir, '..')},
stdout=subprocess.PIPE)
output = process.communicate()[0]
self.assertIn('--version', output)
self.assertIn('--help', output)

View File

@ -95,6 +95,11 @@ class ArtistTest(unittest.TestCase):
{'__model__': 'Artist', 'uri': 'uri', 'name': 'name'},
Artist(uri='uri', name='name').serialize())
def test_serialize_falsy_values(self):
self.assertDictEqual(
{'__model__': 'Artist', 'uri': '', 'name': None},
Artist(uri='', name=None).serialize())
def test_to_json_and_back(self):
artist1 = Artist(uri='uri', name='name')
serialized = json.dumps(artist1, cls=ModelJSONEncoder)

View File

@ -26,6 +26,8 @@ class TranslatorTest(unittest.TestCase):
'album-artist': 'albumartistname',
'title': 'trackname',
'track-count': 2,
'album-disc-number': 2,
'album-disc-count': 3,
'date': FakeGstDate(2006, 1, 1,),
'container-format': 'ID3 tag',
'duration': 4531,
@ -39,6 +41,7 @@ class TranslatorTest(unittest.TestCase):
self.album = {
'name': 'albumname',
'num_tracks': 2,
'num_discs': 3,
'musicbrainz_id': 'mbalbumid',
}
@ -57,6 +60,7 @@ class TranslatorTest(unittest.TestCase):
'name': 'trackname',
'date': '2006-01-01',
'track_no': 1,
'disc_no': 2,
'length': 4531,
'musicbrainz_id': 'mbtrackid',
'last_modified': 1234,
@ -206,6 +210,14 @@ class ScannerTest(unittest.TestCase):
self.scan('scanner/image')
self.assert_(self.errors)
def test_log_file_is_ignored(self):
self.scan('scanner/example.log')
self.assert_(self.errors)
def test_empty_wav_file_is_ignored(self):
self.scan('scanner/empty.wav')
self.assert_(self.errors)
@unittest.SkipTest
def test_song_without_time_is_handeled(self):
pass

View File

@ -4,7 +4,6 @@ from __future__ import unicode_literals
import os
import shutil
import sys
import tempfile
import unittest
@ -117,86 +116,42 @@ class GetOrCreateFileTest(unittest.TestCase):
class PathToFileURITest(unittest.TestCase):
def test_simple_path(self):
if sys.platform == 'win32':
result = path.path_to_uri('C:/WINDOWS/clock.avi')
self.assertEqual(result, 'file:///C://WINDOWS/clock.avi')
else:
result = path.path_to_uri('/etc/fstab')
self.assertEqual(result, 'file:///etc/fstab')
def test_dir_and_path(self):
if sys.platform == 'win32':
result = path.path_to_uri('C:/WINDOWS/', 'clock.avi')
self.assertEqual(result, 'file:///C://WINDOWS/clock.avi')
else:
result = path.path_to_uri('/etc', 'fstab')
self.assertEqual(result, 'file:///etc/fstab')
result = path.path_to_uri('/etc/fstab')
self.assertEqual(result, 'file:///etc/fstab')
def test_space_in_path(self):
if sys.platform == 'win32':
result = path.path_to_uri('C:/test this')
self.assertEqual(result, 'file:///C://test%20this')
else:
result = path.path_to_uri('/tmp/test this')
self.assertEqual(result, 'file:///tmp/test%20this')
result = path.path_to_uri('/tmp/test this')
self.assertEqual(result, 'file:///tmp/test%20this')
def test_unicode_in_path(self):
if sys.platform == 'win32':
result = path.path_to_uri('C:/æøå')
self.assertEqual(result, 'file:///C://%C3%A6%C3%B8%C3%A5')
else:
result = path.path_to_uri('/tmp/æøå')
self.assertEqual(result, 'file:///tmp/%C3%A6%C3%B8%C3%A5')
result = path.path_to_uri('/tmp/æøå')
self.assertEqual(result, 'file:///tmp/%C3%A6%C3%B8%C3%A5')
def test_utf8_in_path(self):
if sys.platform == 'win32':
result = path.path_to_uri('C:/æøå'.encode('utf-8'))
self.assertEqual(result, 'file:///C://%C3%A6%C3%B8%C3%A5')
else:
result = path.path_to_uri('/tmp/æøå'.encode('utf-8'))
self.assertEqual(result, 'file:///tmp/%C3%A6%C3%B8%C3%A5')
result = path.path_to_uri('/tmp/æøå'.encode('utf-8'))
self.assertEqual(result, 'file:///tmp/%C3%A6%C3%B8%C3%A5')
def test_latin1_in_path(self):
if sys.platform == 'win32':
result = path.path_to_uri('C:/æøå'.encode('latin-1'))
self.assertEqual(result, 'file:///C://%E6%F8%E5')
else:
result = path.path_to_uri('/tmp/æøå'.encode('latin-1'))
self.assertEqual(result, 'file:///tmp/%E6%F8%E5')
result = path.path_to_uri('/tmp/æøå'.encode('latin-1'))
self.assertEqual(result, 'file:///tmp/%E6%F8%E5')
class UriToPathTest(unittest.TestCase):
def test_simple_uri(self):
if sys.platform == 'win32':
result = path.uri_to_path('file:///C://WINDOWS/clock.avi')
self.assertEqual(result, 'C:/WINDOWS/clock.avi'.encode('utf-8'))
else:
result = path.uri_to_path('file:///etc/fstab')
self.assertEqual(result, '/etc/fstab'.encode('utf-8'))
result = path.uri_to_path('file:///etc/fstab')
self.assertEqual(result, '/etc/fstab'.encode('utf-8'))
def test_space_in_uri(self):
if sys.platform == 'win32':
result = path.uri_to_path('file:///C://test%20this')
self.assertEqual(result, 'C:/test this'.encode('utf-8'))
else:
result = path.uri_to_path('file:///tmp/test%20this')
self.assertEqual(result, '/tmp/test this'.encode('utf-8'))
result = path.uri_to_path('file:///tmp/test%20this')
self.assertEqual(result, '/tmp/test this'.encode('utf-8'))
def test_unicode_in_uri(self):
if sys.platform == 'win32':
result = path.uri_to_path('file:///C://%C3%A6%C3%B8%C3%A5')
self.assertEqual(result, 'C:/æøå'.encode('utf-8'))
else:
result = path.uri_to_path('file:///tmp/%C3%A6%C3%B8%C3%A5')
self.assertEqual(result, '/tmp/æøå'.encode('utf-8'))
result = path.uri_to_path('file:///tmp/%C3%A6%C3%B8%C3%A5')
self.assertEqual(result, '/tmp/æøå'.encode('utf-8'))
def test_latin1_in_uri(self):
if sys.platform == 'win32':
result = path.uri_to_path('file:///C://%E6%F8%E5')
self.assertEqual(result, 'C:/æøå'.encode('latin-1'))
else:
result = path.uri_to_path('file:///tmp/%E6%F8%E5')
self.assertEqual(result, '/tmp/æøå'.encode('latin-1'))
result = path.uri_to_path('file:///tmp/%E6%F8%E5')
self.assertEqual(result, '/tmp/æøå'.encode('latin-1'))
class SplitPathTest(unittest.TestCase):

View File

@ -37,5 +37,7 @@ class VersionTest(unittest.TestCase):
self.assertLess(SV('0.11.1'), SV('0.12.0'))
self.assertLess(SV('0.12.0'), SV('0.13.0'))
self.assertLess(SV('0.13.0'), SV('0.14.0'))
self.assertLess(SV('0.14.0'), SV(__version__))
self.assertLess(SV(__version__), SV('0.14.2'))
self.assertLess(SV('0.14.0'), SV('0.14.1'))
self.assertLess(SV('0.14.1'), SV('0.14.2'))
self.assertLess(SV('0.14.2'), SV(__version__))
self.assertLess(SV(__version__), SV('0.15.1'))