diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 5946bd73..60e88a9d 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -13,7 +13,7 @@ import gst.pbutils # noqa import pykka from mopidy import exceptions -from mopidy.audio import icy, playlists, utils +from mopidy.audio import icy, utils from mopidy.audio.constants import PlaybackState from mopidy.audio.listener import AudioListener from mopidy.internal import deprecation, process @@ -27,8 +27,6 @@ logger = logging.getLogger(__name__) gst_logger = logging.getLogger('mopidy.audio.gst') icy.register() -playlists.register_typefinders() -playlists.register_elements() _GST_STATE_MAPPING = { gst.STATE_PLAYING: PlaybackState.PLAYING, diff --git a/mopidy/audio/playlists.py b/mopidy/audio/playlists.py index ddbd9c66..34e3ed7a 100644 --- a/mopidy/audio/playlists.py +++ b/mopidy/audio/playlists.py @@ -2,8 +2,6 @@ from __future__ import absolute_import, unicode_literals import io -import gobject - import pygst pygst.require('0.10') import gst # noqa @@ -107,262 +105,3 @@ def parse_asx(data): for entry in element.findall('entry[@href]'): yield entry.get('href', '').strip() - - -def parse_urilist(data): - for line in data.readlines(): - if not line.startswith('#') and gst.uri_is_valid(line.strip()): - yield line - - -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']) - # 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): - - """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.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.ghost_srcpad: - self.srcpad = gst.ghost_pad_new_notarget('src', gst.PAD_SRC) - else: - 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): - """Do something useful with the URIs. - - :param uris: list of URIs - :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 - - 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 - - # Ensure we handle remaining events in a sane way. - return pad.event_default(event) - - -class M3uDecoder(BasePlaylistElement): - __gstdetails__ = ('M3U Decoder', - 'Decoder', - 'Convert .m3u to text/uri-list', - 'Mopidy') - - 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, - gst.caps_from_string('text/uri-list')) - - __gsttemplates__ = (sinkpad_template, srcpad_template) - - def convert(self, data): - return parse_m3u(data) - - -class PlsDecoder(BasePlaylistElement): - __gstdetails__ = ('PLS Decoder', - 'Decoder', - 'Convert .pls to text/uri-list', - 'Mopidy') - - 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, - gst.caps_from_string('text/uri-list')) - - __gsttemplates__ = (sinkpad_template, srcpad_template) - - def convert(self, data): - return parse_pls(data) - - -class XspfDecoder(BasePlaylistElement): - __gstdetails__ = ('XSPF Decoder', - 'Decoder', - 'Convert .pls to text/uri-list', - 'Mopidy') - - 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, - gst.caps_from_string('text/uri-list')) - - __gsttemplates__ = (sinkpad_template, srcpad_template) - - def convert(self, data): - 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', - 'Convert a text/uri-list to a stream', - 'Mopidy') - - 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, - gst.caps_new_any()) - - ghost_srcpad = True # We need to hook this up to our internal decodebin - - __gsttemplates__ = (sinkpad_template, srcpad_template) - - 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 - # elements downstream figure out actual muxing - self.uridecodebin.set_property('caps', gst.caps_new_any()) - - 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 = b'Playlists pointing to other playlists is not supported' - self.post_message(gst.message_new_error(self, error, message)) - return 1 # GST_PAD_PROBE_OK - - 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. - 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) - gst.element_register( - element_class, element_class.__name__.lower(), gst.RANK_MARGINAL) - - -def register_elements(): - register_element(M3uDecoder) - register_element(PlsDecoder) - register_element(XspfDecoder) - register_element(AsxDecoder) - register_element(UriListElement)