Release v0.12.0
This commit is contained in:
commit
fb326ce395
@ -46,5 +46,6 @@ Backend implementations
|
||||
=======================
|
||||
|
||||
* :mod:`mopidy.backends.dummy`
|
||||
* :mod:`mopidy.backends.spotify`
|
||||
* :mod:`mopidy.backends.local`
|
||||
* :mod:`mopidy.backends.spotify`
|
||||
* :mod:`mopidy.backends.stream`
|
||||
|
||||
@ -5,6 +5,66 @@ Changes
|
||||
This change log is used to track all major changes to Mopidy.
|
||||
|
||||
|
||||
v0.12.0 (2013-03-12)
|
||||
====================
|
||||
|
||||
The 0.12 release has been delayed for a while because of some issues related
|
||||
some ongoing GStreamer cleanup we didn't invest enough time to finish. Finally,
|
||||
we've come to our senses and have now cherry-picked the good parts to bring you
|
||||
a new release, while postponing the GStreamer changes to 0.13. The release adds
|
||||
a new backend for playing audio streams, as well as various minor improvements
|
||||
throughout Mopidy.
|
||||
|
||||
- Make Mopidy work on early Python 2.6 versions. (Fixes: :issue:`302`)
|
||||
|
||||
- ``optparse`` fails if the first argument to ``add_option`` is a unicode
|
||||
string on Python < 2.6.2rc1.
|
||||
|
||||
- ``foo(**data)`` fails if the keys in ``data`` is unicode strings on Python
|
||||
< 2.6.5rc1.
|
||||
|
||||
**Audio sub-system**
|
||||
|
||||
- Improve selection of mixer tracks for volume control. (Fixes: :issue:`307`)
|
||||
|
||||
**Local backend**
|
||||
|
||||
- Make ``mopidy-scan`` support symlinks.
|
||||
|
||||
**Stream backend**
|
||||
|
||||
We've added a new backend for playing audio streams, the :mod:`stream backend
|
||||
<mopidy.backends.stream>`. It is activated by default. The stream backend
|
||||
supports the intersection of what your GStreamer installation supports and what
|
||||
protocols are included in the :attr:`mopidy.settings.STREAM_PROTOCOLS` setting.
|
||||
|
||||
Current limitations:
|
||||
|
||||
- No metadata about the current track in the stream is available.
|
||||
|
||||
- Playlists are not parsed, so you can't play e.g. a M3U or PLS file which
|
||||
contains stream URIs. You need to extract the stream URL from the playlist
|
||||
yourself. See :issue:`303` for progress on this.
|
||||
|
||||
**Core API**
|
||||
|
||||
- :meth:`mopidy.core.PlaylistsController.get_playlists` now accepts an argument
|
||||
``include_tracks``. This defaults to :class:`True`, which has the same old
|
||||
behavior. If set to :class:`False`, the tracks are stripped from the
|
||||
playlists before they are returned. This can be used to limit the amount of
|
||||
data returned if the response is to be passed out of the application, e.g. to
|
||||
a web client. (Fixes: :issue:`297`)
|
||||
|
||||
**Models**
|
||||
|
||||
- Add :attr:`mopidy.models.Album.images` field for including album art URIs.
|
||||
(Partly fixes :issue:`263`)
|
||||
|
||||
- Add :attr:`mopidy.models.Track.disc_no` field. (Partly fixes: :issue:`286`)
|
||||
|
||||
- Add :attr:`mopidy.models.Album.num_discs` field. (Partly fixes: :issue:`286`)
|
||||
|
||||
|
||||
v0.11.1 (2012-12-24)
|
||||
====================
|
||||
|
||||
|
||||
7
docs/modules/backends/stream.rst
Normal file
7
docs/modules/backends/stream.rst
Normal file
@ -0,0 +1,7 @@
|
||||
***********************************************
|
||||
:mod:`mopidy.backends.stream` -- Stream backend
|
||||
***********************************************
|
||||
|
||||
.. automodule:: mopidy.backends.stream
|
||||
:synopsis: Backend for playing audio streams
|
||||
:members:
|
||||
5
fabfile.py
vendored
5
fabfile.py
vendored
@ -1,4 +1,4 @@
|
||||
from fabric.api import local
|
||||
from fabric.api import local, settings
|
||||
|
||||
|
||||
def test(path=None):
|
||||
@ -9,7 +9,8 @@ def test(path=None):
|
||||
def autotest(path=None):
|
||||
while True:
|
||||
local('clear')
|
||||
test(path)
|
||||
with settings(warn_only=True):
|
||||
test(path)
|
||||
local(
|
||||
'inotifywait -q -e create -e modify -e delete '
|
||||
'--exclude ".*\.(pyc|sw.)" -r mopidy/ tests/')
|
||||
|
||||
@ -36,40 +36,27 @@ Building from source
|
||||
sudo apt-get update
|
||||
sudo apt-get install nodejs npm
|
||||
|
||||
2. Assuming you install from PPA, setup your ``NODE_PATH`` environment variable
|
||||
to include ``/usr/lib/node_modules``. Add the following to your
|
||||
``~/.bashrc`` or equivalent::
|
||||
|
||||
export NODE_PATH=/usr/lib/node_modules:$NODE_PATH
|
||||
|
||||
3. Install `Buster.js <http://busterjs.org/>`_ and `Grunt
|
||||
<http://gruntjs.com/>`_ globally (or locally, and make sure you get their
|
||||
binaries on your ``PATH``)::
|
||||
|
||||
sudo npm -g install buster grunt
|
||||
|
||||
4. Install the grunt-buster Grunt plugin locally, when in the ``js/`` dir::
|
||||
2. Enter the ``js/`` dir and install development dependencies::
|
||||
|
||||
cd js/
|
||||
npm install grunt-buster
|
||||
npm install
|
||||
|
||||
5. Install `PhantomJS <http://phantomjs.org/>`_ so that we can run the tests
|
||||
without a browser::
|
||||
That's it.
|
||||
|
||||
sudo apt-get install phantomjs
|
||||
You can now run the tests::
|
||||
|
||||
It is packaged in Ubuntu since 12.04, but I haven't tested with versions
|
||||
older than 1.6 which is the one packaged in Ubuntu 12.10.
|
||||
npm test
|
||||
|
||||
6. Run Grunt to lint, test, concatenate, and minify the source::
|
||||
To run tests automatically when you save a file::
|
||||
|
||||
grunt
|
||||
npm run-script watch
|
||||
|
||||
The files in ``../mopidy/frontends/http/data/`` should now be up to date.
|
||||
To run tests, concatenate, minify the source, and update the JavaScript files
|
||||
in ``mopidy/frontends/http/data/``::
|
||||
|
||||
npm run-script build
|
||||
|
||||
Development tips
|
||||
================
|
||||
To run other `grunt <http://gruntjs.com/>`_ targets which isn't predefined in
|
||||
``package.json`` and thus isn't available through ``npm run-script``::
|
||||
|
||||
If you're coding on the JavaScript library, you should know about ``grunt
|
||||
watch``. It lints and tests the code every time you save a file.
|
||||
PATH=./node_modules/.bin:$PATH grunt foo
|
||||
|
||||
@ -64,7 +64,9 @@ module.exports = function (grunt) {
|
||||
uglify: {}
|
||||
});
|
||||
|
||||
grunt.registerTask("default", "lint buster concat min");
|
||||
grunt.registerTask("test", "lint buster");
|
||||
grunt.registerTask("build", "lint buster concat min");
|
||||
grunt.registerTask("default", "build");
|
||||
|
||||
grunt.loadNpmTasks("grunt-buster");
|
||||
};
|
||||
|
||||
15
js/package.json
Normal file
15
js/package.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "mopidy",
|
||||
"version": "0.0.0",
|
||||
"devDependencies": {
|
||||
"buster": "*",
|
||||
"grunt": "*",
|
||||
"grunt-buster": "*",
|
||||
"phantomjs": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "grunt test",
|
||||
"build": "grunt build",
|
||||
"watch": "grunt watch"
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,10 @@
|
||||
/*global bane:false, when:false*/
|
||||
|
||||
function Mopidy(settings) {
|
||||
if (!(this instanceof Mopidy)) {
|
||||
return new Mopidy(settings);
|
||||
}
|
||||
|
||||
this._settings = this._configure(settings || {});
|
||||
this._console = this._getConsole();
|
||||
|
||||
|
||||
@ -48,6 +48,15 @@ buster.testCase("Mopidy", {
|
||||
new Mopidy({webSocket: {}});
|
||||
|
||||
refute.called(this.webSocketConstructorStub);
|
||||
},
|
||||
|
||||
"works without 'new' keyword": function () {
|
||||
var mopidyConstructor = Mopidy; // To trick jshint into submission
|
||||
|
||||
var mopidy = mopidyConstructor({webSocket: {}});
|
||||
|
||||
assert.isObject(mopidy);
|
||||
assert(mopidy instanceof Mopidy);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ if (isinstance(pykka.__version__, basestring)
|
||||
warnings.filterwarnings('ignore', 'could not open display')
|
||||
|
||||
|
||||
__version__ = '0.11.1'
|
||||
__version__ = '0.12.0'
|
||||
|
||||
|
||||
from mopidy import settings as default_settings_module
|
||||
|
||||
@ -79,37 +79,39 @@ def main():
|
||||
def parse_options():
|
||||
parser = optparse.OptionParser(
|
||||
version='Mopidy %s' % versioning.get_version())
|
||||
# NOTE First argument to add_option must be bytestrings on Python < 2.6.2
|
||||
# See https://github.com/mopidy/mopidy/issues/302 for details
|
||||
parser.add_option(
|
||||
'--help-gst',
|
||||
b'--help-gst',
|
||||
action='store_true', dest='help_gst',
|
||||
help='show GStreamer help options')
|
||||
parser.add_option(
|
||||
'-i', '--interactive',
|
||||
b'-i', '--interactive',
|
||||
action='store_true', dest='interactive',
|
||||
help='ask interactively for required settings which are missing')
|
||||
parser.add_option(
|
||||
'-q', '--quiet',
|
||||
b'-q', '--quiet',
|
||||
action='store_const', const=0, dest='verbosity_level',
|
||||
help='less output (warning level)')
|
||||
parser.add_option(
|
||||
'-v', '--verbose',
|
||||
b'-v', '--verbose',
|
||||
action='count', default=1, dest='verbosity_level',
|
||||
help='more output (debug level)')
|
||||
parser.add_option(
|
||||
'--save-debug-log',
|
||||
b'--save-debug-log',
|
||||
action='store_true', dest='save_debug_log',
|
||||
help='save debug log to "./mopidy.log"')
|
||||
parser.add_option(
|
||||
'--list-settings',
|
||||
b'--list-settings',
|
||||
action='callback',
|
||||
callback=settings_utils.list_settings_optparse_callback,
|
||||
help='list current settings')
|
||||
parser.add_option(
|
||||
'--list-deps',
|
||||
b'--list-deps',
|
||||
action='callback', callback=deps.list_deps_optparse_callback,
|
||||
help='list dependencies and their versions')
|
||||
parser.add_option(
|
||||
'--debug-thread',
|
||||
b'--debug-thread',
|
||||
action='store_true', dest='debug_thread',
|
||||
help='run background thread that dumps tracebacks on SIGUSR1')
|
||||
return parser.parse_args(args=mopidy_args)[0]
|
||||
|
||||
@ -158,15 +158,23 @@ class Audio(pykka.ThreadingActor):
|
||||
mixer.get_factory().get_name(), track.label)
|
||||
|
||||
def _select_mixer_track(self, mixer, track_label):
|
||||
# Look for track with label == MIXER_TRACK, otherwise fallback to
|
||||
# master track which is also an output.
|
||||
# Ignore tracks without volumes, then look for track with
|
||||
# label == settings.MIXER_TRACK, otherwise fallback to first usable
|
||||
# track hoping the mixer gave them to us in a sensible order.
|
||||
|
||||
usable_tracks = []
|
||||
for track in mixer.list_tracks():
|
||||
if track_label:
|
||||
if track.label == track_label:
|
||||
return track
|
||||
if not mixer.get_volume(track):
|
||||
continue
|
||||
|
||||
if track_label and track.label == track_label:
|
||||
return track
|
||||
elif track.flags & (gst.interfaces.MIXER_TRACK_MASTER |
|
||||
gst.interfaces.MIXER_TRACK_OUTPUT):
|
||||
return track
|
||||
usable_tracks.append(track)
|
||||
|
||||
if usable_tracks:
|
||||
return usable_tracks[0]
|
||||
|
||||
def _teardown_mixer(self):
|
||||
if self._mixer is not None:
|
||||
|
||||
23
mopidy/audio/utils.py
Normal file
23
mopidy/audio/utils.py
Normal file
@ -0,0 +1,23 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gst
|
||||
|
||||
|
||||
def supported_uri_schemes(uri_schemes):
|
||||
"""Determine which URIs we can actually support from provided whitelist.
|
||||
|
||||
:param uri_schemes: list/set of URIs to check support for.
|
||||
:type uri_schemes: list or set or URI schemes as strings.
|
||||
:rtype: set of URI schemes we can support via this GStreamer install.
|
||||
"""
|
||||
supported_schemes= set()
|
||||
registry = gst.registry_get_default()
|
||||
|
||||
for factory in registry.get_feature_list(gst.TYPE_ELEMENT_FACTORY):
|
||||
for uri in factory.get_uri_protocols():
|
||||
if uri in uri_schemes:
|
||||
supported_schemes.add(uri)
|
||||
|
||||
return supported_schemes
|
||||
@ -57,9 +57,9 @@ class BaseLibraryProvider(object):
|
||||
"""
|
||||
See :meth:`mopidy.core.LibraryController.find_exact`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
*MAY be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
pass
|
||||
|
||||
def lookup(self, uri):
|
||||
"""
|
||||
@ -73,17 +73,17 @@ class BaseLibraryProvider(object):
|
||||
"""
|
||||
See :meth:`mopidy.core.LibraryController.refresh`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
*MAY be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
pass
|
||||
|
||||
def search(self, **query):
|
||||
"""
|
||||
See :meth:`mopidy.core.LibraryController.search`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
*MAY be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
pass
|
||||
|
||||
|
||||
class BasePlaybackProvider(object):
|
||||
|
||||
@ -98,6 +98,9 @@ def _convert_mpd_data(data, tracks, music_dir):
|
||||
if not data:
|
||||
return
|
||||
|
||||
# NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5
|
||||
# See https://github.com/mopidy/mopidy/issues/302 for details.
|
||||
|
||||
track_kwargs = {}
|
||||
album_kwargs = {}
|
||||
artist_kwargs = {}
|
||||
@ -105,38 +108,38 @@ def _convert_mpd_data(data, tracks, music_dir):
|
||||
|
||||
if 'track' in data:
|
||||
if '/' in data['track']:
|
||||
album_kwargs['num_tracks'] = int(data['track'].split('/')[1])
|
||||
track_kwargs['track_no'] = int(data['track'].split('/')[0])
|
||||
album_kwargs[b'num_tracks'] = int(data['track'].split('/')[1])
|
||||
track_kwargs[b'track_no'] = int(data['track'].split('/')[0])
|
||||
else:
|
||||
track_kwargs['track_no'] = int(data['track'])
|
||||
track_kwargs[b'track_no'] = int(data['track'])
|
||||
|
||||
if 'artist' in data:
|
||||
artist_kwargs['name'] = data['artist']
|
||||
albumartist_kwargs['name'] = data['artist']
|
||||
artist_kwargs[b'name'] = data['artist']
|
||||
albumartist_kwargs[b'name'] = data['artist']
|
||||
|
||||
if 'albumartist' in data:
|
||||
albumartist_kwargs['name'] = data['albumartist']
|
||||
albumartist_kwargs[b'name'] = data['albumartist']
|
||||
|
||||
if 'album' in data:
|
||||
album_kwargs['name'] = data['album']
|
||||
album_kwargs[b'name'] = data['album']
|
||||
|
||||
if 'title' in data:
|
||||
track_kwargs['name'] = data['title']
|
||||
track_kwargs[b'name'] = data['title']
|
||||
|
||||
if 'date' in data:
|
||||
track_kwargs['date'] = data['date']
|
||||
track_kwargs[b'date'] = data['date']
|
||||
|
||||
if 'musicbrainz_trackid' in data:
|
||||
track_kwargs['musicbrainz_id'] = data['musicbrainz_trackid']
|
||||
track_kwargs[b'musicbrainz_id'] = data['musicbrainz_trackid']
|
||||
|
||||
if 'musicbrainz_albumid' in data:
|
||||
album_kwargs['musicbrainz_id'] = data['musicbrainz_albumid']
|
||||
album_kwargs[b'musicbrainz_id'] = data['musicbrainz_albumid']
|
||||
|
||||
if 'musicbrainz_artistid' in data:
|
||||
artist_kwargs['musicbrainz_id'] = data['musicbrainz_artistid']
|
||||
artist_kwargs[b'musicbrainz_id'] = data['musicbrainz_artistid']
|
||||
|
||||
if 'musicbrainz_albumartistid' in data:
|
||||
albumartist_kwargs['musicbrainz_id'] = (
|
||||
albumartist_kwargs[b'musicbrainz_id'] = (
|
||||
data['musicbrainz_albumartistid'])
|
||||
|
||||
if data['file'][0] == '/':
|
||||
@ -147,18 +150,18 @@ def _convert_mpd_data(data, tracks, music_dir):
|
||||
|
||||
if artist_kwargs:
|
||||
artist = Artist(**artist_kwargs)
|
||||
track_kwargs['artists'] = [artist]
|
||||
track_kwargs[b'artists'] = [artist]
|
||||
|
||||
if albumartist_kwargs:
|
||||
albumartist = Artist(**albumartist_kwargs)
|
||||
album_kwargs['artists'] = [albumartist]
|
||||
album_kwargs[b'artists'] = [albumartist]
|
||||
|
||||
if album_kwargs:
|
||||
album = Album(**album_kwargs)
|
||||
track_kwargs['album'] = album
|
||||
track_kwargs[b'album'] = album
|
||||
|
||||
track_kwargs['uri'] = path_to_uri(music_dir, path)
|
||||
track_kwargs['length'] = int(data.get('time', 0)) * 1000
|
||||
track_kwargs[b'uri'] = path_to_uri(music_dir, path)
|
||||
track_kwargs[b'length'] = int(data.get('time', 0)) * 1000
|
||||
|
||||
track = Track(**track_kwargs)
|
||||
tracks.add(track)
|
||||
|
||||
23
mopidy/backends/stream/__init__.py
Normal file
23
mopidy/backends/stream/__init__.py
Normal file
@ -0,0 +1,23 @@
|
||||
"""A backend for playing music for streaming music.
|
||||
|
||||
This backend will handle streaming of URIs in
|
||||
:attr:`mopidy.settings.STREAM_PROTOCOLS` assuming the right plugins are
|
||||
installed.
|
||||
|
||||
**Issues:**
|
||||
|
||||
https://github.com/mopidy/mopidy/issues?labels=Stream+backend
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
- None
|
||||
|
||||
**Settings:**
|
||||
|
||||
- :attr:`mopidy.settings.STREAM_PROTOCOLS`
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# flake8: noqa
|
||||
from .actor import StreamBackend
|
||||
38
mopidy/backends/stream/actor.py
Normal file
38
mopidy/backends/stream/actor.py
Normal file
@ -0,0 +1,38 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import urlparse
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.audio import utils
|
||||
from mopidy.backends import base
|
||||
from mopidy.models import SearchResult, Track
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.stream')
|
||||
|
||||
|
||||
class StreamBackend(pykka.ThreadingActor, base.Backend):
|
||||
def __init__(self, audio):
|
||||
super(StreamBackend, self).__init__()
|
||||
|
||||
self.library = StreamLibraryProvider(backend=self)
|
||||
self.playback = base.BasePlaybackProvider(audio=audio, backend=self)
|
||||
self.playlists = None
|
||||
|
||||
self.uri_schemes = utils.supported_uri_schemes(
|
||||
settings.STREAM_PROTOCOLS)
|
||||
|
||||
|
||||
# TODO: Should we consider letting lookup know how to expand common playlist
|
||||
# formats (m3u, pls, etc) for http(s) URIs?
|
||||
class StreamLibraryProvider(base.BaseLibraryProvider):
|
||||
def lookup(self, uri):
|
||||
if urlparse.urlsplit(uri).scheme not in self.backend.uri_schemes:
|
||||
return []
|
||||
# TODO: actually lookup the stream metadata by getting tags in same
|
||||
# way as we do for updating the local library with mopidy.scanner
|
||||
# Note that we would only want the stream metadata at this stage,
|
||||
# not the currently playing track's.
|
||||
return [Track(uri=uri, name=uri)]
|
||||
@ -41,7 +41,7 @@ class LibraryController(object):
|
||||
query = query or kwargs
|
||||
futures = [
|
||||
b.library.find_exact(**query) for b in self.backends.with_library]
|
||||
return pykka.get_all(futures)
|
||||
return [result for result in pykka.get_all(futures) if result]
|
||||
|
||||
def lookup(self, uri):
|
||||
"""
|
||||
@ -101,4 +101,4 @@ class LibraryController(object):
|
||||
query = query or kwargs
|
||||
futures = [
|
||||
b.library.search(**query) for b in self.backends.with_library]
|
||||
return pykka.get_all(futures)
|
||||
return [result for result in pykka.get_all(futures) if result]
|
||||
|
||||
@ -15,11 +15,14 @@ class PlaylistsController(object):
|
||||
self.backends = backends
|
||||
self.core = core
|
||||
|
||||
def get_playlists(self):
|
||||
def get_playlists(self, include_tracks=True):
|
||||
futures = [
|
||||
b.playlists.playlists for b in self.backends.with_playlists]
|
||||
results = pykka.get_all(futures)
|
||||
return list(itertools.chain(*results))
|
||||
playlists = list(itertools.chain(*results))
|
||||
if not include_tracks:
|
||||
playlists = [p.copy(tracks=[]) for p in playlists]
|
||||
return playlists
|
||||
|
||||
playlists = property(get_playlists)
|
||||
"""
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/*! Mopidy.js - built 2012-12-04
|
||||
/*! Mopidy.js - built 2013-01-16
|
||||
* http://www.mopidy.com/
|
||||
* Copyright (c) 2012 Stein Magnus Jodal and contributors
|
||||
* Copyright (c) 2013 Stein Magnus Jodal and contributors
|
||||
* Licensed under the Apache License, Version 2.0 */
|
||||
|
||||
/**
|
||||
@ -910,6 +910,10 @@ define(['module'], function () {
|
||||
/*global bane:false, when:false*/
|
||||
|
||||
function Mopidy(settings) {
|
||||
if (!(this instanceof Mopidy)) {
|
||||
return new Mopidy(settings);
|
||||
}
|
||||
|
||||
this._settings = this._configure(settings || {});
|
||||
this._console = this._getConsole();
|
||||
|
||||
|
||||
6
mopidy/frontends/http/data/mopidy.min.js
vendored
6
mopidy/frontends/http/data/mopidy.min.js
vendored
File diff suppressed because one or more lines are too long
@ -19,10 +19,12 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener):
|
||||
hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME)
|
||||
port = settings.MPD_SERVER_PORT
|
||||
|
||||
# NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5
|
||||
# See https://github.com/mopidy/mopidy/issues/302 for details.
|
||||
try:
|
||||
network.Server(
|
||||
hostname, port,
|
||||
protocol=session.MpdSession, protocol_kwargs={'core': core},
|
||||
protocol=session.MpdSession, protocol_kwargs={b'core': core},
|
||||
max_connections=settings.MPD_SERVER_MAX_CONNECTIONS,
|
||||
timeout=settings.MPD_SERVER_CONNECTION_TIMEOUT)
|
||||
except IOError as error:
|
||||
|
||||
@ -56,7 +56,12 @@ def handle_request(pattern, auth_required=True):
|
||||
if match is not None:
|
||||
mpd_commands.add(
|
||||
MpdCommand(name=match.group(), auth_required=auth_required))
|
||||
compiled_pattern = re.compile(pattern, flags=re.UNICODE)
|
||||
# NOTE Make pattern a bytestring to get bytestring keys in the dict
|
||||
# returned from matches.groupdict(), which is again used as a **kwargs
|
||||
# dict. This is needed to work on Python < 2.6.5.
|
||||
# See https://github.com/mopidy/mopidy/issues/302 for details.
|
||||
bytestring_pattern = pattern.encode('utf-8')
|
||||
compiled_pattern = re.compile(bytestring_pattern, flags=re.UNICODE)
|
||||
if compiled_pattern in request_handlers:
|
||||
raise ValueError('Tried to redefine handler for %s with %s' % (
|
||||
pattern, func))
|
||||
|
||||
@ -140,6 +140,8 @@ def query_from_mpd_list_format(field, mpd_query):
|
||||
"""
|
||||
Converts an MPD ``list`` query to a Mopidy query.
|
||||
"""
|
||||
# NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5
|
||||
# See https://github.com/mopidy/mopidy/issues/302 for details
|
||||
if mpd_query is None:
|
||||
return {}
|
||||
try:
|
||||
@ -155,15 +157,14 @@ def query_from_mpd_list_format(field, mpd_query):
|
||||
if field == 'album':
|
||||
if not tokens[0]:
|
||||
raise ValueError
|
||||
return {'artist': [tokens[0]]}
|
||||
return {b'artist': [tokens[0]]} # See above NOTE
|
||||
else:
|
||||
raise MpdArgError(
|
||||
'should be "Album" for 3 arguments', command='list')
|
||||
elif len(tokens) % 2 == 0:
|
||||
query = {}
|
||||
while tokens:
|
||||
key = tokens[0].lower()
|
||||
key = str(key) # Needed for kwargs keys on OS X and Windows
|
||||
key = str(tokens[0].lower()) # See above NOTE
|
||||
value = tokens[1]
|
||||
tokens = tokens[2:]
|
||||
if key not in ('artist', 'album', 'date', 'genre'):
|
||||
|
||||
@ -66,13 +66,15 @@ class ImmutableObject(object):
|
||||
:type values: dict
|
||||
:rtype: new instance of the model being copied
|
||||
"""
|
||||
# NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5
|
||||
# See https://github.com/mopidy/mopidy/issues/302 for details
|
||||
data = {}
|
||||
for key in self.__dict__.keys():
|
||||
public_key = key.lstrip('_')
|
||||
data[public_key] = values.pop(public_key, self.__dict__[key])
|
||||
data[str(public_key)] = values.pop(public_key, self.__dict__[key])
|
||||
for key in values.keys():
|
||||
if hasattr(self, key):
|
||||
data[key] = values.pop(key)
|
||||
data[str(key)] = values.pop(key)
|
||||
if values:
|
||||
raise TypeError(
|
||||
'copy() got an unexpected keyword argument "%s"' % key)
|
||||
@ -85,7 +87,9 @@ class ImmutableObject(object):
|
||||
public_key = key.lstrip('_')
|
||||
value = self.__dict__[key]
|
||||
if isinstance(value, (set, frozenset, list, tuple)):
|
||||
value = [o.serialize() for o in value]
|
||||
value = [
|
||||
v.serialize() if isinstance(v, ImmutableObject) else v
|
||||
for v in value]
|
||||
elif isinstance(value, ImmutableObject):
|
||||
value = value.serialize()
|
||||
if value:
|
||||
@ -123,11 +127,16 @@ def model_json_decoder(dct):
|
||||
{u'a_track': Track(artists=[], name=u'name')}
|
||||
|
||||
"""
|
||||
# NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5
|
||||
# See https://github.com/mopidy/mopidy/issues/302 for details.
|
||||
if '__model__' in dct:
|
||||
model_name = dct.pop('__model__')
|
||||
cls = globals().get(model_name, None)
|
||||
if issubclass(cls, ImmutableObject):
|
||||
return cls(**dct)
|
||||
kwargs = {}
|
||||
for key, value in dct.items():
|
||||
kwargs[str(key)] = value
|
||||
return cls(**kwargs)
|
||||
return dct
|
||||
|
||||
|
||||
@ -161,10 +170,14 @@ class Album(ImmutableObject):
|
||||
:type artists: list of :class:`Artist`
|
||||
:param num_tracks: number of tracks in album
|
||||
:type num_tracks: integer
|
||||
:param num_discs: number of discs in album
|
||||
:type num_discs: integer or :class:`None` if unknown
|
||||
:param date: album release date (YYYY or YYYY-MM-DD)
|
||||
:type date: string
|
||||
:param musicbrainz_id: MusicBrainz ID
|
||||
:type musicbrainz_id: string
|
||||
:param images: album image URIs
|
||||
:type images: list of strings
|
||||
"""
|
||||
|
||||
#: The album URI. Read-only.
|
||||
@ -179,14 +192,23 @@ class Album(ImmutableObject):
|
||||
#: The number of tracks in the album. Read-only.
|
||||
num_tracks = 0
|
||||
|
||||
#: The number of discs in the album. Read-only.
|
||||
num_discs = None
|
||||
|
||||
#: The album release date. Read-only.
|
||||
date = None
|
||||
|
||||
#: The MusicBrainz ID of the album. Read-only.
|
||||
musicbrainz_id = None
|
||||
|
||||
#: The album image URIs. Read-only.
|
||||
images = frozenset()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.__dict__['artists'] = frozenset(kwargs.pop('artists', []))
|
||||
# NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5
|
||||
# See https://github.com/mopidy/mopidy/issues/302 for details
|
||||
self.__dict__[b'artists'] = frozenset(kwargs.pop('artists', []))
|
||||
self.__dict__[b'images'] = frozenset(kwargs.pop('images', []))
|
||||
super(Album, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
@ -202,6 +224,8 @@ class Track(ImmutableObject):
|
||||
:type album: :class:`Album`
|
||||
:param track_no: track number in album
|
||||
:type track_no: integer
|
||||
:param disc_no: disc number in album
|
||||
:type disc_no: integer or :class:`None` if unknown
|
||||
:param date: track release date (YYYY or YYYY-MM-DD)
|
||||
:type date: string
|
||||
:param length: track length in milliseconds
|
||||
@ -224,9 +248,12 @@ class Track(ImmutableObject):
|
||||
#: The track :class:`Album`. Read-only.
|
||||
album = None
|
||||
|
||||
#: The track number in album. Read-only.
|
||||
#: The track number in the album. Read-only.
|
||||
track_no = 0
|
||||
|
||||
#: The disc number in the album. Read-only.
|
||||
disc_no = None
|
||||
|
||||
#: The track release date. Read-only.
|
||||
date = None
|
||||
|
||||
@ -240,7 +267,9 @@ class Track(ImmutableObject):
|
||||
musicbrainz_id = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.__dict__['artists'] = frozenset(kwargs.pop('artists', []))
|
||||
# NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5
|
||||
# See https://github.com/mopidy/mopidy/issues/302 for details
|
||||
self.__dict__[b'artists'] = frozenset(kwargs.pop('artists', []))
|
||||
super(Track, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
@ -272,9 +301,11 @@ class TlTrack(ImmutableObject):
|
||||
track = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5
|
||||
# See https://github.com/mopidy/mopidy/issues/302 for details
|
||||
if len(args) == 2 and len(kwargs) == 0:
|
||||
kwargs['tlid'] = args[0]
|
||||
kwargs['track'] = args[1]
|
||||
kwargs[b'tlid'] = args[0]
|
||||
kwargs[b'track'] = args[1]
|
||||
args = []
|
||||
super(TlTrack, self).__init__(*args, **kwargs)
|
||||
|
||||
@ -309,7 +340,9 @@ class Playlist(ImmutableObject):
|
||||
last_modified = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.__dict__['tracks'] = tuple(kwargs.pop('tracks', []))
|
||||
# NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5
|
||||
# See https://github.com/mopidy/mopidy/issues/302 for details
|
||||
self.__dict__[b'tracks'] = tuple(kwargs.pop('tracks', []))
|
||||
super(Playlist, self).__init__(*args, **kwargs)
|
||||
|
||||
# TODO: def insert(self, pos, track): ... ?
|
||||
@ -345,7 +378,9 @@ class SearchResult(ImmutableObject):
|
||||
albums = tuple()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.__dict__['tracks'] = tuple(kwargs.pop('tracks', []))
|
||||
self.__dict__['artists'] = tuple(kwargs.pop('artists', []))
|
||||
self.__dict__['albums'] = tuple(kwargs.pop('albums', []))
|
||||
# NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5
|
||||
# See https://github.com/mopidy/mopidy/issues/302 for details
|
||||
self.__dict__[b'tracks'] = tuple(kwargs.pop('tracks', []))
|
||||
self.__dict__[b'artists'] = tuple(kwargs.pop('artists', []))
|
||||
self.__dict__[b'albums'] = tuple(kwargs.pop('albums', []))
|
||||
super(SearchResult, self).__init__(*args, **kwargs)
|
||||
|
||||
@ -79,12 +79,14 @@ def main():
|
||||
def parse_options():
|
||||
parser = optparse.OptionParser(
|
||||
version='Mopidy %s' % versioning.get_version())
|
||||
# NOTE First argument to add_option must be bytestrings on Python < 2.6.2
|
||||
# See https://github.com/mopidy/mopidy/issues/302 for details
|
||||
parser.add_option(
|
||||
'-q', '--quiet',
|
||||
b'-q', '--quiet',
|
||||
action='store_const', const=0, dest='verbosity_level',
|
||||
help='less output (warning level)')
|
||||
parser.add_option(
|
||||
'-v', '--verbose',
|
||||
b'-v', '--verbose',
|
||||
action='count', default=1, dest='verbosity_level',
|
||||
help='more output (debug level)')
|
||||
return parser.parse_args(args=mopidy_args)[0]
|
||||
@ -96,9 +98,12 @@ def translator(data):
|
||||
artist_kwargs = {}
|
||||
track_kwargs = {}
|
||||
|
||||
# NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5
|
||||
# See https://github.com/mopidy/mopidy/issues/302 for details.
|
||||
|
||||
def _retrieve(source_key, target_key, target):
|
||||
if source_key in data:
|
||||
target[target_key] = data[source_key]
|
||||
target[str(target_key)] = data[source_key]
|
||||
|
||||
_retrieve(gst.TAG_ALBUM, 'name', album_kwargs)
|
||||
_retrieve(gst.TAG_TRACK_COUNT, 'num_tracks', album_kwargs)
|
||||
@ -111,7 +116,7 @@ def translator(data):
|
||||
except ValueError:
|
||||
pass # Ignore invalid dates
|
||||
else:
|
||||
track_kwargs['date'] = date.isoformat()
|
||||
track_kwargs[b'date'] = date.isoformat()
|
||||
|
||||
_retrieve(gst.TAG_TITLE, 'name', track_kwargs)
|
||||
_retrieve(gst.TAG_TRACK_NUMBER, 'track_no', track_kwargs)
|
||||
@ -125,12 +130,12 @@ def translator(data):
|
||||
'musicbrainz-albumartistid', 'musicbrainz_id', albumartist_kwargs)
|
||||
|
||||
if albumartist_kwargs:
|
||||
album_kwargs['artists'] = [Artist(**albumartist_kwargs)]
|
||||
album_kwargs[b'artists'] = [Artist(**albumartist_kwargs)]
|
||||
|
||||
track_kwargs['uri'] = data['uri']
|
||||
track_kwargs['length'] = data[gst.TAG_DURATION]
|
||||
track_kwargs['album'] = Album(**album_kwargs)
|
||||
track_kwargs['artists'] = [Artist(**artist_kwargs)]
|
||||
track_kwargs[b'uri'] = data['uri']
|
||||
track_kwargs[b'length'] = data[gst.TAG_DURATION]
|
||||
track_kwargs[b'album'] = Album(**album_kwargs)
|
||||
track_kwargs[b'artists'] = [Artist(**artist_kwargs)]
|
||||
|
||||
return Track(**track_kwargs)
|
||||
|
||||
|
||||
@ -20,10 +20,12 @@ from __future__ import unicode_literals
|
||||
#: BACKENDS = (
|
||||
#: u'mopidy.backends.local.LocalBackend',
|
||||
#: u'mopidy.backends.spotify.SpotifyBackend',
|
||||
#: u'mopidy.backends.spotify.StreamBackend',
|
||||
#: )
|
||||
BACKENDS = (
|
||||
'mopidy.backends.local.LocalBackend',
|
||||
'mopidy.backends.spotify.SpotifyBackend',
|
||||
'mopidy.backends.stream.StreamBackend',
|
||||
)
|
||||
|
||||
#: The log format used for informational logging.
|
||||
@ -286,7 +288,7 @@ SPOTIFY_PROXY_USERNAME = None
|
||||
|
||||
#: Spotify proxy password.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.backends.spotify`
|
||||
#: Used by :mod:`mopidy.backends.spotify`.
|
||||
#:
|
||||
#: Default::
|
||||
#:
|
||||
@ -295,9 +297,32 @@ SPOTIFY_PROXY_PASSWORD = None
|
||||
|
||||
#: Max number of seconds to wait for Spotify operations to complete.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.backends.spotify`
|
||||
#: Used by :mod:`mopidy.backends.spotify`.
|
||||
#:
|
||||
#: Default::
|
||||
#:
|
||||
#: SPOTIFY_TIMEOUT = 10
|
||||
SPOTIFY_TIMEOUT = 10
|
||||
|
||||
#: Whitelist of URIs to support streaming from.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.backends.stream`.
|
||||
#:
|
||||
#: Default::
|
||||
#:
|
||||
#: STREAM_PROTOCOLS = (
|
||||
#: u'http',
|
||||
#: u'https',
|
||||
#: u'mms',
|
||||
#: u'rtmp',
|
||||
#: u'rtmps',
|
||||
#: u'rtsp',
|
||||
#: )
|
||||
STREAM_PROTOCOLS = (
|
||||
'http',
|
||||
'https',
|
||||
'mms',
|
||||
'rtmp',
|
||||
'rtmps',
|
||||
'rtsp',
|
||||
)
|
||||
|
||||
@ -120,7 +120,7 @@ def find_files(path):
|
||||
if not os.path.basename(path).startswith(b'.'):
|
||||
yield path
|
||||
else:
|
||||
for dirpath, dirnames, filenames in os.walk(path):
|
||||
for dirpath, dirnames, filenames in os.walk(path, followlinks=True):
|
||||
for dirname in dirnames:
|
||||
if dirname.startswith(b'.'):
|
||||
# Skip hidden folders by modifying dirnames inplace
|
||||
|
||||
@ -101,7 +101,7 @@ class DebugThread(threading.Thread):
|
||||
stack = ''.join(traceback.format_stack(frame))
|
||||
logger.debug(
|
||||
'Current state of %s (%s):\n%s',
|
||||
threads[ident], ident, stack)
|
||||
threads.get(ident, '?'), ident, stack)
|
||||
del frame
|
||||
|
||||
self.event.clear()
|
||||
|
||||
@ -11,7 +11,7 @@ from mopidy.backends import listener
|
||||
class BackendEventsTest(object):
|
||||
def setUp(self):
|
||||
self.audio = mock.Mock(spec=audio.Audio)
|
||||
self.backend = self.backend_class.start(audio=audio).proxy()
|
||||
self.backend = self.backend_class.start(audio=self.audio).proxy()
|
||||
self.core = core.Core.start(backends=[self.backend]).proxy()
|
||||
|
||||
def tearDown(self):
|
||||
|
||||
@ -90,6 +90,22 @@ class CoreLibraryTest(unittest.TestCase):
|
||||
self.library1.find_exact.assert_called_once_with(any=['a'])
|
||||
self.library2.find_exact.assert_called_once_with(any=['a'])
|
||||
|
||||
def test_find_exact_filters_out_none(self):
|
||||
track1 = Track(uri='dummy1:a')
|
||||
result1 = SearchResult(tracks=[track1])
|
||||
|
||||
self.library1.find_exact().get.return_value = result1
|
||||
self.library1.find_exact.reset_mock()
|
||||
self.library2.find_exact().get.return_value = None
|
||||
self.library2.find_exact.reset_mock()
|
||||
|
||||
result = self.core.library.find_exact(any=['a'])
|
||||
|
||||
self.assertIn(result1, result)
|
||||
self.assertNotIn(None, result)
|
||||
self.library1.find_exact.assert_called_once_with(any=['a'])
|
||||
self.library2.find_exact.assert_called_once_with(any=['a'])
|
||||
|
||||
def test_find_accepts_query_dict_instead_of_kwargs(self):
|
||||
track1 = Track(uri='dummy1:a')
|
||||
track2 = Track(uri='dummy2:a')
|
||||
@ -126,6 +142,22 @@ class CoreLibraryTest(unittest.TestCase):
|
||||
self.library1.search.assert_called_once_with(any=['a'])
|
||||
self.library2.search.assert_called_once_with(any=['a'])
|
||||
|
||||
def test_search_filters_out_none(self):
|
||||
track1 = Track(uri='dummy1:a')
|
||||
result1 = SearchResult(tracks=[track1])
|
||||
|
||||
self.library1.search().get.return_value = result1
|
||||
self.library1.search.reset_mock()
|
||||
self.library2.search().get.return_value = None
|
||||
self.library2.search.reset_mock()
|
||||
|
||||
result = self.core.library.search(any=['a'])
|
||||
|
||||
self.assertIn(result1, result)
|
||||
self.assertNotIn(None, result)
|
||||
self.library1.search.assert_called_once_with(any=['a'])
|
||||
self.library2.search.assert_called_once_with(any=['a'])
|
||||
|
||||
def test_search_accepts_query_dict_instead_of_kwargs(self):
|
||||
track1 = Track(uri='dummy1:a')
|
||||
track2 = Track(uri='dummy2:a')
|
||||
|
||||
@ -46,6 +46,22 @@ class PlaylistsTest(unittest.TestCase):
|
||||
self.assertIn(self.pl2a, result)
|
||||
self.assertIn(self.pl2b, result)
|
||||
|
||||
def test_get_playlists_includes_tracks_by_default(self):
|
||||
result = self.core.playlists.get_playlists()
|
||||
|
||||
self.assertEqual(result[0].name, 'A')
|
||||
self.assertEqual(len(result[0].tracks), 1)
|
||||
self.assertEqual(result[1].name, 'B')
|
||||
self.assertEqual(len(result[1].tracks), 1)
|
||||
|
||||
def test_get_playlist_can_strip_tracks_from_returned_playlists(self):
|
||||
result = self.core.playlists.get_playlists(include_tracks=False)
|
||||
|
||||
self.assertEqual(result[0].name, 'A')
|
||||
self.assertEqual(len(result[0].tracks), 0)
|
||||
self.assertEqual(result[1].name, 'B')
|
||||
self.assertEqual(len(result[1].tracks), 0)
|
||||
|
||||
def test_create_without_uri_scheme_uses_first_backend(self):
|
||||
playlist = Playlist()
|
||||
self.sp1.create().get.return_value = playlist
|
||||
|
||||
@ -199,10 +199,16 @@ class AlbumTest(unittest.TestCase):
|
||||
|
||||
def test_num_tracks(self):
|
||||
num_tracks = 11
|
||||
album = Album(num_tracks=11)
|
||||
album = Album(num_tracks=num_tracks)
|
||||
self.assertEqual(album.num_tracks, num_tracks)
|
||||
self.assertRaises(AttributeError, setattr, album, 'num_tracks', None)
|
||||
|
||||
def test_num_discs(self):
|
||||
num_discs = 2
|
||||
album = Album(num_discs=num_discs)
|
||||
self.assertEqual(album.num_discs, num_discs)
|
||||
self.assertRaises(AttributeError, setattr, album, 'num_discs', None)
|
||||
|
||||
def test_date(self):
|
||||
date = '1977-01-01'
|
||||
album = Album(date=date)
|
||||
@ -216,18 +222,25 @@ class AlbumTest(unittest.TestCase):
|
||||
self.assertRaises(
|
||||
AttributeError, setattr, album, 'musicbrainz_id', None)
|
||||
|
||||
def test_images(self):
|
||||
image = 'data:foobar'
|
||||
album = Album(images=[image])
|
||||
self.assertIn(image, album.images)
|
||||
self.assertRaises(AttributeError, setattr, album, 'images', None)
|
||||
|
||||
def test_invalid_kwarg(self):
|
||||
test = lambda: Album(foo='baz')
|
||||
self.assertRaises(TypeError, test)
|
||||
|
||||
def test_repr_without_artists(self):
|
||||
self.assertEquals(
|
||||
"Album(artists=[], name=u'name', uri=u'uri')",
|
||||
"Album(artists=[], images=[], name=u'name', uri=u'uri')",
|
||||
repr(Album(uri='uri', name='name')))
|
||||
|
||||
def test_repr_with_artists(self):
|
||||
self.assertEquals(
|
||||
"Album(artists=[Artist(name=u'foo')], name=u'name', uri=u'uri')",
|
||||
"Album(artists=[Artist(name=u'foo')], images=[], name=u'name', "
|
||||
"uri=u'uri')",
|
||||
repr(Album(uri='uri', name='name', artists=[Artist(name='foo')])))
|
||||
|
||||
def test_serialize_without_artists(self):
|
||||
@ -242,6 +255,13 @@ class AlbumTest(unittest.TestCase):
|
||||
'artists': [artist.serialize()]},
|
||||
Album(uri='uri', name='name', artists=[artist]).serialize())
|
||||
|
||||
def test_serialize_with_images(self):
|
||||
image = 'data:foobar'
|
||||
self.assertDictEqual(
|
||||
{'__model__': 'Album', 'uri': 'uri', 'name': 'name',
|
||||
'images': [image]},
|
||||
Album(uri='uri', name='name', images=[image]).serialize())
|
||||
|
||||
def test_to_json_and_back(self):
|
||||
album1 = Album(uri='uri', name='name', artists=[Artist(name='foo')])
|
||||
serialized = json.dumps(album1, cls=ModelJSONEncoder)
|
||||
@ -389,6 +409,12 @@ class TrackTest(unittest.TestCase):
|
||||
self.assertEqual(track.track_no, track_no)
|
||||
self.assertRaises(AttributeError, setattr, track, 'track_no', None)
|
||||
|
||||
def test_disc_no(self):
|
||||
disc_no = 2
|
||||
track = Track(disc_no=disc_no)
|
||||
self.assertEqual(track.disc_no, disc_no)
|
||||
self.assertRaises(AttributeError, setattr, track, 'disc_no', None)
|
||||
|
||||
def test_date(self):
|
||||
date = '1977-01-01'
|
||||
track = Track(date=date)
|
||||
|
||||
@ -32,36 +32,40 @@ class TranslatorTest(unittest.TestCase):
|
||||
'musicbrainz-albumartistid': 'mbalbumartistid',
|
||||
}
|
||||
|
||||
# NOTE: kwargs are explicitly made bytestrings to work on Python
|
||||
# 2.6.0/2.6.1. See https://github.com/mopidy/mopidy/issues/302 for
|
||||
# details.
|
||||
|
||||
self.album = {
|
||||
'name': 'albumname',
|
||||
'num_tracks': 2,
|
||||
'musicbrainz_id': 'mbalbumid',
|
||||
b'name': 'albumname',
|
||||
b'num_tracks': 2,
|
||||
b'musicbrainz_id': 'mbalbumid',
|
||||
}
|
||||
|
||||
self.artist = {
|
||||
'name': 'name',
|
||||
'musicbrainz_id': 'mbartistid',
|
||||
b'name': 'name',
|
||||
b'musicbrainz_id': 'mbartistid',
|
||||
}
|
||||
|
||||
self.albumartist = {
|
||||
'name': 'albumartistname',
|
||||
'musicbrainz_id': 'mbalbumartistid',
|
||||
b'name': 'albumartistname',
|
||||
b'musicbrainz_id': 'mbalbumartistid',
|
||||
}
|
||||
|
||||
self.track = {
|
||||
'uri': 'uri',
|
||||
'name': 'trackname',
|
||||
'date': '2006-01-01',
|
||||
'track_no': 1,
|
||||
'length': 4531,
|
||||
'musicbrainz_id': 'mbtrackid',
|
||||
b'uri': 'uri',
|
||||
b'name': 'trackname',
|
||||
b'date': '2006-01-01',
|
||||
b'track_no': 1,
|
||||
b'length': 4531,
|
||||
b'musicbrainz_id': 'mbtrackid',
|
||||
}
|
||||
|
||||
def build_track(self):
|
||||
if self.albumartist:
|
||||
self.album['artists'] = [Artist(**self.albumartist)]
|
||||
self.track['album'] = Album(**self.album)
|
||||
self.track['artists'] = [Artist(**self.artist)]
|
||||
self.album[b'artists'] = [Artist(**self.albumartist)]
|
||||
self.track[b'album'] = Album(**self.album)
|
||||
self.track[b'artists'] = [Artist(**self.artist)]
|
||||
return Track(**self.track)
|
||||
|
||||
def check(self):
|
||||
|
||||
@ -15,15 +15,19 @@ class LocaleDecodeTest(unittest.TestCase):
|
||||
result = locale_decode(
|
||||
b'[Errno 98] Adresse d\xc3\xa9j\xc3\xa0 utilis\xc3\xa9e')
|
||||
|
||||
self.assertEquals('[Errno 98] Adresse d\xe9j\xe0 utilis\xe9e', result)
|
||||
self.assertEqual('[Errno 98] Adresse d\xe9j\xe0 utilis\xe9e', result)
|
||||
|
||||
def test_can_decode_an_ioerror_with_french_content(self, mock):
|
||||
mock.return_value = 'UTF-8'
|
||||
|
||||
error = IOError(98, b'Adresse d\xc3\xa9j\xc3\xa0 utilis\xc3\xa9e')
|
||||
result = locale_decode(error)
|
||||
expected = '[Errno 98] Adresse d\xe9j\xe0 utilis\xe9e'
|
||||
|
||||
self.assertEquals('[Errno 98] Adresse d\xe9j\xe0 utilis\xe9e', result)
|
||||
self.assertEqual(
|
||||
expected, result,
|
||||
'%r decoded to %r does not match expected %r' % (
|
||||
error, result, expected))
|
||||
|
||||
def test_does_not_use_locale_to_decode_unicode_strings(self, mock):
|
||||
mock.return_value = 'UTF-8'
|
||||
|
||||
@ -201,10 +201,12 @@ class JsonRpcSingleCommandTest(JsonRpcTestBase):
|
||||
self.assertEqual(self.core.playback.get_volume().get(), 37)
|
||||
|
||||
def test_call_methods_with_named_params(self):
|
||||
# NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5
|
||||
# See https://github.com/mopidy/mopidy/issues/302 for details.
|
||||
request = {
|
||||
'jsonrpc': '2.0',
|
||||
'method': 'core.playback.set_volume',
|
||||
'params': {'volume': 37},
|
||||
'params': {b'volume': 37},
|
||||
'id': 1,
|
||||
}
|
||||
response = self.jrw.handle_data(request)
|
||||
@ -603,7 +605,7 @@ class JsonRpcInspectorTest(JsonRpcTestBase):
|
||||
|
||||
self.assertIn('core.playlists.get_playlists', methods)
|
||||
self.assertEquals(
|
||||
len(methods['core.playlists.get_playlists']['params']), 0)
|
||||
len(methods['core.playlists.get_playlists']['params']), 1)
|
||||
|
||||
self.assertIn('core.tracklist.filter', methods.keys())
|
||||
self.assertEquals(
|
||||
|
||||
@ -34,5 +34,6 @@ class VersionTest(unittest.TestCase):
|
||||
self.assertLess(SV('0.8.1'), SV('0.9.0'))
|
||||
self.assertLess(SV('0.9.0'), SV('0.10.0'))
|
||||
self.assertLess(SV('0.10.0'), SV('0.11.0'))
|
||||
self.assertLess(SV('0.11.0'), SV(__version__))
|
||||
self.assertLess(SV(__version__), SV('0.11.2'))
|
||||
self.assertLess(SV('0.11.0'), SV('0.11.1'))
|
||||
self.assertLess(SV('0.11.1'), SV(__version__))
|
||||
self.assertLess(SV(__version__), SV('0.12.1'))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user