Merge pull request #295 from adamcik/feature/streaming-backend

Add basic streaming backend support.
This commit is contained in:
Stein Magnus Jodal 2013-01-02 13:25:03 -08:00
commit f4251e63fa
9 changed files with 165 additions and 18 deletions

23
mopidy/audio/utils.py Normal file
View 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

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

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

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

@ -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.
@ -301,3 +303,26 @@ SPOTIFY_PROXY_PASSWORD = None
#:
#: 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

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

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