parent
92187f2c3f
commit
2d10eef0b1
@ -53,6 +53,11 @@ Bug fix release.
|
||||
services installed from packages that properly set :confval:`core/data_dir`,
|
||||
like the Debian and Arch pakages. (Fixes: :issue:`1259`, PR: :issue:`1266`)
|
||||
|
||||
- Stream: Expand nested playlists to find the stream URI. This used to work,
|
||||
but regressed in 1.1.0 with the extraction of stream playlist parsing from
|
||||
GStreamer to being handled by the Mopidy-Stream backend. (Fixes:
|
||||
:issue:`1250`)
|
||||
|
||||
- Stream: If "file" is present in the :confval:`stream/protocols` config value
|
||||
and the :ref:`ext-file` extension is enabled, we exited with an error because
|
||||
two extensions claimed the same URI scheme. We now log a warning recommending
|
||||
|
||||
@ -3,6 +3,7 @@ from __future__ import absolute_import, unicode_literals
|
||||
import fnmatch
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
import urlparse
|
||||
|
||||
import pykka
|
||||
@ -80,22 +81,70 @@ class StreamPlaybackProvider(backend.PlaybackProvider):
|
||||
stream.Extension.dist_name, stream.Extension.version))
|
||||
|
||||
def translate_uri(self, uri):
|
||||
try:
|
||||
scan_result = self._scanner.scan(uri)
|
||||
except exceptions.ScannerError as e:
|
||||
logger.warning(
|
||||
'Problem scanning URI %s: %s', uri, e)
|
||||
return None
|
||||
return unwrap_stream(
|
||||
uri,
|
||||
timeout=self._config['stream']['timeout'],
|
||||
scanner=self._scanner,
|
||||
requests_session=self._session)
|
||||
|
||||
if not (scan_result.mime.startswith('text/') or
|
||||
|
||||
def unwrap_stream(uri, timeout, scanner, requests_session):
|
||||
"""
|
||||
Get a stream URI from a playlist URI, ``uri``.
|
||||
|
||||
Unwraps nested playlists until something that's not a playlist is found or
|
||||
the ``timeout`` is reached.
|
||||
"""
|
||||
|
||||
original_uri = uri
|
||||
deadline = time.time() + timeout
|
||||
|
||||
while time.time() < deadline:
|
||||
logger.debug('Unwrapping stream from URI: %s', uri)
|
||||
|
||||
try:
|
||||
scan_timeout = deadline - time.time()
|
||||
if scan_timeout < 0:
|
||||
logger.info(
|
||||
'Unwrapping stream from URI (%s) failed: '
|
||||
'timed out in %sms',
|
||||
uri, timeout)
|
||||
return None
|
||||
scan_result = scanner.scan(uri, timeout=scan_timeout)
|
||||
except exceptions.ScannerError as exc:
|
||||
logger.debug('GStreamer failed scanning URI (%s): %s', uri, exc)
|
||||
scan_result = None
|
||||
|
||||
if scan_result is not None and not (
|
||||
scan_result.mime.startswith('text/') or
|
||||
scan_result.mime.startswith('application/')):
|
||||
logger.debug(
|
||||
'Unwrapped potential %s stream: %s', scan_result.mime, uri)
|
||||
return uri
|
||||
|
||||
content = http.download(self._session, uri)
|
||||
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
|
||||
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
|
||||
|
||||
tracks = list(playlists.parse(content))
|
||||
if tracks:
|
||||
# TODO Test streams and return first that seems to be playable
|
||||
return tracks[0]
|
||||
uris = playlists.parse(content)
|
||||
if not uris:
|
||||
logger.debug(
|
||||
'Failed parsing URI (%s) as playlist; found potential stream.',
|
||||
uri)
|
||||
return uri
|
||||
|
||||
# TODO Test streams and return first that seems to be playable
|
||||
logger.debug(
|
||||
'Parsed playlist (%s) and found new URI: %s', uri, uris[0])
|
||||
uri = uris[0]
|
||||
|
||||
@ -12,7 +12,8 @@ from mopidy.stream import actor
|
||||
|
||||
|
||||
TIMEOUT = 1000
|
||||
URI = 'http://example.com/listen.m3u'
|
||||
PLAYLIST_URI = 'http://example.com/listen.m3u'
|
||||
STREAM_URI = 'http://example.com/stream.mp3'
|
||||
BODY = """
|
||||
#EXTM3U
|
||||
http://example.com/stream.mp3
|
||||
@ -37,9 +38,7 @@ def audio():
|
||||
|
||||
@pytest.fixture
|
||||
def scanner():
|
||||
scanner = mock.Mock(spec=scan.Scanner)
|
||||
scanner.scan.return_value.mime = 'text/foo'
|
||||
return scanner
|
||||
return mock.Mock(spec=scan.Scanner)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -55,58 +54,115 @@ def provider(audio, backend, config):
|
||||
return actor.StreamPlaybackProvider(audio, backend, config)
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_translate_uri_of_audio_stream_returns_same_uri(
|
||||
scanner, provider):
|
||||
class TestTranslateURI(object):
|
||||
|
||||
scanner.scan.return_value.mime = 'audio/ogg'
|
||||
@responses.activate
|
||||
def test_audio_stream_returns_same_uri(self, scanner, provider):
|
||||
scanner.scan.return_value.mime = 'audio/mpeg'
|
||||
|
||||
result = provider.translate_uri(URI)
|
||||
result = provider.translate_uri(STREAM_URI)
|
||||
|
||||
scanner.scan.assert_called_once_with(URI)
|
||||
assert result == URI
|
||||
scanner.scan.assert_called_once_with(STREAM_URI, timeout=mock.ANY)
|
||||
assert result == STREAM_URI
|
||||
|
||||
@responses.activate
|
||||
def test_text_playlist_with_mpeg_stream(
|
||||
self, scanner, provider, caplog):
|
||||
|
||||
@responses.activate
|
||||
def test_translate_uri_of_playlist_returns_first_uri_in_list(
|
||||
scanner, provider):
|
||||
scanner.scan.side_effect = [
|
||||
mock.Mock(mime='text/foo'), # scanning playlist
|
||||
mock.Mock(mime='audio/mpeg'), # scanning stream
|
||||
]
|
||||
responses.add(
|
||||
responses.GET, PLAYLIST_URI,
|
||||
body=BODY, content_type='audio/x-mpegurl')
|
||||
|
||||
responses.add(
|
||||
responses.GET, URI, body=BODY, content_type='audio/x-mpegurl')
|
||||
result = provider.translate_uri(PLAYLIST_URI)
|
||||
|
||||
result = provider.translate_uri(URI)
|
||||
assert scanner.scan.mock_calls == [
|
||||
mock.call(PLAYLIST_URI, timeout=mock.ANY),
|
||||
mock.call(STREAM_URI, timeout=mock.ANY),
|
||||
]
|
||||
assert result == STREAM_URI
|
||||
|
||||
scanner.scan.assert_called_once_with(URI)
|
||||
assert result == 'http://example.com/stream.mp3'
|
||||
assert responses.calls[0].request.headers['User-Agent'].startswith(
|
||||
'Mopidy-Stream/')
|
||||
# Check logging to ensure debuggability
|
||||
assert 'Unwrapping stream from URI: %s' % PLAYLIST_URI
|
||||
assert 'Parsed playlist (%s)' % PLAYLIST_URI in caplog.text()
|
||||
assert 'Unwrapping stream from URI: %s' % STREAM_URI
|
||||
assert (
|
||||
'Unwrapped potential audio/mpeg stream: %s' % STREAM_URI
|
||||
in caplog.text())
|
||||
|
||||
# Check proper Requests session setup
|
||||
assert responses.calls[0].request.headers['User-Agent'].startswith(
|
||||
'Mopidy-Stream/')
|
||||
|
||||
@responses.activate
|
||||
def test_translate_uri_of_playlist_with_xml_mimetype(scanner, provider):
|
||||
scanner.scan.return_value.mime = 'application/xspf+xml'
|
||||
responses.add(
|
||||
responses.GET, URI, body=BODY, content_type='application/xspf+xml')
|
||||
@responses.activate
|
||||
def test_xml_playlist_with_mpeg_stream(self, scanner, provider):
|
||||
scanner.scan.side_effect = [
|
||||
mock.Mock(mime='application/xspf+xml'), # scanning playlist
|
||||
mock.Mock(mime='audio/mpeg'), # scanning stream
|
||||
]
|
||||
responses.add(
|
||||
responses.GET, PLAYLIST_URI,
|
||||
body=BODY, content_type='application/xspf+xml')
|
||||
|
||||
result = provider.translate_uri(URI)
|
||||
result = provider.translate_uri(PLAYLIST_URI)
|
||||
|
||||
scanner.scan.assert_called_once_with(URI)
|
||||
assert result == 'http://example.com/stream.mp3'
|
||||
assert scanner.scan.mock_calls == [
|
||||
mock.call(PLAYLIST_URI, timeout=mock.ANY),
|
||||
mock.call(STREAM_URI, timeout=mock.ANY),
|
||||
]
|
||||
assert result == STREAM_URI
|
||||
|
||||
@responses.activate
|
||||
def test_scan_fails_but_playlist_parsing_succeeds(
|
||||
self, scanner, provider, caplog):
|
||||
|
||||
def test_translate_uri_when_scanner_fails(scanner, provider, caplog):
|
||||
scanner.scan.side_effect = exceptions.ScannerError('foo failed')
|
||||
scanner.scan.side_effect = [
|
||||
exceptions.ScannerError('some failure'), # scanning playlist
|
||||
mock.Mock(mime='audio/mpeg'), # scanning stream
|
||||
]
|
||||
responses.add(
|
||||
responses.GET, PLAYLIST_URI,
|
||||
body=BODY, content_type='audio/x-mpegurl')
|
||||
|
||||
result = provider.translate_uri('bar')
|
||||
result = provider.translate_uri(PLAYLIST_URI)
|
||||
|
||||
assert result is None
|
||||
assert 'Problem scanning URI bar: foo failed' in caplog.text()
|
||||
assert 'Unwrapping stream from URI: %s' % PLAYLIST_URI
|
||||
assert (
|
||||
'GStreamer failed scanning URI (%s)' % PLAYLIST_URI
|
||||
in caplog.text())
|
||||
assert 'Parsed playlist (%s)' % PLAYLIST_URI in caplog.text()
|
||||
assert (
|
||||
'Unwrapped potential audio/mpeg stream: %s' % STREAM_URI
|
||||
in caplog.text())
|
||||
assert result == STREAM_URI
|
||||
|
||||
@responses.activate
|
||||
def test_scan_fails_and_playlist_parsing_fails(
|
||||
self, scanner, provider, caplog):
|
||||
|
||||
def test_translate_uri_when_playlist_download_fails(provider, caplog):
|
||||
with mock.patch.object(actor, 'http') as http_mock:
|
||||
http_mock.download.return_value = None
|
||||
scanner.scan.side_effect = exceptions.ScannerError('some failure')
|
||||
responses.add(
|
||||
responses.GET, STREAM_URI,
|
||||
body=b'some audio data', content_type='audio/mpeg')
|
||||
|
||||
result = provider.translate_uri(URI)
|
||||
result = provider.translate_uri(STREAM_URI)
|
||||
|
||||
assert result is None
|
||||
assert 'Unwrapping stream from URI: %s' % STREAM_URI
|
||||
assert (
|
||||
'GStreamer failed scanning URI (%s)' % STREAM_URI
|
||||
in caplog.text())
|
||||
assert (
|
||||
'Failed parsing URI (%s) as playlist; found potential stream.'
|
||||
% 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
|
||||
|
||||
result = provider.translate_uri(PLAYLIST_URI)
|
||||
|
||||
assert result is None
|
||||
|
||||
Loading…
Reference in New Issue
Block a user