Merge branch 'develop' into feature/limit-spotify-data-pushing
Conflicts: mopidy/backends/spotify/session_manager.py
This commit is contained in:
commit
45d4445eea
1
AUTHORS
1
AUTHORS
@ -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>
|
||||
|
||||
BIN
docs/_static/woutervanwijk-mopidy-webclient.png
vendored
Normal file
BIN
docs/_static/woutervanwijk-mopidy-webclient.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
@ -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`
|
||||
|
||||
@ -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)
|
||||
====================
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
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:
|
||||
@ -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]
|
||||
|
||||
@ -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
50
mopidy/audio/utils.py
Normal 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
|
||||
@ -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 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)
|
||||
|
||||
@ -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')
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
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
|
||||
37
mopidy/backends/stream/actor.py
Normal file
37
mopidy/backends/stream/actor.py
Normal 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)]
|
||||
@ -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]
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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.'
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user