Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make sure the stream backend playback/library handle URIs the same way #1447

Merged
merged 7 commits into from Feb 15, 2016
74 changes: 40 additions & 34 deletions mopidy/stream/actor.py
Expand Up @@ -25,10 +25,19 @@ def __init__(self, config, audio):
timeout=config['stream']['timeout'],
proxy_config=config['proxy'])

self.library = StreamLibraryProvider(
backend=self, blacklist=config['stream']['metadata_blacklist'])
self.playback = StreamPlaybackProvider(
audio=audio, backend=self, config=config)
self._session = http.get_requests_session(
proxy_config=config['proxy'],
user_agent='%s/%s' % (
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.uri_schemes = audio_lib.supported_uri_schemes(
Expand All @@ -43,51 +52,48 @@ def __init__(self, config, audio):


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):
if urllib.parse.urlsplit(uri).scheme not in self.backend.uri_schemes:
return []

if self._blacklist_re.match(uri):
if self.backend._blacklist_re.match(uri):
logger.debug('URI matched metadata lookup blacklist: %s', uri)
return [Track(uri=uri)]

try:
result = self._scanner.scan(uri)
result = _unwrap_stream(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be nicer with _, scan_result = ... ?

uri,
timeout=self.backend._timeout,
scanner=self.backend._scanner,
requests_session=self.backend._session)[1]

if result:
track = tags.convert_tags_to_track(result.tags).replace(
uri=uri, length=result.duration)
except exceptions.ScannerError as e:
logger.warning('Problem looking up %s: %s', uri, e)
else:
logger.warning('Problem looking up %s: %s', uri)
track = Track(uri=uri)

return [track]


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):
if urllib.parse.urlsplit(uri).scheme not in self.backend.uri_schemes:
return None

if self.backend._blacklist_re.match(uri):
logger.debug('URI matched metadata lookup blacklist: %s', uri)
return uri

return _unwrap_stream(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be nicer with uri, _ = ... and then return uri ?

uri,
timeout=self._config['stream']['timeout'],
scanner=self._scanner,
requests_session=self._session)
timeout=self.backend._timeout,
scanner=self.backend._scanner,
requests_session=self.backend._session)[0]


# TODO: cleanup the return value of this.
def _unwrap_stream(uri, timeout, scanner, requests_session):
"""
Get a stream URI from a playlist URI, ``uri``.
Expand All @@ -105,7 +111,7 @@ def _unwrap_stream(uri, timeout, scanner, requests_session):
logger.info(
'Unwrapping stream from URI (%s) failed: '
'playlist referenced itself', uri)
return None
return None, None
else:
seen_uris.add(uri)

Expand All @@ -117,7 +123,7 @@ def _unwrap_stream(uri, timeout, scanner, requests_session):
logger.info(
'Unwrapping stream from URI (%s) failed: '
'timed out in %sms', uri, timeout)
return None
return None, None
scan_result = scanner.scan(uri, timeout=scan_timeout)
except exceptions.ScannerError as exc:
logger.debug('GStreamer failed scanning URI (%s): %s', uri, exc)
Expand All @@ -130,29 +136,29 @@ def _unwrap_stream(uri, timeout, scanner, requests_session):
):
logger.debug(
'Unwrapped potential %s stream: %s', scan_result.mime, uri)
return uri
return uri, scan_result

download_timeout = deadline - time.time()
if download_timeout < 0:
logger.info(
'Unwrapping stream from URI (%s) failed: timed out in %sms',
uri, timeout)
return None
return None, None
content = http.download(
requests_session, uri, timeout=download_timeout)

if content is None:
logger.info(
'Unwrapping stream from URI (%s) failed: '
'error downloading URI %s', original_uri, uri)
return None
return None, None

uris = playlists.parse(content)
if not uris:
logger.debug(
'Failed parsing URI (%s) as playlist; found potential stream.',
uri)
return uri
return uri, None

# TODO Test streams and return first that seems to be playable
logger.debug(
Expand Down
52 changes: 30 additions & 22 deletions tests/stream/test_library.py
Expand Up @@ -4,7 +4,6 @@

import pytest

from mopidy.audio import scan
from mopidy.internal import path
from mopidy.models import Track
from mopidy.stream import actor
Expand All @@ -13,43 +12,52 @@


@pytest.fixture
def scanner():
return scan.Scanner(timeout=100, proxy_config={})
def config():
return {
'proxy': {},
'stream': {
'timeout': 1000,
'metadata_blacklist': [],
'protocols': ['file'],
},
'file': {
'enabled': False
},
}


@pytest.fixture
def backend(scanner):
backend = mock.Mock()
backend.uri_schemes = ['file']
backend._scanner = scanner
return backend
def audio():
return mock.Mock()


@pytest.fixture
def track_uri():
return path.path_to_uri(path_to_data_dir('song1.wav'))


def test_lookup_ignores_unknown_scheme(backend):
library = actor.StreamLibraryProvider(backend, [])
def test_lookup_ignores_unknown_scheme(audio, config):
backend = actor.StreamBackend(audio=audio, config=config)
backend.library.lookup('http://example.com') == []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing assert


assert library.lookup('http://example.com') == []

def test_lookup_respects_blacklist(audio, config, track_uri):
config['stream']['metadata_blacklist'].append(track_uri)
backend = actor.StreamBackend(audio=audio, config=config)

def test_lookup_respects_blacklist(backend, track_uri):
library = actor.StreamLibraryProvider(backend, [track_uri])
assert backend.library.lookup(track_uri) == [Track(uri=track_uri)]

assert library.lookup(track_uri) == [Track(uri=track_uri)]

def test_lookup_respects_blacklist_globbing(audio, config, track_uri):
blacklist_glob = path.path_to_uri(path_to_data_dir('')) + '*'
config['stream']['metadata_blacklist'].append(blacklist_glob)
backend = actor.StreamBackend(audio=audio, config=config)

def test_lookup_respects_blacklist_globbing(backend, track_uri):
blacklist = [path.path_to_uri(path_to_data_dir('')) + '*']
library = actor.StreamLibraryProvider(backend, blacklist)
assert backend.library.lookup(track_uri) == [Track(uri=track_uri)]

assert library.lookup(track_uri) == [Track(uri=track_uri)]

def test_lookup_converts_uri_metadata_to_track(audio, config, track_uri):
backend = actor.StreamBackend(audio=audio, config=config)

def test_lookup_converts_uri_metadata_to_track(backend, track_uri):
library = actor.StreamLibraryProvider(backend, [])

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)]
44 changes: 29 additions & 15 deletions tests/stream/test_playback.py
Expand Up @@ -4,6 +4,8 @@

import pytest

import requests.exceptions

import responses

from mopidy import exceptions
Expand All @@ -27,6 +29,11 @@ def config():
'proxy': {},
'stream': {
'timeout': TIMEOUT,
'metadata_blacklist': [],
'protocols': ['http'],
},
'file': {
'enabled': False
},
}

Expand All @@ -36,24 +43,21 @@ def audio():
return mock.Mock()


@pytest.fixture
@pytest.yield_fixture
def scanner():
scan_mock = mock.Mock(spec=scan.Scanner)
scan_mock.scan.return_value = None
return scan_mock
patcher = mock.patch.object(scan, 'Scanner')
yield patcher.start()()
patcher.stop()


@pytest.fixture
def backend(scanner):
backend = mock.Mock()
backend.uri_schemes = ['file']
backend._scanner = scanner
return backend
def backend(audio, config, scanner):
return actor.StreamBackend(audio=audio, config=config)


@pytest.fixture
def provider(audio, backend, config):
return actor.StreamPlaybackProvider(audio, backend, config)
def provider(backend):
return backend.playback


class TestTranslateURI(object):
Expand Down Expand Up @@ -184,14 +188,24 @@ def test_scan_fails_and_playlist_parsing_fails(
% STREAM_URI in caplog.text())
assert result == STREAM_URI

def test_failed_download_returns_none(self, provider, caplog):
with mock.patch.object(actor, 'http') as http_mock:
http_mock.download.return_value = None
@responses.activate
def test_failed_download_returns_none(self, scanner, provider, caplog):
scanner.scan.side_effect = [
mock.Mock(mime='text/foo', playable=False)
]

responses.add(
responses.GET, PLAYLIST_URI,
body=requests.exceptions.HTTPError('Kaboom'))

result = provider.translate_uri(PLAYLIST_URI)
result = provider.translate_uri(PLAYLIST_URI)

assert result is None

assert (
'Unwrapping stream from URI (%s) failed: '
'error downloading URI' % PLAYLIST_URI) in caplog.text()

@responses.activate
def test_playlist_references_itself(self, scanner, provider, caplog):
scanner.scan.side_effect = [
Expand Down