Merge pull request #1447 from adamcik/fix/1445-unify-stream-uri-handling

Make sure the stream backend playback/library handle URIs the same way
This commit is contained in:
Stein Magnus Jodal 2016-02-15 20:58:42 +01:00
commit 9a6636a974
4 changed files with 106 additions and 75 deletions

View File

@ -110,6 +110,12 @@ M3U backend
- Improve reliability of playlist updates using the core playlist API by - Improve reliability of playlist updates using the core playlist API by
applying the write-replace pattern for file updates. applying the write-replace pattern for file updates.
Stream backend
--------------
- Make sure both lookup and playback correctly handle playlists and our
blacklist support. (Fixes: :issue:`1445`, PR: :issue:`1447`)
MPD frontend MPD frontend
------------ ------------

View File

@ -25,10 +25,19 @@ class StreamBackend(pykka.ThreadingActor, backend.Backend):
timeout=config['stream']['timeout'], timeout=config['stream']['timeout'],
proxy_config=config['proxy']) proxy_config=config['proxy'])
self.library = StreamLibraryProvider( self._session = http.get_requests_session(
backend=self, blacklist=config['stream']['metadata_blacklist']) proxy_config=config['proxy'],
self.playback = StreamPlaybackProvider( user_agent='%s/%s' % (
audio=audio, backend=self, config=config) stream.Extension.dist_name, stream.Extension.version))
blacklist = config['stream']['metadata_blacklist']
self._blacklist_re = re.compile(
r'^(%s)$' % '|'.join(fnmatch.translate(u) for u in blacklist))
self._timeout = config['stream']['timeout']
self.library = StreamLibraryProvider(backend=self)
self.playback = StreamPlaybackProvider(audio=audio, backend=self)
self.playlists = None self.playlists = None
self.uri_schemes = audio_lib.supported_uri_schemes( self.uri_schemes = audio_lib.supported_uri_schemes(
@ -43,27 +52,23 @@ class StreamBackend(pykka.ThreadingActor, backend.Backend):
class StreamLibraryProvider(backend.LibraryProvider): class StreamLibraryProvider(backend.LibraryProvider):
def __init__(self, backend, blacklist):
super(StreamLibraryProvider, self).__init__(backend)
self._scanner = backend._scanner
self._blacklist_re = re.compile(
r'^(%s)$' % '|'.join(fnmatch.translate(u) for u in blacklist))
def lookup(self, uri): def lookup(self, uri):
if urllib.parse.urlsplit(uri).scheme not in self.backend.uri_schemes: if urllib.parse.urlsplit(uri).scheme not in self.backend.uri_schemes:
return [] return []
if self._blacklist_re.match(uri): if self.backend._blacklist_re.match(uri):
logger.debug('URI matched metadata lookup blacklist: %s', uri) logger.debug('URI matched metadata lookup blacklist: %s', uri)
return [Track(uri=uri)] return [Track(uri=uri)]
try: _, scan_result = _unwrap_stream(
result = self._scanner.scan(uri) uri, timeout=self.backend._timeout, scanner=self.backend._scanner,
track = tags.convert_tags_to_track(result.tags).replace( requests_session=self.backend._session)
uri=uri, length=result.duration)
except exceptions.ScannerError as e: if scan_result:
logger.warning('Problem looking up %s: %s', uri, e) track = tags.convert_tags_to_track(scan_result.tags).replace(
uri=uri, length=scan_result.duration)
else:
logger.warning('Problem looking up %s: %s', uri)
track = Track(uri=uri) track = Track(uri=uri)
return [track] return [track]
@ -71,23 +76,21 @@ class StreamLibraryProvider(backend.LibraryProvider):
class StreamPlaybackProvider(backend.PlaybackProvider): class StreamPlaybackProvider(backend.PlaybackProvider):
def __init__(self, audio, backend, config):
super(StreamPlaybackProvider, self).__init__(audio, backend)
self._config = config
self._scanner = backend._scanner
self._session = http.get_requests_session(
proxy_config=config['proxy'],
user_agent='%s/%s' % (
stream.Extension.dist_name, stream.Extension.version))
def translate_uri(self, uri): def translate_uri(self, uri):
return _unwrap_stream( if urllib.parse.urlsplit(uri).scheme not in self.backend.uri_schemes:
uri, return None
timeout=self._config['stream']['timeout'],
scanner=self._scanner, if self.backend._blacklist_re.match(uri):
requests_session=self._session) logger.debug('URI matched metadata lookup blacklist: %s', uri)
return uri
unwrapped_uri, _ = _unwrap_stream(
uri, timeout=self.backend._timeout, scanner=self.backend._scanner,
requests_session=self.backend._session)
return unwrapped_uri
# TODO: cleanup the return value of this.
def _unwrap_stream(uri, timeout, scanner, requests_session): def _unwrap_stream(uri, timeout, scanner, requests_session):
""" """
Get a stream URI from a playlist URI, ``uri``. Get a stream URI from a playlist URI, ``uri``.
@ -105,7 +108,7 @@ def _unwrap_stream(uri, timeout, scanner, requests_session):
logger.info( logger.info(
'Unwrapping stream from URI (%s) failed: ' 'Unwrapping stream from URI (%s) failed: '
'playlist referenced itself', uri) 'playlist referenced itself', uri)
return None return None, None
else: else:
seen_uris.add(uri) seen_uris.add(uri)
@ -117,7 +120,7 @@ def _unwrap_stream(uri, timeout, scanner, requests_session):
logger.info( logger.info(
'Unwrapping stream from URI (%s) failed: ' 'Unwrapping stream from URI (%s) failed: '
'timed out in %sms', uri, timeout) 'timed out in %sms', uri, timeout)
return None return None, None
scan_result = scanner.scan(uri, timeout=scan_timeout) scan_result = scanner.scan(uri, timeout=scan_timeout)
except exceptions.ScannerError as exc: except exceptions.ScannerError as exc:
logger.debug('GStreamer failed scanning URI (%s): %s', uri, exc) logger.debug('GStreamer failed scanning URI (%s): %s', uri, exc)
@ -130,14 +133,14 @@ def _unwrap_stream(uri, timeout, scanner, requests_session):
): ):
logger.debug( logger.debug(
'Unwrapped potential %s stream: %s', scan_result.mime, uri) 'Unwrapped potential %s stream: %s', scan_result.mime, uri)
return uri return uri, scan_result
download_timeout = deadline - time.time() download_timeout = deadline - time.time()
if download_timeout < 0: if download_timeout < 0:
logger.info( logger.info(
'Unwrapping stream from URI (%s) failed: timed out in %sms', 'Unwrapping stream from URI (%s) failed: timed out in %sms',
uri, timeout) uri, timeout)
return None return None, None
content = http.download( content = http.download(
requests_session, uri, timeout=download_timeout) requests_session, uri, timeout=download_timeout)
@ -145,14 +148,14 @@ def _unwrap_stream(uri, timeout, scanner, requests_session):
logger.info( logger.info(
'Unwrapping stream from URI (%s) failed: ' 'Unwrapping stream from URI (%s) failed: '
'error downloading URI %s', original_uri, uri) 'error downloading URI %s', original_uri, uri)
return None return None, None
uris = playlists.parse(content) uris = playlists.parse(content)
if not uris: if not uris:
logger.debug( logger.debug(
'Failed parsing URI (%s) as playlist; found potential stream.', 'Failed parsing URI (%s) as playlist; found potential stream.',
uri) uri)
return uri return uri, None
# TODO Test streams and return first that seems to be playable # TODO Test streams and return first that seems to be playable
logger.debug( logger.debug(

View File

@ -4,7 +4,6 @@ import mock
import pytest import pytest
from mopidy.audio import scan
from mopidy.internal import path from mopidy.internal import path
from mopidy.models import Track from mopidy.models import Track
from mopidy.stream import actor from mopidy.stream import actor
@ -13,16 +12,23 @@ from tests import path_to_data_dir
@pytest.fixture @pytest.fixture
def scanner(): def config():
return scan.Scanner(timeout=100, proxy_config={}) return {
'proxy': {},
'stream': {
'timeout': 1000,
'metadata_blacklist': [],
'protocols': ['file'],
},
'file': {
'enabled': False
},
}
@pytest.fixture @pytest.fixture
def backend(scanner): def audio():
backend = mock.Mock() return mock.Mock()
backend.uri_schemes = ['file']
backend._scanner = scanner
return backend
@pytest.fixture @pytest.fixture
@ -30,26 +36,28 @@ def track_uri():
return path.path_to_uri(path_to_data_dir('song1.wav')) return path.path_to_uri(path_to_data_dir('song1.wav'))
def test_lookup_ignores_unknown_scheme(backend): def test_lookup_ignores_unknown_scheme(audio, config):
library = actor.StreamLibraryProvider(backend, []) backend = actor.StreamBackend(audio=audio, config=config)
assert backend.library.lookup('http://example.com') == []
assert library.lookup('http://example.com') == []
def test_lookup_respects_blacklist(backend, track_uri): def test_lookup_respects_blacklist(audio, config, track_uri):
library = actor.StreamLibraryProvider(backend, [track_uri]) config['stream']['metadata_blacklist'].append(track_uri)
backend = actor.StreamBackend(audio=audio, config=config)
assert library.lookup(track_uri) == [Track(uri=track_uri)] assert backend.library.lookup(track_uri) == [Track(uri=track_uri)]
def test_lookup_respects_blacklist_globbing(backend, track_uri): def test_lookup_respects_blacklist_globbing(audio, config, track_uri):
blacklist = [path.path_to_uri(path_to_data_dir('')) + '*'] blacklist_glob = path.path_to_uri(path_to_data_dir('')) + '*'
library = actor.StreamLibraryProvider(backend, blacklist) config['stream']['metadata_blacklist'].append(blacklist_glob)
backend = actor.StreamBackend(audio=audio, config=config)
assert library.lookup(track_uri) == [Track(uri=track_uri)] assert backend.library.lookup(track_uri) == [Track(uri=track_uri)]
def test_lookup_converts_uri_metadata_to_track(backend, track_uri): def test_lookup_converts_uri_metadata_to_track(audio, config, track_uri):
library = actor.StreamLibraryProvider(backend, []) backend = actor.StreamBackend(audio=audio, config=config)
assert library.lookup(track_uri) == [Track(length=4406, uri=track_uri)] result = backend.library.lookup(track_uri)
assert result == [Track(length=4406, uri=track_uri)]

View File

@ -4,6 +4,8 @@ import mock
import pytest import pytest
import requests.exceptions
import responses import responses
from mopidy import exceptions from mopidy import exceptions
@ -27,6 +29,11 @@ def config():
'proxy': {}, 'proxy': {},
'stream': { 'stream': {
'timeout': TIMEOUT, 'timeout': TIMEOUT,
'metadata_blacklist': [],
'protocols': ['http'],
},
'file': {
'enabled': False
}, },
} }
@ -36,24 +43,21 @@ def audio():
return mock.Mock() return mock.Mock()
@pytest.fixture @pytest.yield_fixture
def scanner(): def scanner():
scan_mock = mock.Mock(spec=scan.Scanner) patcher = mock.patch.object(scan, 'Scanner')
scan_mock.scan.return_value = None yield patcher.start()()
return scan_mock patcher.stop()
@pytest.fixture @pytest.fixture
def backend(scanner): def backend(audio, config, scanner):
backend = mock.Mock() return actor.StreamBackend(audio=audio, config=config)
backend.uri_schemes = ['file']
backend._scanner = scanner
return backend
@pytest.fixture @pytest.fixture
def provider(audio, backend, config): def provider(backend):
return actor.StreamPlaybackProvider(audio, backend, config) return backend.playback
class TestTranslateURI(object): class TestTranslateURI(object):
@ -184,14 +188,24 @@ class TestTranslateURI(object):
% STREAM_URI in caplog.text()) % STREAM_URI in caplog.text())
assert result == STREAM_URI assert result == STREAM_URI
def test_failed_download_returns_none(self, provider, caplog): @responses.activate
with mock.patch.object(actor, 'http') as http_mock: def test_failed_download_returns_none(self, scanner, provider, caplog):
http_mock.download.return_value = None scanner.scan.side_effect = [
mock.Mock(mime='text/foo', playable=False)
]
result = provider.translate_uri(PLAYLIST_URI) responses.add(
responses.GET, PLAYLIST_URI,
body=requests.exceptions.HTTPError('Kaboom'))
result = provider.translate_uri(PLAYLIST_URI)
assert result is None assert result is None
assert (
'Unwrapping stream from URI (%s) failed: '
'error downloading URI' % PLAYLIST_URI) in caplog.text()
@responses.activate @responses.activate
def test_playlist_references_itself(self, scanner, provider, caplog): def test_playlist_references_itself(self, scanner, provider, caplog):
scanner.scan.side_effect = [ scanner.scan.side_effect = [