Merge branch 'develop' into feature/limit-spotify-data-pushing

Conflicts:
	mopidy/backends/spotify/session_manager.py
This commit is contained in:
Stein Magnus Jodal 2013-01-03 16:18:08 +01:00
commit 45d4445eea
26 changed files with 356 additions and 90 deletions

View File

@ -13,3 +13,4 @@
- Matt Bray <mattjbray@gmail.com>
- Trygve Aaberge <trygveaa@gmail.com>
- Wouter van Wijk <woutervanwijk@gmail.com>
- Jeremy B. Merrill <jeremybmerrill@gmail.com>

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -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`

View File

@ -10,10 +10,39 @@ v0.12.0 (in development)
(in development)
**Spotify**
- 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.
**Spotify backend**
- Let GStreamer handle time position tracking and seeks. (Fixes: :issue:`191`)
**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` settings.
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.
v0.11.0 (2012-12-24)
====================

View File

@ -4,11 +4,26 @@
HTTP clients
************
Mopidy added an :ref:`http-frontend` in 0.10 which provides the building blocks
needed for creating web clients for Mopidy with the help of a WebSocket and a
JavaScript library provided by Mopidy.
Mopidy added an :ref:`HTTP frontend <http-frontend>` in 0.10 which provides the
building blocks needed for creating web clients for Mopidy with the help of a
WebSocket and a JavaScript library provided by Mopidy.
This page will list any HTTP/web Mopidy clients. If you've created one, please
notify us so we can include your client on this page.
See :ref:`http-frontend` for details on how to build your own web client.
woutervanwijk/Mopidy-Webclient
==============================
.. image:: /_static/woutervanwijk-mopidy-webclient.png
:width: 410
:height: 511
The first web client for Mopidy is still under development, but is already very
usable. It targets both desktop and mobile browsers.
To try it out, get a copy of https://github.com/woutervanwijk/Mopidy-WebClient
and point the :attr:`mopidy.settings.HTTP_SERVER_STATIC_DIR` setting towards
your copy of the web client.

View File

@ -210,6 +210,10 @@ software packages, as Wheezy is going to be the next release of Debian.
aplay /usr/share/sounds/alsa/Front_Center.wav
If you hear a voice saying "Front Center," then your sound is working. Don't
be concerned if this test sound includes static, output from Mopidy will not.
Test your sound with gstreamer to determine sound quality.
To make the change to analog output stick, you can add the ``amixer`` command
to e.g. ``/etc/rc.local``, which will be executed when the system is
booting.

View File

@ -0,0 +1,7 @@
***********************************************
:mod:`mopidy.backends.stream` -- Stream backend
***********************************************
.. automodule:: mopidy.backends.stream
:synopsis: Backend for playing audio streams
:members:

View File

@ -79,37 +79,40 @@ def main():
def parse_options():
parser = optparse.OptionParser(
version='Mopidy %s' % versioning.get_version())
# NOTE Python 2.6: To support Python versions < 2.6.2rc1 we must use
# bytestrings for the first argument to ``add_option``
# 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]

View File

@ -4,3 +4,5 @@ from __future__ import unicode_literals
from .actor import Audio
from .listener import AudioListener
from .constants import PlaybackState
from .utils import (calculate_duration, create_buffer, millisecond_to_clocktime,
supported_uri_schemes)

50
mopidy/audio/utils.py Normal file
View File

@ -0,0 +1,50 @@
from __future__ import unicode_literals
import pygst
pygst.require('0.10')
import gst
def calculate_duration(num_samples, sample_rate):
"""Determine duration of samples using GStreamer helper for precise math."""
return gst.util_uint64_scale(num_samples, gst.SECOND, sample_rate)
def create_buffer(data, capabilites=None, timestamp=None, duration=None):
"""Create a new GStreamer buffer based on provided data.
Mainly intended to keep gst imports out of non-audio modules.
"""
buffer_ = gst.Buffer(data)
if capabilites:
if isinstance(capabilites, basestring):
capabilites = gst.caps_from_string(capabilites)
buffer_.set_caps(capabilites)
if timestamp:
buffer_.timestamp = timestamp
if duration:
buffer_.duration = duration
return buffer_
def millisecond_to_clocktime(value):
"""Convert a millisecond time to internal gstreamer time."""
return value * gst.MSECOND
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

View File

@ -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):

View File

@ -98,6 +98,9 @@ def _convert_mpd_data(data, tracks, music_dir):
if not data:
return
# 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.
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)

View File

@ -163,7 +163,9 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
translator.to_mopidy_track(t) for t in results.tracks()])
future.set(search_result)
if not self.backend.spotify.connected.wait(settings.SPOTIFY_TIMEOUT):
# Wait always returns None on python 2.6 :/
self.backend.spotify.connected.wait(settings.SPOTIFY_TIMEOUT)
if not self.backend.spotify.connected.is_set():
logger.debug('Not connected: Spotify search cancelled')
return SearchResult(uri='spotify:search')

View File

@ -5,6 +5,7 @@ import functools
from spotify import Link, SpotifyError
from mopidy import audio
from mopidy.backends import base
@ -30,6 +31,10 @@ class SpotifyPlaybackProvider(base.BasePlaybackProvider):
'width=(int)16, depth=(int)16, signed=(boolean)true, '
'rate=(int)44100')
def __init__(self, *args, **kwargs):
super(SpotifyPlaybackProvider, self).__init__(*args, **kwargs)
self._first_seek = False
def play(self, track):
if track.uri is None:
return False
@ -42,10 +47,13 @@ class SpotifyPlaybackProvider(base.BasePlaybackProvider):
seek_data_callback_bound = functools.partial(
seek_data_callback, spotify_backend)
self._first_seek = True
try:
self.backend.spotify.session.load(
Link.from_string(track.uri).as_track())
self.backend.spotify.session.play(1)
self.backend.spotify.buffer_timestamp = 0
self.audio.prepare_change()
self.audio.set_appsrc(
@ -75,5 +83,12 @@ class SpotifyPlaybackProvider(base.BasePlaybackProvider):
def on_seek_data(self, time_position):
logger.debug('playback.on_seek_data(%d) called', time_position)
self.backend.spotify.next_buffer_timestamp = time_position
if time_position == 0 and self._first_seek:
self._first_seek = False
logger.debug('Skipping seek due to issue #300')
return
self.backend.spotify.buffer_timestamp = audio.millisecond_to_clocktime(
time_position)
self.backend.spotify.session.seek(time_position)

View File

@ -1,9 +1,5 @@
from __future__ import unicode_literals
import pygst
pygst.require('0.10')
import gst
import logging
import os
import threading
@ -47,7 +43,7 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
self.connected = threading.Event()
self.push_audio_data = True
self.next_buffer_timestamp = None
self.buffer_timestamp = 0
self.container_manager = None
self.playlist_manager = None
@ -85,6 +81,7 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
def logged_out(self, session):
"""Callback used by pyspotify"""
logger.info('Disconnected from Spotify')
self.connected.clear()
def metadata_updated(self, session):
"""Callback used by pyspotify"""
@ -125,11 +122,14 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
'sample_rate': sample_rate,
'channels': channels,
}
buffer_ = gst.Buffer(bytes(frames))
buffer_.set_caps(gst.caps_from_string(capabilites))
if self.next_buffer_timestamp is not None:
buffer_.timestamp = self.next_buffer_timestamp * gst.MSECOND
self.next_buffer_timestamp = None
duration = audio.calculate_duration(num_frames, sample_rate)
buffer_ = audio.create_buffer(bytes(frames),
capabilites=capabilites,
timestamp=self.buffer_timestamp,
duration=duration)
self.buffer_timestamp += duration
if self.audio.emit_data(buffer_).get():
return num_frames

View 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

View File

@ -0,0 +1,37 @@
from __future__ import unicode_literals
import logging
import urlparse
import pykka
from mopidy import audio as audio_lib, settings
from mopidy.backends import base
from mopidy.models import 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 = audio_lib.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)]

View File

@ -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]

View File

@ -79,12 +79,15 @@ def main():
def parse_options():
parser = optparse.OptionParser(
version='Mopidy %s' % versioning.get_version())
# NOTE Python 2.6: To support Python versions < 2.6.2rc1 we must use
# bytestrings for the first argument to ``add_option``
# 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 +99,13 @@ def translator(data):
artist_kwargs = {}
track_kwargs = {}
# 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.
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 +118,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 +132,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)

View File

@ -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',
)

View File

@ -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

View File

@ -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()

View File

@ -142,7 +142,13 @@ def validate_settings(defaults, settings):
'SPOTIFY_LIB_CACHE': 'SPOTIFY_CACHE_PATH',
}
list_of_one_or_more = [
must_be_iterable = [
'BACKENDS',
'FRONTENDS',
'STREAM_PROTOCOLS',
]
must_have_value_set = [
'BACKENDS',
'FRONTENDS',
]
@ -171,13 +177,13 @@ def validate_settings(defaults, settings):
'Deprecated setting, please set the value via the GStreamer '
'bin in OUTPUT.')
elif setting in list_of_one_or_more:
if not hasattr(value, '__iter__'):
errors[setting] = (
'Must be a tuple. '
"Remember the comma after single values: (u'value',)")
if not value:
errors[setting] = 'Must contain at least one value.'
elif setting in must_be_iterable and not hasattr(value, '__iter__'):
errors[setting] = (
'Must be a tuple. '
"Remember the comma after single values: (u'value',)")
elif setting in must_have_value_set and not value:
errors[setting] = 'Must be set.'
elif setting not in defaults and not setting.startswith('CUSTOM_'):
errors[setting] = 'Unknown setting.'

View File

@ -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')

View File

@ -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):

View File

@ -79,13 +79,13 @@ class ValidateSettingsTest(unittest.TestCase):
result = setting_utils.validate_settings(
self.defaults, {'FRONTENDS': []})
self.assertEqual(
result['FRONTENDS'], 'Must contain at least one value.')
result['FRONTENDS'], 'Must be set.')
def test_empty_backends_list_returns_error(self):
result = setting_utils.validate_settings(
self.defaults, {'BACKENDS': []})
self.assertEqual(
result['BACKENDS'], 'Must contain at least one value.')
result['BACKENDS'], 'Must be set.')
def test_noniterable_multivalue_setting_returns_error(self):
result = setting_utils.validate_settings(