From c376ac4183e290449f032fb0f7ea289386bdacbe Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 28 May 2013 22:58:13 +0200 Subject: [PATCH 01/17] audio: Start adding playlist typefinder code. This allows gstreamer pipelines to determine when they are getting m3u, pls or xspf files and distinguish them from text/plain content. --- mopidy/audio/actor.py | 3 ++- mopidy/audio/playlists.py | 52 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 mopidy/audio/playlists.py diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index af0a0c68..a943b567 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -11,13 +11,14 @@ import pykka from mopidy.utils import process -from . import mixers, utils +from . import mixers, playlists, utils from .constants import PlaybackState from .listener import AudioListener logger = logging.getLogger('mopidy.audio') mixers.register_mixers() +playlists.register_typefinders() MB = 1 << 20 diff --git a/mopidy/audio/playlists.py b/mopidy/audio/playlists.py new file mode 100644 index 00000000..5235d4dd --- /dev/null +++ b/mopidy/audio/playlists.py @@ -0,0 +1,52 @@ +from __future__ import unicode_literals + +import pygst +pygst.require('0.10') +import gst + +import xml.dom.pulldom + + +# TODO: make detect_FOO_header reusable in general mopidy code. +# i.e. give it just a "peek" like function. +def detect_m3u_header(typefind): + return typefind.peek(0, 8) == b'#EXTM3U\n' + + +def detect_pls_header(typefind): + print repr(typefind.peek(0, 11) == b'[playlist]\n') + return typefind.peek(0, 11) == b'[playlist]\n' + + +def detect_xspf_header(typefind): + # Get more data than the 90 needed for header in case spacing is funny. + data = typefind.peek(0, 150) + + # Bail early if the words xml and playlist are not present. + if not data or b'xml' not in data or b'playlist' not in data: + return False + + # TODO: handle parser errors. + # Try parsing what we have, bailing on first element. + for event, node in xml.dom.pulldom.parseString(data): + if event == xml.dom.pulldom.START_ELEMENT: + return (node.tagName == 'playlist' and + node.node.namespaceURI == 'http://xspf.org/ns/0/') + return False + + +def playlist_typefinder(typefind, func, caps): + if func(typefind): + typefind.suggest(gst.TYPE_FIND_MAXIMUM, caps) + + +def register_typefind(mimetype, func, extensions): + caps = gst.caps_from_string(mimetype) + gst.type_find_register(mimetype, gst.RANK_PRIMARY, playlist_typefinder, + extensions, caps, func, caps) + + +def register_typefinders(): + register_typefind('audio/x-mpegurl', detect_m3u_header, [b'm3u', b'm3u8']) + register_typefind('audio/x-scpls', detect_pls_header, [b'pls']) + register_typefind('application/xspf+xml', detect_xspf_header, [b'xspf']) From 0f749702c48f74297370a7b87df3daac0754121f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 28 May 2013 23:10:16 +0200 Subject: [PATCH 02/17] audio: Add simple parsers for m3u, pls, xspf and uri lists. These parsers, and the detectors should probably be moved out at some point, but for now the simple version of these will do. --- mopidy/audio/playlists.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/mopidy/audio/playlists.py b/mopidy/audio/playlists.py index 5235d4dd..b7821ed7 100644 --- a/mopidy/audio/playlists.py +++ b/mopidy/audio/playlists.py @@ -4,6 +4,8 @@ import pygst pygst.require('0.10') import gst +import ConfigParser as configparser +import xml.etree.ElementTree import xml.dom.pulldom @@ -35,6 +37,36 @@ def detect_xspf_header(typefind): return False +def parse_m3u(data): + # TODO: convert non uris to file uris. + for line in data.readlines(): + if not line.startswith('#') and line.strip(): + yield line + + +def parse_pls(data): + # TODO: error handling of bad playlists. + # TODO: convert non uris to file uris. + cp = configparser.RawConfigParser() + cp.readfp(data) + for i in xrange(1, cp.getint('playlist', 'numberofentries')): + yield cp.get('playlist', 'file%d' % i) + + +def parse_xspf(data): + # TODO: handle parser errors + root = xml.etree.ElementTree.fromstring(data.read()) + tracklist = tree.find('{http://xspf.org/ns/0/}trackList') + for track in tracklist.findall('{http://xspf.org/ns/0/}track'): + yield track.findtext('{http://xspf.org/ns/0/}location') + + +def parse_urilist(data): + for line in data.readlines(): + if not line.startswith('#') and line.strip(): + yield line + + def playlist_typefinder(typefind, func, caps): if func(typefind): typefind.suggest(gst.TYPE_FIND_MAXIMUM, caps) From 1138ff879358d1ffc83ce97dd15ef90dc92dc023 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 28 May 2013 23:15:32 +0200 Subject: [PATCH 03/17] audui: Add BasePlaylistElement This element is the building block for the "decoders" that will convert the m3u, pls and xspf files to urilists and also the urilist player. --- mopidy/audio/playlists.py | 47 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/mopidy/audio/playlists.py b/mopidy/audio/playlists.py index b7821ed7..59877939 100644 --- a/mopidy/audio/playlists.py +++ b/mopidy/audio/playlists.py @@ -5,6 +5,7 @@ pygst.require('0.10') import gst import ConfigParser as configparser +import io import xml.etree.ElementTree import xml.dom.pulldom @@ -82,3 +83,49 @@ def register_typefinders(): register_typefind('audio/x-mpegurl', detect_m3u_header, [b'm3u', b'm3u8']) register_typefind('audio/x-scpls', detect_pls_header, [b'pls']) register_typefind('application/xspf+xml', detect_xspf_header, [b'xspf']) + + +class BasePlaylistElement(gst.Bin): + sinktemplate = None + srctemplate = None + ghostsrc = False + + def __init__(self): + super(BasePlaylistElement, self).__init__() + self._data = io.BytesIO() + self._done = False + + self.sink = gst.Pad(self.sinktemplate) + self.sink.set_chain_function(self._chain) + self.sink.set_event_function(self._event) + self.add_pad(self.sink) + + if self.ghostsrc: + self.src = gst.ghost_pad_new_notarget('src', gst.PAD_SRC) + else: + self.src = gst.Pad(self.srctemplate) + self.add_pad(self.src) + + def convert(self, data): + raise NotImplementedError + + def handle(self, uris): + self.src.push(gst.Buffer('\n'.join(uris))) + return False + + def _chain(self, pad, buf): + if not self._done: + self._data.write(buf.data) + return gst.FLOW_OK + return gst.FLOW_EOS + + def _event(self, pad, event): + if event.type == gst.EVENT_NEWSEGMENT: + return True + + if event.type == gst.EVENT_EOS: + self._done = True + self._data.seek(0) + if self.handle(list(self.convert(self._data))): + return True + return pad.event_default(event) From e2f9a3bad6a92f5d01e9de259cb01825b85973af Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 28 May 2013 23:22:24 +0200 Subject: [PATCH 04/17] audio: Add playlist decoders. These elements convert their respective formats to an urilist that we can handle in a genric way. --- mopidy/audio/actor.py | 2 + mopidy/audio/playlists.py | 80 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index a943b567..c68a5417 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -18,7 +18,9 @@ from .listener import AudioListener logger = logging.getLogger('mopidy.audio') mixers.register_mixers() + playlists.register_typefinders() +playlists.register_elements() MB = 1 << 20 diff --git a/mopidy/audio/playlists.py b/mopidy/audio/playlists.py index 59877939..4432887d 100644 --- a/mopidy/audio/playlists.py +++ b/mopidy/audio/playlists.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import pygst pygst.require('0.10') import gst +import gobject import ConfigParser as configparser import io @@ -129,3 +130,82 @@ class BasePlaylistElement(gst.Bin): if self.handle(list(self.convert(self._data))): return True return pad.event_default(event) + + +class M3UDecoder(BasePlaylistElement): + __gstdetails__ = ('M3U Decoder', + 'Decoder', + 'Convert .m3u to text/uri-list', + 'Mopidy') + + sinktemplate = gst.PadTemplate ('sink', + gst.PAD_SINK, + gst.PAD_ALWAYS, + gst.caps_from_string('audio/x-mpegurl')) + + srctemplate = gst.PadTemplate ('src', + gst.PAD_SRC, + gst.PAD_ALWAYS, + gst.caps_from_string('text/uri-list')) + + __gsttemplates__ = (sinktemplate, srctemplate) + + def convert(self, data): + return parse_m3u(data) + + +class PLSDecoder(BasePlaylistElement): + __gstdetails__ = ('PLS Decoder', + 'Decoder', + 'Convert .pls to text/uri-list', + 'Mopidy') + + sinktemplate = gst.PadTemplate ('sink', + gst.PAD_SINK, + gst.PAD_ALWAYS, + gst.caps_from_string('audio/x-scpls')) + + srctemplate = gst.PadTemplate ('src', + gst.PAD_SRC, + gst.PAD_ALWAYS, + gst.caps_from_string('text/uri-list')) + + __gsttemplates__ = (sinktemplate, srctemplate) + + def convert(self, data): + return parse_pls(data) + + +class XSPFDecoder(BasePlaylistElement): + __gstdetails__ = ('XSPF Decoder', + 'Decoder', + 'Convert .pls to text/uri-list', + 'Mopidy') + + sinktemplate = gst.PadTemplate ('sink', + gst.PAD_SINK, + gst.PAD_ALWAYS, + gst.caps_from_string('application/xspf+xml')) + + srctemplate = gst.PadTemplate ('src', + gst.PAD_SRC, + gst.PAD_ALWAYS, + gst.caps_from_string('text/uri-list')) + + __gsttemplates__ = (sinktemplate, srctemplate) + + def convert(self, data): + return parse_xspf(data) + + + +def register_element(element_class): + gobject.type_register(element_class) + gst.element_register( + element_class, element_class.__name__.lower(), gst.RANK_MARGINAL) + + +def register_elements(): + register_element(M3UDecoder) + register_element(PLSDecoder) + register_element(XSPFDecoder) From acbaab59e5c341eaa2e61bc18d317eedba2e66f8 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 28 May 2013 23:31:39 +0200 Subject: [PATCH 05/17] audio: A uri list element. This element "simply" takes the list of uris that our other elements have already converted to simpler format, picks the first uri and play it. This is done by ensuring that we block the right EOS messages, and all new segment messages from the original sources. With these events blocked we can inject our own nested uridecodebin to play the uri and push our own data. The nested uridecodebin is setup with caps('any') to ensure that we don't suddenly demux and end up with multiple streams by accident. --- mopidy/audio/playlists.py | 44 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/mopidy/audio/playlists.py b/mopidy/audio/playlists.py index 4432887d..764ec785 100644 --- a/mopidy/audio/playlists.py +++ b/mopidy/audio/playlists.py @@ -198,6 +198,49 @@ class XSPFDecoder(BasePlaylistElement): return parse_xspf(data) +class UriListElement(BasePlaylistElement): + __gstdetails__ = ('URIListDemuxer', + 'Demuxer', + 'Convert a text/uri-list to a stream', + 'Mopidy') + + sinktemplate = gst.PadTemplate ('sink', + gst.PAD_SINK, + gst.PAD_ALWAYS, + gst.caps_from_string('text/uri-list')) + + srctemplate = gst.PadTemplate ('src', + gst.PAD_SRC, + gst.PAD_ALWAYS, + gst.caps_new_any()) + + ghostsrc = True # We need to hook this up to our internal decodebin + + __gsttemplates__ = (sinktemplate, srctemplate) + + def __init__(self): + super(UriListElement, self).__init__() + self.uridecodebin = gst.element_factory_make('uridecodebin') + self.uridecodebin.connect('pad-added', self.pad_added) + # Limit to anycaps so we get a single stream out, letting other + # elmenets downstream figure out actual muxing + self.uridecodebin.set_property('caps', gst.caps_new_any()) + + def pad_added(self, src, pad): + self.src.set_target(pad) + + def handle(self, uris): + # TODO: hookup about to finish and errors to rest of uris so we + # round robin, only giving up once all have been tried. + self.add(self.uridecodebin) + self.uridecodebin.set_state(gst.STATE_READY) + self.uridecodebin.set_property('uri', uris[0]) + self.uridecodebin.sync_state_with_parent() + return True # Make sure we consume the EOS that triggered us. + + def convert(self, data): + return parse_urilist(data) + def register_element(element_class): gobject.type_register(element_class) @@ -209,3 +252,4 @@ def register_elements(): register_element(M3UDecoder) register_element(PLSDecoder) register_element(XSPFDecoder) + register_element(UriListElement) From a2b95c3a3a92d398933baee932808a09400be272 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 28 May 2013 23:54:23 +0200 Subject: [PATCH 06/17] audio: Cleanup playlists elements docs and interace. --- mopidy/audio/playlists.py | 93 +++++++++++++++++++++++++++------------ 1 file changed, 64 insertions(+), 29 deletions(-) diff --git a/mopidy/audio/playlists.py b/mopidy/audio/playlists.py index 764ec785..7c1a2d4c 100644 --- a/mopidy/audio/playlists.py +++ b/mopidy/audio/playlists.py @@ -40,7 +40,7 @@ def detect_xspf_header(typefind): def parse_m3u(data): - # TODO: convert non uris to file uris. + # TODO: convert non URIs to file URIs. for line in data.readlines(): if not line.startswith('#') and line.strip(): yield line @@ -48,7 +48,7 @@ def parse_m3u(data): def parse_pls(data): # TODO: error handling of bad playlists. - # TODO: convert non uris to file uris. + # TODO: convert non URIs to file URIs. cp = configparser.RawConfigParser() cp.readfp(data) for i in xrange(1, cp.getint('playlist', 'numberofentries')): @@ -87,31 +87,64 @@ def register_typefinders(): class BasePlaylistElement(gst.Bin): - sinktemplate = None - srctemplate = None - ghostsrc = False + """Base class for creating GStreamer elements for playlist support. + + This element performs the following steps: + + 1. Initializes src and sink pads for the element. + 2. Collects data from the sink until EOS is reached. + 3. Passes the collected data to :meth:`convert` to get a list of URIs. + 4. Passes the list of URIs to :meth:`handle`, default handling is to pass + the URIs to the src element as a uri-list. + 5. If handle returned true, the EOS consumed and nothing more happens, if + it is not consumed it flows on to the next element downstream, which is + likely our uri-list consumer which needs the EOS to know we are done + sending URIs. + """ + + sinkpad_template = None + """GStreamer pad template to use for sink, must be overriden.""" + + srcpad_template = None + """GStreamer pad template to use for src, must be overriden.""" + + ghost_srcpad = False + """Indicates if src pad should be ghosted or not.""" def __init__(self): + """Sets up src and sink pads plus behaviour.""" super(BasePlaylistElement, self).__init__() self._data = io.BytesIO() self._done = False - self.sink = gst.Pad(self.sinktemplate) - self.sink.set_chain_function(self._chain) - self.sink.set_event_function(self._event) - self.add_pad(self.sink) + self.sinkpad = gst.Pad(self.sinkpad_template) + self.sinkpad.set_chain_function(self._chain) + self.sinkpad.set_event_function(self._event) + self.add_pad(self.sinkpad) - if self.ghostsrc: - self.src = gst.ghost_pad_new_notarget('src', gst.PAD_SRC) + if self.ghost_srcpad: + self.srcpad = gst.ghost_pad_new_notarget('src', gst.PAD_SRC) else: - self.src = gst.Pad(self.srctemplate) - self.add_pad(self.src) + self.srcpad = gst.Pad(self.srcpad_template) + self.add_pad(self.srcpad) def convert(self, data): + """Convert the data we have colleted to URIs. + + :param data: Collected data buffer. + :type data: :class:`io.BytesIO` + :returns: iterable or generator of URIs. + """ raise NotImplementedError def handle(self, uris): - self.src.push(gst.Buffer('\n'.join(uris))) + """Do something usefull with the URIs. + + :param uris: List of URIs. + :type uris: :type:`list` + :returns: Boolean indicating if EOS should be consumed. + """ + self.srcpad.push(gst.Buffer('\n'.join(uris))) return False def _chain(self, pad, buf): @@ -129,6 +162,8 @@ class BasePlaylistElement(gst.Bin): self._data.seek(0) if self.handle(list(self.convert(self._data))): return True + + # Ensure we handle remaining events in a sane way. return pad.event_default(event) @@ -138,17 +173,17 @@ class M3UDecoder(BasePlaylistElement): 'Convert .m3u to text/uri-list', 'Mopidy') - sinktemplate = gst.PadTemplate ('sink', + sinkpad_template = gst.PadTemplate ('sink', gst.PAD_SINK, gst.PAD_ALWAYS, gst.caps_from_string('audio/x-mpegurl')) - srctemplate = gst.PadTemplate ('src', + srcpad_template = gst.PadTemplate ('src', gst.PAD_SRC, gst.PAD_ALWAYS, gst.caps_from_string('text/uri-list')) - __gsttemplates__ = (sinktemplate, srctemplate) + __gsttemplates__ = (sinkpad_template, srcpad_template) def convert(self, data): return parse_m3u(data) @@ -160,17 +195,17 @@ class PLSDecoder(BasePlaylistElement): 'Convert .pls to text/uri-list', 'Mopidy') - sinktemplate = gst.PadTemplate ('sink', + sinkpad_template = gst.PadTemplate ('sink', gst.PAD_SINK, gst.PAD_ALWAYS, gst.caps_from_string('audio/x-scpls')) - srctemplate = gst.PadTemplate ('src', + srcpad_template = gst.PadTemplate ('src', gst.PAD_SRC, gst.PAD_ALWAYS, gst.caps_from_string('text/uri-list')) - __gsttemplates__ = (sinktemplate, srctemplate) + __gsttemplates__ = (sinkpad_template, srcpad_template) def convert(self, data): return parse_pls(data) @@ -182,17 +217,17 @@ class XSPFDecoder(BasePlaylistElement): 'Convert .pls to text/uri-list', 'Mopidy') - sinktemplate = gst.PadTemplate ('sink', + sinkpad_template = gst.PadTemplate ('sink', gst.PAD_SINK, gst.PAD_ALWAYS, gst.caps_from_string('application/xspf+xml')) - srctemplate = gst.PadTemplate ('src', + srcpad_template = gst.PadTemplate ('src', gst.PAD_SRC, gst.PAD_ALWAYS, gst.caps_from_string('text/uri-list')) - __gsttemplates__ = (sinktemplate, srctemplate) + __gsttemplates__ = (sinkpad_template, srcpad_template) def convert(self, data): return parse_xspf(data) @@ -204,19 +239,19 @@ class UriListElement(BasePlaylistElement): 'Convert a text/uri-list to a stream', 'Mopidy') - sinktemplate = gst.PadTemplate ('sink', + sinkpad_template = gst.PadTemplate ('sink', gst.PAD_SINK, gst.PAD_ALWAYS, gst.caps_from_string('text/uri-list')) - srctemplate = gst.PadTemplate ('src', + srcpad_template = gst.PadTemplate ('src', gst.PAD_SRC, gst.PAD_ALWAYS, gst.caps_new_any()) - ghostsrc = True # We need to hook this up to our internal decodebin + ghost_srcpad = True # We need to hook this up to our internal decodebin - __gsttemplates__ = (sinktemplate, srctemplate) + __gsttemplates__ = (sinkpad_template, srcpad_template) def __init__(self): super(UriListElement, self).__init__() @@ -227,10 +262,10 @@ class UriListElement(BasePlaylistElement): self.uridecodebin.set_property('caps', gst.caps_new_any()) def pad_added(self, src, pad): - self.src.set_target(pad) + self.srcpad.set_target(pad) def handle(self, uris): - # TODO: hookup about to finish and errors to rest of uris so we + # TODO: hookup about to finish and errors to rest of URIs so we # round robin, only giving up once all have been tried. self.add(self.uridecodebin) self.uridecodebin.set_state(gst.STATE_READY) From a112275c25734dfe3f260b00f92628edbae9471e Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 29 May 2013 00:18:23 +0200 Subject: [PATCH 07/17] audio: Add IcySrc. Quick hack that wraps a regular HTTP src in a custom bin that converts icy:// to http:// - this is needed to get for instance http://somafm.com/m3u/groovesalad.m3u to work. --- mopidy/audio/playlists.py | 50 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/mopidy/audio/playlists.py b/mopidy/audio/playlists.py index 7c1a2d4c..ea7ff042 100644 --- a/mopidy/audio/playlists.py +++ b/mopidy/audio/playlists.py @@ -277,6 +277,55 @@ class UriListElement(BasePlaylistElement): return parse_urilist(data) +class IcySrc(gst.Bin, gst.URIHandler): + __gstdetails__ = ('IcySrc', + 'Src', + 'Http src wrapper for icy:// support.', + 'Mopidy') + + srcpad_template = gst.PadTemplate ('src', + gst.PAD_SRC, + gst.PAD_ALWAYS, + gst.caps_new_any()) + + __gsttemplates__ = (srcpad_template,) + + def __init__(self): + super(IcySrc, self).__init__() + self._httpsrc = gst.element_make_from_uri(gst.URI_SRC, 'http://') + try: + self._httpsrc.set_property('iradio-mode', True) + except TypeError: + pass + self.add(self._httpsrc) + + self._srcpad = gst.GhostPad('src', self._httpsrc.get_pad('src')) + self.add_pad(self._srcpad) + + @classmethod + def do_get_type_full(cls): + return gst.URI_SRC + + @classmethod + def do_get_protocols_full(cls): + return [b'icy', b'icyx'] + + def do_set_uri(self, uri): + if uri.startswith('icy://'): + return self._httpsrc.set_uri(b'http://' + uri[len('icy://'):]) + elif uri.startswith('icyx://'): + return self._httpsrc.set_uri(b'https://' + uri[len('icyx://'):]) + else: + return False + + def do_get_uri(self): + uri = self._httpsrc.get_uri() + if uri.startswith('http://'): + return b'icy://' + uri[len('http://'):] + else: + return b'icyx://' + uri[len('https://'):] + + def register_element(element_class): gobject.type_register(element_class) gst.element_register( @@ -288,3 +337,4 @@ def register_elements(): register_element(PLSDecoder) register_element(XSPFDecoder) register_element(UriListElement) + register_element(IcySrc) From d3f97c128c0fbf6d83bc99c4f3513542d47dcb91 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 2 Jun 2013 16:03:29 +0200 Subject: [PATCH 08/17] audio: Review cleanups --- mopidy/audio/playlists.py | 47 +++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/mopidy/audio/playlists.py b/mopidy/audio/playlists.py index ea7ff042..71c76cef 100644 --- a/mopidy/audio/playlists.py +++ b/mopidy/audio/playlists.py @@ -18,7 +18,6 @@ def detect_m3u_header(typefind): def detect_pls_header(typefind): - print repr(typefind.peek(0, 11) == b'[playlist]\n') return typefind.peek(0, 11) == b'[playlist]\n' @@ -65,7 +64,7 @@ def parse_xspf(data): def parse_urilist(data): for line in data.readlines(): - if not line.startswith('#') and line.strip(): + if not line.startswith('#') and gst.uri_is_valid(line.strip()): yield line @@ -131,18 +130,18 @@ class BasePlaylistElement(gst.Bin): def convert(self, data): """Convert the data we have colleted to URIs. - :param data: Collected data buffer. + :param data: collected data buffer :type data: :class:`io.BytesIO` - :returns: iterable or generator of URIs. + :returns: iterable or generator of URIs """ raise NotImplementedError def handle(self, uris): - """Do something usefull with the URIs. + """Do something useful with the URIs. - :param uris: List of URIs. + :param uris: list of URIs :type uris: :type:`list` - :returns: Boolean indicating if EOS should be consumed. + :returns: boolean indicating if EOS should be consumed """ self.srcpad.push(gst.Buffer('\n'.join(uris))) return False @@ -167,18 +166,18 @@ class BasePlaylistElement(gst.Bin): return pad.event_default(event) -class M3UDecoder(BasePlaylistElement): +class M3uDecoder(BasePlaylistElement): __gstdetails__ = ('M3U Decoder', 'Decoder', 'Convert .m3u to text/uri-list', 'Mopidy') - sinkpad_template = gst.PadTemplate ('sink', + sinkpad_template = gst.PadTemplate('sink', gst.PAD_SINK, gst.PAD_ALWAYS, gst.caps_from_string('audio/x-mpegurl')) - srcpad_template = gst.PadTemplate ('src', + srcpad_template = gst.PadTemplate('src', gst.PAD_SRC, gst.PAD_ALWAYS, gst.caps_from_string('text/uri-list')) @@ -189,18 +188,18 @@ class M3UDecoder(BasePlaylistElement): return parse_m3u(data) -class PLSDecoder(BasePlaylistElement): +class PlsDecoder(BasePlaylistElement): __gstdetails__ = ('PLS Decoder', 'Decoder', 'Convert .pls to text/uri-list', 'Mopidy') - sinkpad_template = gst.PadTemplate ('sink', + sinkpad_template = gst.PadTemplate('sink', gst.PAD_SINK, gst.PAD_ALWAYS, gst.caps_from_string('audio/x-scpls')) - srcpad_template = gst.PadTemplate ('src', + srcpad_template = gst.PadTemplate('src', gst.PAD_SRC, gst.PAD_ALWAYS, gst.caps_from_string('text/uri-list')) @@ -211,18 +210,18 @@ class PLSDecoder(BasePlaylistElement): return parse_pls(data) -class XSPFDecoder(BasePlaylistElement): +class XspfDecoder(BasePlaylistElement): __gstdetails__ = ('XSPF Decoder', 'Decoder', 'Convert .pls to text/uri-list', 'Mopidy') - sinkpad_template = gst.PadTemplate ('sink', + sinkpad_template = gst.PadTemplate('sink', gst.PAD_SINK, gst.PAD_ALWAYS, gst.caps_from_string('application/xspf+xml')) - srcpad_template = gst.PadTemplate ('src', + srcpad_template = gst.PadTemplate('src', gst.PAD_SRC, gst.PAD_ALWAYS, gst.caps_from_string('text/uri-list')) @@ -239,12 +238,12 @@ class UriListElement(BasePlaylistElement): 'Convert a text/uri-list to a stream', 'Mopidy') - sinkpad_template = gst.PadTemplate ('sink', + sinkpad_template = gst.PadTemplate('sink', gst.PAD_SINK, gst.PAD_ALWAYS, gst.caps_from_string('text/uri-list')) - srcpad_template = gst.PadTemplate ('src', + srcpad_template = gst.PadTemplate('src', gst.PAD_SRC, gst.PAD_ALWAYS, gst.caps_new_any()) @@ -258,7 +257,7 @@ class UriListElement(BasePlaylistElement): self.uridecodebin = gst.element_factory_make('uridecodebin') self.uridecodebin.connect('pad-added', self.pad_added) # Limit to anycaps so we get a single stream out, letting other - # elmenets downstream figure out actual muxing + # elements downstream figure out actual muxing self.uridecodebin.set_property('caps', gst.caps_new_any()) def pad_added(self, src, pad): @@ -280,10 +279,10 @@ class UriListElement(BasePlaylistElement): class IcySrc(gst.Bin, gst.URIHandler): __gstdetails__ = ('IcySrc', 'Src', - 'Http src wrapper for icy:// support.', + 'HTTP src wrapper for icy:// support.', 'Mopidy') - srcpad_template = gst.PadTemplate ('src', + srcpad_template = gst.PadTemplate('src', gst.PAD_SRC, gst.PAD_ALWAYS, gst.caps_new_any()) @@ -333,8 +332,8 @@ def register_element(element_class): def register_elements(): - register_element(M3UDecoder) - register_element(PLSDecoder) - register_element(XSPFDecoder) + register_element(M3uDecoder) + register_element(PlsDecoder) + register_element(XspfDecoder) register_element(UriListElement) register_element(IcySrc) From f67aa95c2ec7ccce3bd9398d8a7a0cd2324f922c Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 26 Jun 2013 22:29:13 +0200 Subject: [PATCH 09/17] audio: add basic .asx support --- mopidy/audio/playlists.py | 54 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/mopidy/audio/playlists.py b/mopidy/audio/playlists.py index 71c76cef..ca807ed9 100644 --- a/mopidy/audio/playlists.py +++ b/mopidy/audio/playlists.py @@ -34,7 +34,22 @@ def detect_xspf_header(typefind): for event, node in xml.dom.pulldom.parseString(data): if event == xml.dom.pulldom.START_ELEMENT: return (node.tagName == 'playlist' and - node.node.namespaceURI == 'http://xspf.org/ns/0/') + node.namespaceURI == 'http://xspf.org/ns/0/') + return False + + +def detect_asx_header(typefind): + data = typefind.peek(0, 50) + + # Bail early if the words xml and playlist are not present. + if not data or b'asx' not in data: + return False + + # TODO: handle parser errors. + # Try parsing what we have, bailing on first element. + for event, node in xml.dom.pulldom.parseString(data): + if event == xml.dom.pulldom.START_ELEMENT: + return node.tagName == 'asx' return False @@ -56,12 +71,21 @@ def parse_pls(data): def parse_xspf(data): # TODO: handle parser errors + # TODO: make sure tracklist == trackList etc. root = xml.etree.ElementTree.fromstring(data.read()) tracklist = tree.find('{http://xspf.org/ns/0/}trackList') for track in tracklist.findall('{http://xspf.org/ns/0/}track'): yield track.findtext('{http://xspf.org/ns/0/}location') +def parse_asx(data): + # TODO: handle parser errors + # TODO: make sure entry == Entry etc. + root = xml.etree.ElementTree.fromstring(data.read()) + for entry in root.findall('entry'): + yield entry.find('ref').attrib['href'] + + def parse_urilist(data): for line in data.readlines(): if not line.startswith('#') and gst.uri_is_valid(line.strip()): @@ -83,6 +107,9 @@ def register_typefinders(): register_typefind('audio/x-mpegurl', detect_m3u_header, [b'm3u', b'm3u8']) register_typefind('audio/x-scpls', detect_pls_header, [b'pls']) register_typefind('application/xspf+xml', detect_xspf_header, [b'xspf']) + # NOTE: seems we can't use video/x-ms-asf which is the correct mime for asx + # as it is shared with asf for streaming videos :/ + register_typefind('audio/x-ms-asx', detect_asx_header, [b'asx']) class BasePlaylistElement(gst.Bin): @@ -232,6 +259,28 @@ class XspfDecoder(BasePlaylistElement): return parse_xspf(data) +class AsxDecoder(BasePlaylistElement): + __gstdetails__ = ('ASX Decoder', + 'Decoder', + 'Convert .asx to text/uri-list', + 'Mopidy') + + sinkpad_template = gst.PadTemplate('sink', + gst.PAD_SINK, + gst.PAD_ALWAYS, + gst.caps_from_string('audio/x-ms-asx')) + + srcpad_template = gst.PadTemplate('src', + gst.PAD_SRC, + gst.PAD_ALWAYS, + gst.caps_from_string('text/uri-list')) + + __gsttemplates__ = (sinkpad_template, srcpad_template) + + def convert(self, data): + return parse_asx(data) + + class UriListElement(BasePlaylistElement): __gstdetails__ = ('URIListDemuxer', 'Demuxer', @@ -266,6 +315,7 @@ class UriListElement(BasePlaylistElement): def handle(self, uris): # TODO: hookup about to finish and errors to rest of URIs so we # round robin, only giving up once all have been tried. + # TODO: uris could be empty. self.add(self.uridecodebin) self.uridecodebin.set_state(gst.STATE_READY) self.uridecodebin.set_property('uri', uris[0]) @@ -335,5 +385,7 @@ def register_elements(): register_element(M3uDecoder) register_element(PlsDecoder) register_element(XspfDecoder) + register_element(AsxDecoder) register_element(UriListElement) + # TODO: only register if nothing can handle icy scheme register_element(IcySrc) From 0bcb805cf52f324e195ff62c7935ff5e57793cf7 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 26 Jun 2013 23:31:53 +0200 Subject: [PATCH 10/17] audio: Improve xml playlist handling --- mopidy/audio/playlists.py | 63 ++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/mopidy/audio/playlists.py b/mopidy/audio/playlists.py index ca807ed9..aa02d112 100644 --- a/mopidy/audio/playlists.py +++ b/mopidy/audio/playlists.py @@ -7,8 +7,11 @@ import gobject import ConfigParser as configparser import io -import xml.etree.ElementTree -import xml.dom.pulldom + +try: + import xml.etree.cElementTree as elementtree +except ImportError: + import xml.etree.ElementTree as elementtree # TODO: make detect_FOO_header reusable in general mopidy code. @@ -23,33 +26,22 @@ def detect_pls_header(typefind): def detect_xspf_header(typefind): # Get more data than the 90 needed for header in case spacing is funny. - data = typefind.peek(0, 150) - - # Bail early if the words xml and playlist are not present. - if not data or b'xml' not in data or b'playlist' not in data: - return False - - # TODO: handle parser errors. - # Try parsing what we have, bailing on first element. - for event, node in xml.dom.pulldom.parseString(data): - if event == xml.dom.pulldom.START_ELEMENT: - return (node.tagName == 'playlist' and - node.namespaceURI == 'http://xspf.org/ns/0/') + data = io.BytesIO(typefind.peek(0, 150)) + try: + for event, element in elementtree.iterparse(data, events=('start',)): + return element.tag.lower() == '{http://xspf.org/ns/0/}playlist' + except elementtree.ParseError: + pass return False def detect_asx_header(typefind): - data = typefind.peek(0, 50) - - # Bail early if the words xml and playlist are not present. - if not data or b'asx' not in data: - return False - - # TODO: handle parser errors. - # Try parsing what we have, bailing on first element. - for event, node in xml.dom.pulldom.parseString(data): - if event == xml.dom.pulldom.START_ELEMENT: - return node.tagName == 'asx' + data = io.BytesIO(typefind.peek(0, 50)) + try: + for event, element in elementtree.iterparse(data, events=('start',)): + return element.tag.lower() == 'asx' + except elementtree.ParseError: + pass return False @@ -71,19 +63,21 @@ def parse_pls(data): def parse_xspf(data): # TODO: handle parser errors - # TODO: make sure tracklist == trackList etc. - root = xml.etree.ElementTree.fromstring(data.read()) - tracklist = tree.find('{http://xspf.org/ns/0/}trackList') - for track in tracklist.findall('{http://xspf.org/ns/0/}track'): - yield track.findtext('{http://xspf.org/ns/0/}location') + for event, element in elementtree.iterparse(data): + element.tag = element.tag.lower() # normalize + + ns = 'http://xspf.org/ns/0/' + for track in element.iterfind('{%s}tracklist/{%s}track' % (ns, ns)): + yield track.findtext('{%s}location' % ns) def parse_asx(data): # TODO: handle parser errors - # TODO: make sure entry == Entry etc. - root = xml.etree.ElementTree.fromstring(data.read()) - for entry in root.findall('entry'): - yield entry.find('ref').attrib['href'] + for event, element in elementtree.iterparse(data): + element.tag = element.tag.lower() # normalize + + for ref in element.findall('entry/ref'): + yield ref.get('href', '').strip() def parse_urilist(data): @@ -170,6 +164,7 @@ class BasePlaylistElement(gst.Bin): :type uris: :type:`list` :returns: boolean indicating if EOS should be consumed """ + # TODO: handle unicode uris which we can get out of elementtree self.srcpad.push(gst.Buffer('\n'.join(uris))) return False From f3051c9dd32c8600a5ceee5386e378d70cc1073e Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 26 Jun 2013 23:34:55 +0200 Subject: [PATCH 11/17] audio: Only install icysrc when nothing is already supporting the scheme --- mopidy/audio/playlists.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/playlists.py b/mopidy/audio/playlists.py index aa02d112..17b94b97 100644 --- a/mopidy/audio/playlists.py +++ b/mopidy/audio/playlists.py @@ -382,5 +382,5 @@ def register_elements(): register_element(XspfDecoder) register_element(AsxDecoder) register_element(UriListElement) - # TODO: only register if nothing can handle icy scheme - register_element(IcySrc) + if not gst.element_make_from_uri(gst.URI_SRC, 'icy://'): + register_element(IcySrc) From 6e942a92b3a74d845f55b30e3e6a1b452d6a5b00 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 4 Jul 2013 17:49:10 +0200 Subject: [PATCH 12/17] audio: Post an error if an urilist expands to another urilist --- mopidy/audio/playlists.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/mopidy/audio/playlists.py b/mopidy/audio/playlists.py index 17b94b97..011326ee 100644 --- a/mopidy/audio/playlists.py +++ b/mopidy/audio/playlists.py @@ -306,8 +306,21 @@ class UriListElement(BasePlaylistElement): def pad_added(self, src, pad): self.srcpad.set_target(pad) + pad.add_event_probe(self.pad_event) + + def pad_event(self, pad, event): + if event.has_name('urilist-played'): + error = gst.GError(gst.RESOURCE_ERROR, gst.RESOURCE_ERROR_FAILED, + b'Nested playlists not supported.') + message = gst.message_new_error(self, error, b'Playlists pointing to other playlists is not supported') + self.post_message(message) + return True def handle(self, uris): + struct = gst.Structure('urilist-played') + event = gst.event_new_custom(gst.EVENT_CUSTOM_UPSTREAM, struct) + self.sinkpad.push_event(event) + # TODO: hookup about to finish and errors to rest of URIs so we # round robin, only giving up once all have been tried. # TODO: uris could be empty. From 9e682d9248a532121491c67f7691c94cb97371d5 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 6 Oct 2013 14:18:53 +0200 Subject: [PATCH 13/17] audio: Check for asx/xspf in data before parsing during detection. --- mopidy/audio/playlists.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/mopidy/audio/playlists.py b/mopidy/audio/playlists.py index 011326ee..9f56ea27 100644 --- a/mopidy/audio/playlists.py +++ b/mopidy/audio/playlists.py @@ -25,8 +25,10 @@ def detect_pls_header(typefind): def detect_xspf_header(typefind): - # Get more data than the 90 needed for header in case spacing is funny. - data = io.BytesIO(typefind.peek(0, 150)) + data = typefind.peek(0, 150) + if b'xspf' not in data: + return False + try: for event, element in elementtree.iterparse(data, events=('start',)): return element.tag.lower() == '{http://xspf.org/ns/0/}playlist' @@ -36,7 +38,10 @@ def detect_xspf_header(typefind): def detect_asx_header(typefind): - data = io.BytesIO(typefind.peek(0, 50)) + data = typefind.peek(0, 50) + if b'asx' not in data: + return False + try: for event, element in elementtree.iterparse(data, events=('start',)): return element.tag.lower() == 'asx' From 4a944332370d167c797866b3272c6fc0d2a7d6d4 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 6 Oct 2013 14:19:28 +0200 Subject: [PATCH 14/17] audio: Wrap long line, and explain conidtional instalation of icy element. --- mopidy/audio/playlists.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/playlists.py b/mopidy/audio/playlists.py index 9f56ea27..6f4fdd46 100644 --- a/mopidy/audio/playlists.py +++ b/mopidy/audio/playlists.py @@ -317,8 +317,8 @@ class UriListElement(BasePlaylistElement): if event.has_name('urilist-played'): error = gst.GError(gst.RESOURCE_ERROR, gst.RESOURCE_ERROR_FAILED, b'Nested playlists not supported.') - message = gst.message_new_error(self, error, b'Playlists pointing to other playlists is not supported') - self.post_message(message) + message = b'Playlists pointing to other playlists is not supported' + self.post_message(gst.message_new_error(self, error, message)) return True def handle(self, uris): @@ -400,5 +400,7 @@ def register_elements(): register_element(XspfDecoder) register_element(AsxDecoder) register_element(UriListElement) + + # Only register icy if gst install can't handle it on it's own. if not gst.element_make_from_uri(gst.URI_SRC, 'icy://'): register_element(IcySrc) From b1b522694e1c36da23feee5fb1e322a10e880201 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 6 Oct 2013 15:22:17 +0200 Subject: [PATCH 15/17] audio: Fix lots of playlist issues and add tests. --- mopidy/audio/playlists.py | 45 ++++++++---- tests/audio/playlists_test.py | 127 ++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 13 deletions(-) create mode 100644 tests/audio/playlists_test.py diff --git a/mopidy/audio/playlists.py b/mopidy/audio/playlists.py index 6f4fdd46..096348a0 100644 --- a/mopidy/audio/playlists.py +++ b/mopidy/audio/playlists.py @@ -21,7 +21,7 @@ def detect_m3u_header(typefind): def detect_pls_header(typefind): - return typefind.peek(0, 11) == b'[playlist]\n' + return typefind.peek(0, 11).lower() == b'[playlist]\n' def detect_xspf_header(typefind): @@ -30,7 +30,8 @@ def detect_xspf_header(typefind): return False try: - for event, element in elementtree.iterparse(data, events=('start',)): + data = io.BytesIO(data) + for event, element in elementtree.iterparse(data, events=(b'start',)): return element.tag.lower() == '{http://xspf.org/ns/0/}playlist' except elementtree.ParseError: pass @@ -43,7 +44,8 @@ def detect_asx_header(typefind): return False try: - for event, element in elementtree.iterparse(data, events=('start',)): + data = io.BytesIO(data) + for event, element in elementtree.iterparse(data, events=(b'start',)): return element.tag.lower() == 'asx' except elementtree.ParseError: pass @@ -52,24 +54,38 @@ def detect_asx_header(typefind): def parse_m3u(data): # TODO: convert non URIs to file URIs. + found_header = False for line in data.readlines(): + if found_header or line.startswith('#EXTM3U'): + found_header = True + else: + continue if not line.startswith('#') and line.strip(): - yield line + yield line.strip() def parse_pls(data): - # TODO: error handling of bad playlists. # TODO: convert non URIs to file URIs. - cp = configparser.RawConfigParser() - cp.readfp(data) - for i in xrange(1, cp.getint('playlist', 'numberofentries')): - yield cp.get('playlist', 'file%d' % i) + try: + cp = configparser.RawConfigParser() + cp.readfp(data) + except configparser.Error: + return + + for section in cp.sections(): + if section.lower() != 'playlist': + continue + for i in xrange(cp.getint(section, 'numberofentries')): + yield cp.get(section, 'file%d' % (i+1)) def parse_xspf(data): # TODO: handle parser errors - for event, element in elementtree.iterparse(data): - element.tag = element.tag.lower() # normalize + try: + for event, element in elementtree.iterparse(data): + element.tag = element.tag.lower() # normalize + except elementtree.ParseError: + return ns = 'http://xspf.org/ns/0/' for track in element.iterfind('{%s}tracklist/{%s}track' % (ns, ns)): @@ -78,8 +94,11 @@ def parse_xspf(data): def parse_asx(data): # TODO: handle parser errors - for event, element in elementtree.iterparse(data): - element.tag = element.tag.lower() # normalize + try: + for event, element in elementtree.iterparse(data): + element.tag = element.tag.lower() # normalize + except elementtree.ParseError: + return for ref in element.findall('entry/ref'): yield ref.get('href', '').strip() diff --git a/tests/audio/playlists_test.py b/tests/audio/playlists_test.py new file mode 100644 index 00000000..9f28527e --- /dev/null +++ b/tests/audio/playlists_test.py @@ -0,0 +1,127 @@ +#encoding: utf-8 + +from __future__ import unicode_literals + +import io +import unittest + +from mopidy.audio import playlists + +class TypeFind(object): + def __init__(self, data): + self.data = data + + def peek(self, start, end): + return self.data[start:end] + + +BAD = b'foobarbaz' + +M3U = b"""#EXTM3U +#EXTINF:123, Sample artist - Sample title +file:///tmp/foo +#EXTINF:321,Example Artist - Example title +file:///tmp/bar +#EXTINF:213,Some Artist - Other title +file:///tmp/baz +""" + +PLS = b"""[Playlist] +NumberOfEntries=3 +File1=file:///tmp/foo +Title1=Sample Title +Length1=123 +File2=file:///tmp/bar +Title2=Example title +Length2=321 +File3=file:///tmp/baz +Title3=Other title +Length3=213 +Version=2 +""" + +ASX = b""" + Example + + Sample Title + + + + Example title + + + + Other title + + + +""" + +XSPF = b""" + + + + Sample Title + file:///tmp/foo + + + Example title + file:///tmp/bar + + + Other title + file:///tmp/baz + + + +""" + + +class BasePlaylistTest(object): + valid = None + invalid = None + detect = None + parse = None + + def test_detect_valid_header(self): + self.assertTrue(self.detect(TypeFind(self.valid))) + + def test_detect_invalid_header(self): + self.assertFalse(self.detect(TypeFind(self.invalid))) + + def test_parse_valid_playlist(self): + uris = list(self.parse(io.BytesIO(self.valid))) + expected = [b'file:///tmp/foo', b'file:///tmp/bar', b'file:///tmp/baz'] + self.assertEqual(uris, expected) + + def test_parse_invalid_playlist(self): + uris = list(self.parse(io.BytesIO(self.invalid))) + self.assertEqual(uris, []) + + +class M3uPlaylistTest(BasePlaylistTest, unittest.TestCase): + valid = M3U + invalid = BAD + detect = staticmethod(playlists.detect_m3u_header) + parse = staticmethod(playlists.parse_m3u) + + +class PlsPlaylistTest(BasePlaylistTest, unittest.TestCase): + valid = PLS + invalid = BAD + detect = staticmethod(playlists.detect_pls_header) + parse = staticmethod(playlists.parse_pls) + + +class AsxPlsPlaylistTest(BasePlaylistTest, unittest.TestCase): + valid = ASX + invalid = BAD + detect = staticmethod(playlists.detect_asx_header) + parse = staticmethod(playlists.parse_asx) + + +class XspfPlaylistTest(BasePlaylistTest, unittest.TestCase): + valid = XSPF + invalid = BAD + detect = staticmethod(playlists.detect_xspf_header) + parse = staticmethod(playlists.parse_xspf) From a5e26f9b54b1f655f17f4dc671bf438a85536e93 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 6 Oct 2013 16:57:32 +0200 Subject: [PATCH 16/17] audio: flake8 fixes --- mopidy/audio/playlists.py | 57 ++++++++++++++--------------------- tests/audio/playlists_test.py | 15 ++++----- 2 files changed, 31 insertions(+), 41 deletions(-) diff --git a/mopidy/audio/playlists.py b/mopidy/audio/playlists.py index 096348a0..2f309e30 100644 --- a/mopidy/audio/playlists.py +++ b/mopidy/audio/playlists.py @@ -218,14 +218,12 @@ class M3uDecoder(BasePlaylistElement): 'Convert .m3u to text/uri-list', 'Mopidy') - sinkpad_template = gst.PadTemplate('sink', - gst.PAD_SINK, - gst.PAD_ALWAYS, + sinkpad_template = gst.PadTemplate( + 'sink', gst.PAD_SINK, gst.PAD_ALWAYS, gst.caps_from_string('audio/x-mpegurl')) - srcpad_template = gst.PadTemplate('src', - gst.PAD_SRC, - gst.PAD_ALWAYS, + srcpad_template = gst.PadTemplate( + 'src', gst.PAD_SRC, gst.PAD_ALWAYS, gst.caps_from_string('text/uri-list')) __gsttemplates__ = (sinkpad_template, srcpad_template) @@ -240,14 +238,12 @@ class PlsDecoder(BasePlaylistElement): 'Convert .pls to text/uri-list', 'Mopidy') - sinkpad_template = gst.PadTemplate('sink', - gst.PAD_SINK, - gst.PAD_ALWAYS, + sinkpad_template = gst.PadTemplate( + 'sink', gst.PAD_SINK, gst.PAD_ALWAYS, gst.caps_from_string('audio/x-scpls')) - srcpad_template = gst.PadTemplate('src', - gst.PAD_SRC, - gst.PAD_ALWAYS, + srcpad_template = gst.PadTemplate( + 'src', gst.PAD_SRC, gst.PAD_ALWAYS, gst.caps_from_string('text/uri-list')) __gsttemplates__ = (sinkpad_template, srcpad_template) @@ -262,14 +258,12 @@ class XspfDecoder(BasePlaylistElement): 'Convert .pls to text/uri-list', 'Mopidy') - sinkpad_template = gst.PadTemplate('sink', - gst.PAD_SINK, - gst.PAD_ALWAYS, + sinkpad_template = gst.PadTemplate( + 'sink', gst.PAD_SINK, gst.PAD_ALWAYS, gst.caps_from_string('application/xspf+xml')) - srcpad_template = gst.PadTemplate('src', - gst.PAD_SRC, - gst.PAD_ALWAYS, + srcpad_template = gst.PadTemplate( + 'src', gst.PAD_SRC, gst.PAD_ALWAYS, gst.caps_from_string('text/uri-list')) __gsttemplates__ = (sinkpad_template, srcpad_template) @@ -284,14 +278,12 @@ class AsxDecoder(BasePlaylistElement): 'Convert .asx to text/uri-list', 'Mopidy') - sinkpad_template = gst.PadTemplate('sink', - gst.PAD_SINK, - gst.PAD_ALWAYS, + sinkpad_template = gst.PadTemplate( + 'sink', gst.PAD_SINK, gst.PAD_ALWAYS, gst.caps_from_string('audio/x-ms-asx')) - srcpad_template = gst.PadTemplate('src', - gst.PAD_SRC, - gst.PAD_ALWAYS, + srcpad_template = gst.PadTemplate( + 'src', gst.PAD_SRC, gst.PAD_ALWAYS, gst.caps_from_string('text/uri-list')) __gsttemplates__ = (sinkpad_template, srcpad_template) @@ -306,14 +298,12 @@ class UriListElement(BasePlaylistElement): 'Convert a text/uri-list to a stream', 'Mopidy') - sinkpad_template = gst.PadTemplate('sink', - gst.PAD_SINK, - gst.PAD_ALWAYS, + sinkpad_template = gst.PadTemplate( + 'sink', gst.PAD_SINK, gst.PAD_ALWAYS, gst.caps_from_string('text/uri-list')) - srcpad_template = gst.PadTemplate('src', - gst.PAD_SRC, - gst.PAD_ALWAYS, + srcpad_template = gst.PadTemplate( + 'src', gst.PAD_SRC, gst.PAD_ALWAYS, gst.caps_new_any()) ghost_srcpad = True # We need to hook this up to our internal decodebin @@ -338,7 +328,7 @@ class UriListElement(BasePlaylistElement): b'Nested playlists not supported.') message = b'Playlists pointing to other playlists is not supported' self.post_message(gst.message_new_error(self, error, message)) - return True + return 1 # GST_PAD_PROBE_OK def handle(self, uris): struct = gst.Structure('urilist-played') @@ -364,9 +354,8 @@ class IcySrc(gst.Bin, gst.URIHandler): 'HTTP src wrapper for icy:// support.', 'Mopidy') - srcpad_template = gst.PadTemplate('src', - gst.PAD_SRC, - gst.PAD_ALWAYS, + srcpad_template = gst.PadTemplate( + 'src', gst.PAD_SRC, gst.PAD_ALWAYS, gst.caps_new_any()) __gsttemplates__ = (srcpad_template,) diff --git a/tests/audio/playlists_test.py b/tests/audio/playlists_test.py index 9f28527e..0f031736 100644 --- a/tests/audio/playlists_test.py +++ b/tests/audio/playlists_test.py @@ -7,13 +7,6 @@ import unittest from mopidy.audio import playlists -class TypeFind(object): - def __init__(self, data): - self.data = data - - def peek(self, start, end): - return self.data[start:end] - BAD = b'foobarbaz' @@ -77,6 +70,14 @@ XSPF = b""" """ +class TypeFind(object): + def __init__(self, data): + self.data = data + + def peek(self, start, end): + return self.data[start:end] + + class BasePlaylistTest(object): valid = None invalid = None From c235f7259a4065111f3125bd7b11a3a627c696a4 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 6 Oct 2013 19:56:03 +0200 Subject: [PATCH 17/17] audo: Remove stale TODOs --- mopidy/audio/playlists.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mopidy/audio/playlists.py b/mopidy/audio/playlists.py index 2f309e30..e3f51e41 100644 --- a/mopidy/audio/playlists.py +++ b/mopidy/audio/playlists.py @@ -80,7 +80,6 @@ def parse_pls(data): def parse_xspf(data): - # TODO: handle parser errors try: for event, element in elementtree.iterparse(data): element.tag = element.tag.lower() # normalize @@ -93,7 +92,6 @@ def parse_xspf(data): def parse_asx(data): - # TODO: handle parser errors try: for event, element in elementtree.iterparse(data): element.tag = element.tag.lower() # normalize