diff --git a/docs/changelog.rst b/docs/changelog.rst index 395e968b..09b7840c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -25,6 +25,10 @@ a temporary regression of :issue:`527`. - Added support for deprecated config values in order to allow for graceful removal of :confval:`local/tag_cache_file`. +**Streaming backend** + +- Live lookup of URI metadata has been added. (Fixes :issue:`540`) + **Internal changes** - Events from the audio actor, backends, and core actor are now emitted diff --git a/docs/ext/stream.rst b/docs/ext/stream.rst index ee413b31..30bc22ab 100644 --- a/docs/ext/stream.rst +++ b/docs/ext/stream.rst @@ -42,6 +42,10 @@ Configuration values Whitelist of URI schemas to allow streaming from. Values should be separated by either comma or newline. +.. confval:: stream/timeout + + Number of milliseconds before giving up looking up stream metadata. + Usage ===== diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 12476c2c..f797a84d 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -83,15 +83,14 @@ class Scanner(object): """Polls for messages to collect data.""" start = time.time() timeout_s = self._timeout_ms / float(1000) - poll_timeout_ns = 1000 data = {} while time.time() - start < timeout_s: - message = self._bus.poll(gst.MESSAGE_ANY, poll_timeout_ns) + if not self._bus.have_pending(): + continue + message = self._bus.pop() - if message is None: - pass # polling the bus timed out. - elif message.type == gst.MESSAGE_ERROR: + if message.type == gst.MESSAGE_ERROR: raise exceptions.ScannerError(message.parse_error()[0]) elif message.type == gst.MESSAGE_EOS: return data @@ -133,7 +132,7 @@ def audio_data_to_track(data): def _retrieve(source_key, target_key, target): if source_key in data: - target[target_key] = data[source_key] + target.setdefault(target_key, data[source_key]) _retrieve(gst.TAG_ALBUM, 'name', album_kwargs) _retrieve(gst.TAG_TRACK_COUNT, 'num_tracks', album_kwargs) @@ -156,6 +155,11 @@ def audio_data_to_track(data): _retrieve( 'musicbrainz-albumartistid', 'musicbrainz_id', albumartist_kwargs) + # For streams, will not override if a better value has already been set. + _retrieve(gst.TAG_ORGANIZATION, 'name', track_kwargs) + _retrieve(gst.TAG_LOCATION, 'comment', track_kwargs) + _retrieve(gst.TAG_COPYRIGHT, 'comment', track_kwargs) + if gst.TAG_DATE in data and data[gst.TAG_DATE]: date = data[gst.TAG_DATE] try: @@ -168,9 +172,13 @@ def audio_data_to_track(data): if albumartist_kwargs: album_kwargs['artists'] = [Artist(**albumartist_kwargs)] + if data['mtime']: + track_kwargs['last_modified'] = int(data['mtime']) + + if data[gst.TAG_DURATION]: + track_kwargs['length'] = data[gst.TAG_DURATION] // gst.MSECOND + track_kwargs['uri'] = data['uri'] - track_kwargs['last_modified'] = int(data['mtime']) - track_kwargs['length'] = data[gst.TAG_DURATION] // gst.MSECOND track_kwargs['album'] = Album(**album_kwargs) if ('name' in artist_kwargs diff --git a/mopidy/backends/stream/__init__.py b/mopidy/backends/stream/__init__.py index 061ac5d0..47dd6151 100644 --- a/mopidy/backends/stream/__init__.py +++ b/mopidy/backends/stream/__init__.py @@ -19,6 +19,8 @@ class Extension(ext.Extension): def get_config_schema(self): schema = super(Extension, self).get_config_schema() schema['protocols'] = config.List() + schema['timeout'] = config.Integer( + minimum=1000, maximum=1000 * 60 * 60) return schema def validate_environment(self): diff --git a/mopidy/backends/stream/actor.py b/mopidy/backends/stream/actor.py index 86df447d..c807e09d 100644 --- a/mopidy/backends/stream/actor.py +++ b/mopidy/backends/stream/actor.py @@ -5,7 +5,8 @@ import urlparse import pykka -from mopidy import audio as audio_lib +from mopidy import audio as audio_lib, exceptions +from mopidy.audio import scan from mopidy.backends import base from mopidy.models import Track @@ -16,7 +17,8 @@ class StreamBackend(pykka.ThreadingActor, base.Backend): def __init__(self, config, audio): super(StreamBackend, self).__init__() - self.library = StreamLibraryProvider(backend=self) + self.library = StreamLibraryProvider( + backend=self, timeout=config['stream']['timeout']) self.playback = base.BasePlaybackProvider(audio=audio, backend=self) self.playlists = None @@ -24,14 +26,20 @@ class StreamBackend(pykka.ThreadingActor, base.Backend): config['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 __init__(self, backend, timeout): + super(StreamLibraryProvider, self).__init__(backend) + self._scanner = scan.Scanner(min_duration=None, timeout=timeout) + 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)] + + try: + data = self._scanner.scan(uri) + track = scan.audio_data_to_track(data) + except exceptions.ScannerError as e: + logger.warning('Problem looking up %s: %s', uri, e) + track = Track(uri=uri, name=uri) + + return [track] diff --git a/mopidy/backends/stream/ext.conf b/mopidy/backends/stream/ext.conf index dc0287da..811dec88 100644 --- a/mopidy/backends/stream/ext.conf +++ b/mopidy/backends/stream/ext.conf @@ -8,3 +8,4 @@ protocols = rtmp rtmps rtsp +timeout = 5000