  
      def discover(self,path):
          d = discoverer.Discoverer(path)
-diff -ru /mnt/cdrom/svn/xiph/subtle/Subtitles/Discoverer.py subtle/Subtitles/Discoverer.py
---- /mnt/cdrom/svn/xiph/subtle/Subtitles/Discoverer.py	2008-08-08 16:52:38.000000000 +0100
-+++ subtle/Subtitles/Discoverer.py	2008-08-09 19:37:23.000000000 +0100
-@@ -41,10 +41,10 @@
+
+=== modified file 'Subtle/Subtitles/Discoverer.py'
+--- Subtle/Subtitles/Discoverer.py	2008-08-09 01:28:27 +0000
++++ Subtle/Subtitles/Discoverer.py	2008-09-06 15:57:09 +0000
+@@ -40,8 +40,8 @@
+     """
  
      for func in discover_funcs:
-         print "Testing this func: %s" % func
 -        handle = func(file)
 -        if handle is not None:
--            print "I have found a handle: %s" % handle
--            return handle(file)
+-            return handle(file)            
 +        handles = func(file)
 +        if handles is not None:
-+            print "I have found a handle list: %s" % handles
 +            return handles
-     else:
-         print '----'            
- 
-diff -ru /mnt/cdrom/svn/xiph/subtle/Subtitles/Formats/Softni.py subtle/Subtitles/Formats/Softni.py
---- /mnt/cdrom/svn/xiph/subtle/Subtitles/Formats/Softni.py	2008-08-08 16:52:38.000000000 +0100
-+++ subtle/Subtitles/Formats/Softni.py	2008-08-09 19:59:44.000000000 +0100
-@@ -53,6 +53,9 @@
+ 
+ 
+
+=== added file 'Subtle/Subtitles/Formats/Kate.py'
+--- Subtle/Subtitles/Formats/Kate.py	1970-01-01 00:00:00 +0000
++++ Subtle/Subtitles/Formats/Kate.py	2008-09-17 16:49:37 +0000
+@@ -0,0 +1,619 @@
++#!/usr/bin/env python
++#
++#       Kate.py
++#       
++#       Copyright 2008 ogg.k.ogg.k <ogg.k.ogg.k@googlemail.com>
++#       
++#       This program is free software; you can redistribute it and/or modify
++#       it under the terms of the GNU General Public License as published by
++#       the Free Software Foundation; either version 3 of the License, or
++#       (at your option) any later version.
++#       
++#       This program is distributed in the hope that it will be useful,
++#       but WITHOUT ANY WARRANTY; without even the implied warranty of
++#       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
++#       GNU General Public License for more details.
++#       
++#       You should have received a copy of the GNU General Public License
++#       along with this program; if not, write to the Free Software
++#       Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
++#       MA 02110-1301, USA.
++
++import pygtk
++pygtk.require('2.0')
++
++import gobject
++gobject.threads_init()
++
++import gst
++import gst.interfaces
++import pygst
++#pygst.require('0.10')
++import gtk
++import os
++import time
++from UserString import UserString
++import string
++from .. Subtitles import Subtitles
++
++"""
++    Documentation for the Kate module of the Subtle project.
++
++    Information about the Kate codec may be found at:
++    http://wiki.xiph.org/index.php/OggKate
++
++    This module requires the GStreamer Kate plugin. At the time of this writing,
++    the Kate plugin is not included within GStreamer yet. A patch to gst-plugins-bad
++    is included in the libkate distribution (which is also needed), available at:
++    http://libkate.googlecode.com/
++
++    This module only handles text and timing, so any other information that may
++    be present in a Kate stream (such as graphics, motions, etc) will be LOST when
++    saving it.
++
++    Any number of Kate streams (eg, one for each of a set of languages) may be
++    muxed in an Ogg container. This module supports loading and saving those, but
++    Subtle does not yet support switching between those for edition. Nevertheless,
++    all Kate streams will be saved back.
++"""
++
++
++class KateBase:
++    """
++        Base class for loading/saving Kate streams.
++    """
++
++    def __init__(self, filename, save=False, play=True, subs=None, on_pad_added=None):
++        """
++            constructor: opens filename, calling on_pad_added for each pad found
++        """
++
++        self.filename = filename
++        self.ready = False
++        self.error = False
++        if save:
++            self.subs = subs
++        else:
++            self.subs = {}
++        self.padmap = {}
++        self.katedec = {}
++        self.num_kate_streams = 0
++
++        self.pipeline = gst.Pipeline()
++
++        # make sure we get the messages we need
++        bus = self.pipeline.get_bus()
++        bus.add_signal_watch()
++        bus.connect('message::eos', self.__on_eos)
++        bus.connect('message::error', self.__on_error)
++        #bus.connect('message', self.__on_msg)
++        #bus.enable_sync_message_emission()
++        #bus.connect('sync-message', self.__on_sync_msg)
++
++        # add filesrc and oggdemux, which will dynamically branch into as many katedec as we need
++        self.filesrc = gst.element_factory_make('filesrc')
++        self.filesrc.set_property('location', filename)
++        self.pipeline.add(self.filesrc)
++        self.oggdemux = gst.element_factory_make('oggdemux')
++        self.pipeline.add(self.oggdemux)
++        gst.element_link_many(self.filesrc, self.oggdemux)
++
++        # if saving, we mux and save too
++        if save:
++            self.parsefactories = self.find_parsers()
++            self.parsers = []
++
++            self.oggmux_sources = []
++
++            self.oggmux = gst.element_factory_make('oggmux')
++            self.pipeline.add(self.oggmux)
++            self.filesink = gst.element_factory_make('filesink')
++            self.filesink.set_property('location', filename + '.subtle')
++            self.pipeline.add(self.filesink)
++            gst.element_link_many(self.oggmux, self.filesink)
++
++        # we will create katedec dynamically for each Kate stream encountered
++        self.oggdemux.connect('pad-added', on_pad_added)
++        self.oggdemux.connect('no-more-pads', self.__on_no_more_pads)
++
++        # start reading, and listen for pads being added until no more pads or timeout
++        self.pipeline.set_state(gst.STATE_PAUSED)
++        probe_time = 0
++        while not self.ready:
++                if probe_time > 2:
++                    print 'Failed to detect contents within 2 seconds'
++                    break
++                probe_time += 0.1
++                time.sleep(0.1)
++
++        # if saving, we just linked a pipeline with elements in the ready state, start them
++        if save and play:
++            print 'save and play: setting elements to playing'
++            self.oggmux_sources.append(self.oggmux)
++            self.oggmux_sources.append(self.filesink)
++            self.set_state(gst.STATE_PLAYING)
++
++        # if we have Kate streams, stream the entire file
++        if play and not self.error and self.get_num_kate_streams()>0:
++            self.play()
++
++        # we're done, stop the pipeline
++        self.close()
++
++        # swap the files or remove failed write
++        if save:
++            if not self.error:
++                os.unlink(filename)
++                os.rename(filename+'.subtle', filename)
++            else:
++                os.unlink(filename+'.subtle')
++
++#==============================================================================
++    def s2n(self,state):
++        if state==gst.STATE_NULL:
++           return 'NULL'
++        if state==gst.STATE_READY:
++           return 'READY'
++        if state==gst.STATE_PAUSED:
++           return 'PAUSED'
++        if state==gst.STATE_PLAYING:
++           return 'PLAYING'
++        return 'UNKNOWN'
++
++    def set_state(self,state):
++        for src in self.oggmux_sources:
++            #print 'setting state for %s: %s -> %s' % (src, self.s2n(src.get_state(gst.SECOND)), self.s2n(state))
++            src.set_state(state)
++
++#==============================================================================
++    def play(self):
++        """
++            set the pipeline to play, to process the entire stream
++        """
++
++        print 'Preparing to play'
++        bus = self.pipeline.get_bus()
++
++        ret = self.pipeline.set_state(gst.STATE_PLAYING)
++        if ret != gst.STATE_CHANGE_ASYNC and ret != gst.STATE_CHANGE_SUCCESS:
++            print 'Failed to set pipeline to playing'
++            return False
++        print 'Playing...'
++        while 1:
++            msg = bus.poll(gst.MESSAGE_EOS | gst.MESSAGE_ERROR, gst.SECOND)
++            if msg:
++                break
++        print 'Playing done'
++        return True
++
++
++#==============================================================================
++    def close(self):
++        self.pipeline.set_state(gst.STATE_NULL)
++
++
++#==============================================================================
++    def get_subs(self):
++        return self.subs
++
++
++#==============================================================================
++    def find_parsers(self):
++        """
++            finds all the parsers (from sample code from pygst), so we can
++            pipe non Kate streams
++        """
++
++        registry = gst.registry_get_default()
++        ret = {}
++        for f in registry.get_feature_list(gst.ElementFactory):
++            if f.get_klass().find('Parser') >= 0:
++                for t in f.get_static_pad_templates():
++                    if t.direction == gst.PAD_SINK:
++                        for s in t.get_caps():
++                            ret[s.get_name()] = f.get_name()
++                        break
++        return ret
++
++
++#==============================================================================
++    def check_kate_plugin(self, pad_name):
++        """
++            Checks whether the Kate GStreamer plugin is available, and bails
++            out with an error message if not
++        """
++        if not 'kate' in map(lambda p: p.get_name(), gst.registry_get_default().get_plugin_list()):
++            msg = 'The Kate GStreamer plugin was not found\ncannot use Kate stream %s\n'
++            print msg % pad_name
++            m = gtk.MessageDialog(gtk.Window(gtk.WINDOW_TOPLEVEL),
++                                  gtk.DIALOG_DESTROY_WITH_PARENT,
++                                  gtk.MESSAGE_ERROR,
++                                  gtk.BUTTONS_CLOSE,
++                                  msg % pad_name)
++            m.set_title('Kate plugin not found')
++            m.connect("response", lambda dlg, resp: dlg.destroy())
++            m.show()
++            return False
++        else:
++            return True
++
++
++#==============================================================================
++    def __on_no_more_pads(self, element):
++        print 'No more pads'
++        self.ready = True
++
++
++#==============================================================================
++    def get_num_kate_streams(self):
++        return self.num_kate_streams
++
++
++#==============================================================================
++    def __on_eos(self, bus, msg):
++        print 'EOS'
++        self.eos = True
++        self.ready = True
++
++
++#==============================================================================
++    def __on_error(self, bus, msg):
++        err, debug = msg.parse_error()
++        print 'Error: %s' %err, debug
++        m = gtk.MessageDialog(gtk.Window(gtk.WINDOW_TOPLEVEL),
++                              gtk.DIALOG_DESTROY_WITH_PARENT,
++                              gtk.MESSAGE_ERROR,
++                              gtk.BUTTONS_CLOSE,
++                              'Error: %s' %err)
++        m.set_title('Error')
++        m.connect("response", lambda dlg, resp: dlg.destroy())
++        m.show()
++
++        self.error = True
++        self.ready = True
++
++
++#==============================================================================
++    def __on_msg(self, bus, msg):
++        if msg.structure is None:
++            return
++        #name = msg.structure.get_name()
++        #print 'msg: %s' % name
++        #if msg.type == gst.MESSAGE_STATE_CHANGED:
++        #    old, new, pen = msg.parse_state_changed()
++        #    print '%r: state change: %r -> %r, %r pending' % (msg.src.get_path_string(), old, new, pen)
++
++
++#==============================================================================
++    def __on_sync_msg(self, bus, msg):
++        if msg.structure is None:
++            return
++        name = msg.structure.get_name()
++        #print 'sync: %s' % name
++
++
++
++
++#==============================================================================
++class KateSaver(KateBase):
++    """
++        Class for saving Kate streams.
++    """
++
++    def __init__(self, filename, subs):
++        """
++            constructor: saves the given subtitles as Kate streams to an Ogg file
++        """
++
++        KateBase.__init__(self, filename, True, True, subs, self.__on_pad_added)
++
++
++#==============================================================================
++    def __on_pad_added(self, element, pad):
++        """
++            Intercepts Kate streams we have previously loaded as subtitles and
++            replaces them with the subtitles.
++            Pipes all other streams (Kate and non Kate) through an appropriate parser, 
++        """
++
++        parse_only = True
++        caps = pad.get_caps()
++        mime = caps[0].get_name()
++        print 'New pad, caps %s' % mime
++        if mime == 'application/x-kate':
++            pad_name = pad.get_name()
++            print 'Found Kate stream: %s' % pad_name
++            self.num_kate_streams += 1
++
++            # Check if the Kate plugin is available
++            if self.check_kate_plugin(pad_name):
++                serial = int(string.replace(pad_name, 'serial_', '', 1),16)
++                # get next subtitles object
++                sub = None
++                for tmp in self.subs:
++                    #print "comparing serials %d and %d" % (tmp.getUserdata(), serial)
++                    if tmp.getUserdata() == serial:
++                        sub = tmp
++                        break
++
++                if sub != None:
++                    parse_only = False
++
++                    # intercept the stream by piping it to a fakesink
++                    fakesink = gst.element_factory_make('fakesink')
++                    fakesink.set_name('fakesink_'+pad_name)
++                    self.pipeline.add(fakesink)
++                    pad.link(fakesink.get_pad('sink'))
++
++                    # add a katesrc and kate encoder to replace it
++                    katesrc = KateSource(sub, 'katesrc_'+pad_name)
++                    self.pipeline.add(katesrc)
++                    kateenc = gst.element_factory_make('kateenc')
++                    kateenc.set_name('kateenc_'+pad_name)
++                    kateenc.set_property('language', sub.getLanguage())
++                    kateenc.set_property('category', sub.getCategory())
++                    self.pipeline.add(kateenc)
++                    queue = gst.element_factory_make('queue')
++                    queue.set_property('max-size-buffers', 1000)
++                    self.pipeline.add(queue)
++                    qpad = queue.get_compatible_pad(kateenc.get_pad('src'))
++                    gst.element_link_many(katesrc, kateenc)
++                    kateenc.get_pad('src').link(qpad)
++                    gst.element_link_many(queue, self.oggmux)
++
++                    # the serial number will have been randomized, so reset it as userdata so we can save again
++                    userdata = queue.get_pad('src').get_peer().get_name()
++                    userdata = int(string.replace(userdata, 'sink_', '', 1))
++                    #print 'userdata: %d -> %d' % (sub.getUserdata(), userdata)
++                    sub.setUserdata(userdata)
++
++                    #queue.set_state(gst.STATE_READY)
++                    #kateenc.set_state(gst.STATE_READY)
++                    #katesrc.set_state(gst.STATE_READY)
++                    #fakesink.set_state(gst.STATE_READY)
++                    fakesink.set_state(gst.STATE_PLAYING)
++
++                    self.oggmux_sources.append(queue)
++                    self.oggmux_sources.append(kateenc)
++                    self.oggmux_sources.append(katesrc)
++                    #self.oggmux_sources.append(fakesink)
++
++                else:
++                    # we don't have a replacement for this one, so let it through as is
++                    print 'Kate stream %s has no replacement, parsing and letting it through' % pad_name
++
++        if parse_only:
++            # TODO: buggy theoraparse it seems, it also fails with gst-launch
++            if mime == 'video/x-theora':
++                print 'cannot handle theora for now - it asserts in GStreamer'
++                m = gtk.MessageDialog(gtk.Window(gtk.WINDOW_TOPLEVEL),
++                                      gtk.DIALOG_DESTROY_WITH_PARENT,
++                                      gtk.MESSAGE_WARNING,
++                                      gtk.BUTTONS_CLOSE,
++                                      'The stream contains a Theora stream, and the GStreamer Theora parser is broken\n' +
++                                      'The Theora stream will be lost\n')
++                m.set_title('Warning')
++                m.connect("response", lambda dlg, resp: dlg.destroy())
++                m.show()
++
++                # intercept the stream by piping it to a fakesink
++                fakesink = gst.element_factory_make('fakesink')
++                self.pipeline.add(fakesink)
++                pad.link(fakesink.get_pad('sink'))
++                fakesink.set_state(gst.STATE_READY)
++                self.oggmux_sources.append(fakesink)
++                return
++
++            # let the stream flow to the ogg muxer: link pad to a new pad from oggmux
++            if mime not in self.parsefactories:
++                print 'cannot handle %s' %mime
++                return
++
++            queue = gst.element_factory_make('queue')
++            queue.set_property('max-size-buffers', 1000)
++            self.pipeline.add(queue)
++
++            parser = gst.element_factory_make(self.parsefactories[mime])
++            self.pipeline.add(parser)
++
++            qpad = queue.get_compatible_pad(pad)
++            pad.link(qpad)
++            queue.link(parser)
++            parser.link(self.oggmux)
++            self.parsers.append(parser)
++
++            parser.set_state(gst.STATE_PAUSED) #STATE_READY)
++            self.oggmux_sources.append(parser)
++            queue.set_state(gst.STATE_PAUSED) #STATE_READY)
++            self.oggmux_sources.append(queue)
++
++
++
++
++
++class Kate(Subtitles):
++    """
++        This class handles the Kate subtitle format
++    """
++
++    def __init__(self, filename):
++        """
++            constructor: loads subtitles from a file
++        """
++
++        print 'Kate subtitles module init for %s' % filename
++        Subtitles.__init__(self, filename)
++        self.encoding = "utf-8"
++        self.subType = "Kate"
++        self.subUserdata = None
++
++    def subSave(self, filename, subs):
++        kate = KateSaver(filename, subs)
++
++    def subInit(self, language, category, serial):
++        """
++            Sets up the subtitles object with information read from a Kate stream
++        """
++
++        print 'Kate subtitles module subInit for %s %s on %s' % (language, category, serial)
++        self.subLanguage = language
++        self.subCategory = category
++        self.subUserdata = serial
++
++    def setUserdata(self, userdata):
++        self.subUserdata = userdata
++
++    def getUserdata(self):
++        return self.subUserdata
++
++
++
++class KateSource(gst.BaseSrc):
++    """
++        A source element that send subtitles to the kateenc element.
++    """
++
++    __gsttemplates__ = (gst.PadTemplate("src", gst.PAD_SRC, gst.PAD_ALWAYS, gst.caps_from_string('text/x-pango-markup')))
++
++    def __init__(self, sub, name=None):
++        self.__gobject_init__()
++        if name != None:
++            self.set_name(name)
++        self.sub=sub
++        self.next_index=1
++
++    def do_create(self, offset, size):
++        #print 'do_create called with offset %d' % offset
++        sub = self.sub.getSubByIndex(self.next_index)
++        if sub != None:
++            text = sub.getSubText()
++            start_time = sub.getSubStartTime()
++            end_time = sub.getSubEndTime()
++            #print 'Found sub at %d till %d: %s' % (start_time, end_time, text)
++            self.next_index += 1
++            self.buffer = gst.Buffer(text)
++            self.buffer.timestamp = start_time * gst.MSECOND
++            self.buffer.duration = (end_time-start_time) * gst.MSECOND
++            self.buffer.set_caps(gst.caps_from_string('text/x-pango-markup'))
++            return gst.FLOW_OK, self.buffer
++        else:
++            print 'No sub found, EOS'
++            return gst.FLOW_UNEXPECTED, None
++
++gobject.type_register(KateSource)
++
++
++
++
++
++#==============================================================================
++class KateLoader(KateBase):
++    """
++        Class for loading Kate streams.
++    """
++
++    def __init__(self, filename):
++        """
++            constructor: loads Kate streams for the given file
++        """
++
++        KateBase.__init__(self, filename, False, True, None, self.__on_pad_added)
++
++        # update the subtitles keys once they're all loaded
++        for sub in self.subs:
++            self.subs[sub].updateKeys()
++
++
++#==============================================================================
++    def __on_pad_added(self, element, pad):
++        """
++            Creates Subtitles objects for any Kate stream encountered.
++            Other streams are ignored.
++        """
++
++        caps = pad.get_caps()
++        mime = caps[0].get_name()
++        if mime == 'application/x-kate':
++                pad_name = pad.get_name()
++                print 'Found Kate stream: %s' % pad_name
++                self.num_kate_streams += 1
++
++                # Check if the Kate plugin is available
++                if not self.check_kate_plugin(pad_name):
++                    return
++
++                # add a kate decoder and a fakesink
++                katedec = gst.element_factory_make('katedec')
++                self.pipeline.add(katedec)
++                fakesink = gst.element_factory_make('fakesink')
++                self.pipeline.add(fakesink)
++                gst.element_link_many(katedec, fakesink)
++
++                # create subtitles - initialized later as we don't know language/category yet
++                subs = Kate(self.filename)
++                self.subs[pad_name] = subs
++
++                __kpad0 = katedec.get_pad('sink')
++                pad.link(__kpad0)
++                event_pad = fakesink.get_pad('sink')
++                event_pad.set_chain_function(self.__on_kate_event)
++
++                self.padmap[event_pad] = pad_name
++                self.katedec[pad_name] = katedec
++
++                katedec.set_state(gst.STATE_PLAYING)
++                fakesink.set_state(gst.STATE_PLAYING)
++
++        if mime == 'video/x-theora':
++            print 'cannot handle theora for now - it asserts in GStreamer'
++            m = gtk.MessageDialog(gtk.Window(gtk.WINDOW_TOPLEVEL),
++                                  gtk.DIALOG_DESTROY_WITH_PARENT,
++                                  gtk.MESSAGE_WARNING,
++                                  gtk.BUTTONS_CLOSE,
++                                  'The stream contains a Theora stream, and the GStreamer Theora parser is broken\n' +
++                                  'If you save the stream, the Theora stream will be lost\n')
++            m.set_title('Warning')
++            m.connect("response", lambda dlg, resp: dlg.destroy())
++            m.show()
++
++
++#==============================================================================
++    def __on_kate_event(self, pad, buffer):
++        """
++            Fills subtitles as a stream is read
++        """
++
++        pad_name = self.padmap[pad]
++        #print 'Kate event: %s on pad %s' % ( buffer, pad_name)
++        subs = self.subs[pad_name]
++        start_time_ms = (buffer.timestamp+gst.MSECOND/2) / gst.MSECOND
++        end_time_ms = (buffer.timestamp+buffer.duration+gst.MSECOND/2) / gst.MSECOND
++
++        # initialize the subs list if this is the first event
++        if subs.getUserdata() == None:
++            serial = int(string.replace(pad_name, 'serial_', '', 1),16)
++            language = self.katedec[pad_name].get_property('language')
++            category = self.katedec[pad_name].get_property('category')
++            subs.subInit(language, category, serial)
++
++        subs.subAdd(start_time_ms, end_time_ms, UserString(buffer), None)
++
++        return gst.FLOW_OK
++
++
++
++#==============================================================================
++def discover(file):
++    """
++        Every subtitle should have a discover function
++        and return true if it should handle the requested
++        file.
++    """ 
++
++    kate = KateLoader(file)
++    subs = kate.get_subs()
++    if subs:
++        return subs
++
++
++__all__=["Kate", "KateLoader", "KateSaver"]
+
+=== modified file 'Subtle/Subtitles/Formats/Softni.py'
+--- Subtle/Subtitles/Formats/Softni.py	2008-08-09 01:28:27 +0000
++++ Subtle/Subtitles/Formats/Softni.py	2008-09-06 16:09:17 +0000
+@@ -52,6 +52,9 @@
          file.
      """
  

         return
 
 
--- /dev/null	2008-04-05 19:24:09.000000000 +0100
+++ subtle/Subtitles/Formats/Kate.py	2008-08-11 20:51:02.000000000 +0100
@@ -0,0 +1,619 @@
+#!/usr/bin/env python
+#
+#       Kate.py
+#       
+#       Copyright 2008 ogg.k.ogg.k <ogg.k.ogg.k@googlemail.com>
+#       
+#       This program is free software; you can redistribute it and/or modify
+#       it under the terms of the GNU General Public License as published by
+#       the Free Software Foundation; either version 3 of the License, or
+#       (at your option) any later version.
+#       
+#       This program is distributed in the hope that it will be useful,
+#       but WITHOUT ANY WARRANTY; without even the implied warranty of
+#       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#       GNU General Public License for more details.
+#       
+#       You should have received a copy of the GNU General Public License
+#       along with this program; if not, write to the Free Software
+#       Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+#       MA 02110-1301, USA.
+
+import pygtk
+pygtk.require('2.0')
+
+import gobject
+gobject.threads_init()
+
+import pygst
+pygst.require('0.10')
+import gst
+import gst.interfaces
+import gtk
+import os
+import time
+from UserString import UserString
+import string
+from .. Subtitles import Subtitles
+from Subtitles.Sub import *
+
+"""
+    Documentation for Kate module of Subtle project.
+
+    Information about the Kate codec may be found at:
+    http://wiki.xiph.org/index.php/OggKate
+
+    This module requires the GStreamer Kate plugin. At the time of this writing,
+    the Kate plugin is not included within GStreamer yet. A patch to gst-plugins-bad
+    is included in the libkate distribution (which is also needed), available at:
+    http://libkate.googlecode.com/
+
+    This module only handles text and timing, so any other information that may
+    be present in a Kate stream (such as graphics, motions, etc) will be lost when
+    saving it.
+
+    Any number of Kate streams (eg, one for each of a set of languages) may be
+    muxed in an Ogg container. This module supports loading and saving those, but
+    Subtle does not yet support switching between those for edition. Nevertheless,
+    all Kate streams will be saved back.
+"""
+
+
+class KateBase:
+    """
+        Base class for loading/saving Kate streams.
+    """
+
+    def __init__(self, filename, save=False, play=True, subs=None, on_pad_added=None):
+        """
+            constructor: opens filename, calling on_pad_added for each pad found
+        """
+
+        self.filename = filename
+        self.ready = False
+        self.error = False
+        if save:
+            self.subs = subs
+        else:
+            self.subs = {}
+        self.padmap = {}
+        self.katedec = {}
+        self.num_kate_streams = 0
+
+        self.pipeline = gst.Pipeline()
+
+        # make sure we get the messages we need
+        bus = self.pipeline.get_bus()
+        bus.add_signal_watch()
+        bus.connect('message::eos', self.__on_eos)
+        bus.connect('message::error', self.__on_error)
+        #bus.connect('message', self.__on_msg)
+        #bus.enable_sync_message_emission()
+        #bus.connect('sync-message', self.__on_sync_msg)
+
+        # add filesrc and oggdemux, which will dynamically branch into as many katedec as we need
+        self.filesrc = gst.element_factory_make('filesrc')
+        self.filesrc.set_property('location', filename)
+        self.pipeline.add(self.filesrc)
+        self.oggdemux = gst.element_factory_make('oggdemux')
+        self.pipeline.add(self.oggdemux)
+        gst.element_link_many(self.filesrc, self.oggdemux)
+
+        # if saving, we mux and save too
+        if save:
+            self.parsefactories = self.find_parsers()
+            self.parsers = []
+
+            self.oggmux_sources = []
+
+            self.oggmux = gst.element_factory_make('oggmux')
+            self.pipeline.add(self.oggmux)
+            self.filesink = gst.element_factory_make('filesink')
+            self.filesink.set_property('location', filename + '.subtle')
+            self.pipeline.add(self.filesink)
+            gst.element_link_many(self.oggmux, self.filesink)
+
+        # we will create katedec dynamically for each Kate stream encountered
+        self.oggdemux.connect('pad-added', on_pad_added)
+        self.oggdemux.connect('no-more-pads', self.__on_no_more_pads)
+
+        # start reading, and listen for pads being added until no more pads or timeout
+        self.pipeline.set_state(gst.STATE_PAUSED)
+        probe_time = 0
+        while not self.ready:
+                if probe_time > 2:
+                    print 'Failed to detect contents within 2 seconds'
+                    break
+                probe_time += 0.1
+                time.sleep(0.1)
+
+        # if saving, we just linked a pipeline with elements in the ready state, start them
+        if save and play:
+            self.oggmux_sources.append(self.oggmux)
+            self.oggmux_sources.append(self.filesink)
+            self.set_state(gst.STATE_PLAYING)
+
+        # if we have Kate streams, stream the entire file
+        if play and not self.error and self.get_num_kate_streams()>0:
+            self.play()
+
+        # we're done, stop the pipeline
+        self.close()
+
+        # swap the files or remove failed write
+        if save:
+            if not self.error:
+                os.unlink(filename)
+                os.rename(filename+'.subtle', filename)
+            else:
+                os.unlink(filename+'.subtle')
+
+#==============================================================================
+    def s2n(self,state):
+        if state==gst.STATE_NULL:
+           return 'NULL'
+        if state==gst.STATE_READY:
+           return 'READY'
+        if state==gst.STATE_PAUSED:
+           return 'PAUSED'
+        if state==gst.STATE_PLAYING:
+           return 'PLAYING'
+        return 'UNKNOWN'
+
+    def set_state(self,state):
+        for src in self.oggmux_sources:
+            #print 'setting state for %s: %s -> %s' % (src, self.s2n(src.get_state(gst.SECOND)), self.s2n(state))
+            src.set_state(state)
+
+#==============================================================================
+    def play(self):
+        """
+            set the pipeline to play, to process the entire stream
+        """
+
+        print 'Preparing to play'
+        bus = self.pipeline.get_bus()
+
+        ret = self.pipeline.set_state(gst.STATE_PLAYING)
+        if ret != gst.STATE_CHANGE_ASYNC and ret != gst.STATE_CHANGE_SUCCESS:
+            print 'Failed to set pipeline to playing'
+            return False
+        print 'Playing...'
+        while 1:
+            msg = bus.poll(gst.MESSAGE_EOS | gst.MESSAGE_ERROR, gst.SECOND)
+            if msg:
+                break
+        print 'Playing done'
+        return True
+
+
+#==============================================================================
+    def close(self):
+        self.pipeline.set_state(gst.STATE_NULL)
+
+
+#==============================================================================
+    def get_subs(self):
+        return self.subs
+
+
+#==============================================================================
+    def find_parsers(self):
+        """
+            finds all the parsers (from sample code from pygst), so we can
+            pipe non Kate streams
+        """
+
+        registry = gst.registry_get_default()
+        ret = {}
+        for f in registry.get_feature_list(gst.ElementFactory):
+            if f.get_klass().find('Parser') >= 0:
+                for t in f.get_static_pad_templates():
+                    if t.direction == gst.PAD_SINK:
+                        for s in t.get_caps():
+                            ret[s.get_name()] = f.get_name()
+                        break
+        return ret
+
+
+#==============================================================================
+    def check_kate_plugin(self, pad_name):
+        """
+            Checks whether the Kate GStreamer plugin is available, and bails
+            out with an error message if not
+        """
+        if not 'kate' in map(lambda p: p.get_name(), gst.registry_get_default().get_plugin_list()):
+            msg = 'The Kate GStreamer plugin was not found\ncannot use Kate stream %s\n'
+            print msg % pad_name
+            m = gtk.MessageDialog(gtk.Window(gtk.WINDOW_TOPLEVEL),
+                                  gtk.DIALOG_DESTROY_WITH_PARENT,
+                                  gtk.MESSAGE_ERROR,
+                                  gtk.BUTTONS_CLOSE,
+                                  msg % pad_name)
+            m.set_title('Kate plugin not found')
+            m.connect("response", lambda dlg, resp: dlg.destroy())
+            m.show()
+            return False
+        else:
+            return True
+
+
+#==============================================================================
+    def __on_no_more_pads(self, element):
+        print 'No more pads'
+        self.ready = True
+
+
+#==============================================================================
+    def get_num_kate_streams(self):
+        return self.num_kate_streams
+
+
+#==============================================================================
+    def __on_eos(self, bus, msg):
+        print 'EOS'
+        self.eos = True
+        self.ready = True
+
+
+#==============================================================================
+    def __on_error(self, bus, msg):
+        err, debug = msg.parse_error()
+        print 'Error: %s' %err, debug
+        m = gtk.MessageDialog(gtk.Window(gtk.WINDOW_TOPLEVEL),
+                              gtk.DIALOG_DESTROY_WITH_PARENT,
+                              gtk.MESSAGE_ERROR,
+                              gtk.BUTTONS_CLOSE,
+                              'Error: %s' %err)
+        m.set_title('Error')
+        m.connect("response", lambda dlg, resp: dlg.destroy())
+        m.show()
+
+        self.error = True
+        self.ready = True
+
+
+#==============================================================================
+    def __on_msg(self, bus, msg):
+        if msg.structure is None:
+            return
+        #name = msg.structure.get_name()
+        #print 'msg: %s' % name
+        #if msg.type == gst.MESSAGE_STATE_CHANGED:
+        #    old, new, pen = msg.parse_state_changed()
+        #    print '%r: state change: %r -> %r, %r pending' % (msg.src.get_path_string(), old, new, pen)
+
+
+#==============================================================================
+    def __on_sync_msg(self, bus, msg):
+        if msg.structure is None:
+            return
+        name = msg.structure.get_name()
+        #print 'sync: %s' % name
+
+
+
+
+#==============================================================================
+class KateSaver(KateBase):
+    """
+        Class for saving Kate streams.
+    """
+
+    def __init__(self, filename, subs):
+        """
+            constructor: saves the given subtitles as Kate streams to an Ogg file
+        """
+
+        KateBase.__init__(self, filename, True, True, subs, self.__on_pad_added)
+
+
+#==============================================================================
+    def __on_pad_added(self, element, pad):
+        """
+            Intercepts Kate streams we have previously loaded as subtitles and
+            replaces them with the subtitles.
+            Pipes all other streams (Kate and non Kate) through an appropriate parser, 
+        """
+
+        parse_only = True
+        caps = pad.get_caps()
+        mime = caps[0].get_name()
+        print 'New pad, caps %s' % mime
+        if mime == 'application/x-kate':
+            pad_name = pad.get_name()
+            print 'Found Kate stream: %s' % pad_name
+            self.num_kate_streams += 1
+
+            # Check if the Kate plugin is available
+            if self.check_kate_plugin(pad_name):
+                serial = int(string.replace(pad_name, 'serial_', '', 1),16)
+                # get next subtitles object
+                sub = None
+                for tmp in self.subs:
+                    #print "comparing serials %d and %d" % (tmp.getUserdata(), serial)
+                    if tmp.getUserdata() == serial:
+                        sub = tmp
+                        break
+
+                if sub != None:
+                    parse_only = False
+
+                    # intercept the stream by piping it to a fakesink
+                    fakesink = gst.element_factory_make('fakesink')
+                    fakesink.set_name('fakesink_'+pad_name)
+                    self.pipeline.add(fakesink)
+                    pad.link(fakesink.get_pad('sink'))
+
+                    # add a katesrc and kate encoder to replace it
+                    katesrc = KateSource(sub, 'katesrc_'+pad_name)
+                    self.pipeline.add(katesrc)
+                    kateenc = gst.element_factory_make('kateenc')
+                    kateenc.set_name('kateenc_'+pad_name)
+                    kateenc.set_property('language', sub.getLanguage())
+                    kateenc.set_property('category', sub.getCategory())
+                    self.pipeline.add(kateenc)
+                    queue = gst.element_factory_make('queue')
+                    queue.set_property('max-size-buffers', 1000)
+                    self.pipeline.add(queue)
+                    qpad = queue.get_compatible_pad(kateenc.get_pad('src'))
+                    gst.element_link_many(katesrc, kateenc)
+                    kateenc.get_pad('src').link(qpad)
+                    gst.element_link_many(queue, self.oggmux)
+
+                    # the serial number will have been randomized, so reset it as userdata so we can save again
+                    userdata = queue.get_pad('src').get_peer().get_name()
+                    userdata = int(string.replace(userdata, 'sink_', '', 1))
+                    #print 'userdata: %d -> %d' % (sub.getUserdata(), userdata)
+                    sub.setUserdata(userdata)
+
+                    #queue.set_state(gst.STATE_READY)
+                    #kateenc.set_state(gst.STATE_READY)
+                    #katesrc.set_state(gst.STATE_READY)
+                    #fakesink.set_state(gst.STATE_READY)
+                    fakesink.set_state(gst.STATE_PLAYING)
+
+                    self.oggmux_sources.append(queue)
+                    self.oggmux_sources.append(kateenc)
+                    self.oggmux_sources.append(katesrc)
+                    #self.oggmux_sources.append(fakesink)
+
+                else:
+                    # we don't have a replacement for this one, so let it through as is
+                    print 'Kate stream %s has no replacement, parsing and letting it through' % pad_name
+
+        if parse_only:
+            # TODO: buggy theoraparse it seems, it also fails with gst-launch
+            if mime == 'video/x-theora':
+                print 'cannot handle theora for now - it asserts in GStreamer'
+                m = gtk.MessageDialog(gtk.Window(gtk.WINDOW_TOPLEVEL),
+                                      gtk.DIALOG_DESTROY_WITH_PARENT,
+                                      gtk.MESSAGE_WARNING,
+                                      gtk.BUTTONS_CLOSE,
+                                      'The stream contains a Theora stream, and the GStreamer Theora parser is broken\n' +
+                                      'The Theora stream will be lost\n')
+                m.set_title('Warning')
+                m.connect("response", lambda dlg, resp: dlg.destroy())
+                m.show()
+
+                # intercept the stream by piping it to a fakesink
+                fakesink = gst.element_factory_make('fakesink')
+                self.pipeline.add(fakesink)
+                pad.link(fakesink.get_pad('sink'))
+                fakesink.set_state(gst.STATE_READY)
+                self.oggmux_sources.append(fakesink)
+                return
+
+            # let the stream flow to the ogg muxer: link pad to a new pad from oggmux
+            if mime not in self.parsefactories:
+                print 'cannot handle %s' %mime
+                return
+
+            queue = gst.element_factory_make('queue')
+            queue.set_property('max-size-buffers', 1000)
+            self.pipeline.add(queue)
+
+            parser = gst.element_factory_make(self.parsefactories[mime])
+            self.pipeline.add(parser)
+
+            qpad = queue.get_compatible_pad(pad)
+            pad.link(qpad)
+            queue.link(parser)
+            parser.link(self.oggmux)
+            self.parsers.append(parser)
+
+            parser.set_state(gst.STATE_READY)
+            queue.set_state(gst.STATE_READY)
+            self.oggmux_sources.append(parser)
+            self.oggmux_sources.append(queue)
+
+
+
+
+
+class Kate(Subtitles):
+    """
+        This class handles the Kate subtitle format
+    """
+
+    def __init__(self, filename):
+        """
+            constructor: loads subtitles from a file
+        """
+
+        print 'Kate subtitles module init for %s' % filename
+        Subtitles.__init__(self, filename)
+        self.encoding = "FIXME: I have no idea what this is for ? character encoding ?"
+        self.subType = "Kate"
+        self.subUserdata = None
+
+    def subSave(self, filename, subs):
+        kate = KateSaver(filename, subs)
+
+    def subInit(self, language, category, serial):
+        """
+            Sets up the subtitles object with information read from a Kate stream
+        """
+
+        print 'Kate subtitles module subInit for %s %s on %s' % (language, category, serial)
+        self.subLanguage = language
+        self.subCategory = category
+        self.subUserdata = serial
+
+    def setUserdata(self, userdata):
+        self.subUserdata = userdata
+
+    def getUserdata(self):
+        return self.subUserdata
+
+
+
+class KateSource(gst.BaseSrc):
+    """
+        A source element that send subtitles to the kateenc element.
+    """
+
+    __gsttemplates__ = (gst.PadTemplate("src", gst.PAD_SRC, gst.PAD_ALWAYS, gst.caps_from_string('text/x-pango-markup')))
+
+    def __init__(self, sub, name=None):
+        self.__gobject_init__()
+        if name != None:
+            self.set_name(name)
+        self.sub=sub
+        self.next_index=1
+
+    def do_create(self, offset, size):
+        #print 'do_create called with offset %d' % offset
+        sub = self.sub.getSubByIndex(self.next_index)
+        if sub != None:
+            text = sub.getSubText()
+            start_time = sub.getSubStartTime()
+            end_time = sub.getSubEndTime()
+            #print 'Found sub at %d till %d: %s' % (start_time, end_time, text)
+            self.next_index += 1
+            self.buffer = gst.Buffer(text)
+            self.buffer.timestamp = start_time * gst.MSECOND
+            self.buffer.duration = (end_time-start_time) * gst.MSECOND
+            self.buffer.set_caps(gst.caps_from_string('text/x-pango-markup'))
+            return gst.FLOW_OK, self.buffer
+        else:
+            print 'No sub found, EOS'
+            return gst.FLOW_UNEXPECTED, None
+
+gobject.type_register(KateSource)
+
+
+
+
+
+#==============================================================================
+class KateLoader(KateBase):
+    """
+        Class for loading Kate streams.
+    """
+
+    def __init__(self, filename):
+        """
+            constructor: loads Kate streams for the given file
+        """
+
+        KateBase.__init__(self, filename, False, True, None, self.__on_pad_added)
+
+        # update the subtitles keys once they're all loaded
+        for sub in self.subs:
+            self.subs[sub].updateKeys()
+
+
+#==============================================================================
+    def __on_pad_added(self, element, pad):
+        """
+            Creates Subtitles objects for any Kate stream encountered.
+            Other streams are ignored.
+        """
+
+        caps = pad.get_caps()
+        mime = caps[0].get_name()
+        if mime == 'application/x-kate':
+                pad_name = pad.get_name()
+                print 'Found Kate stream: %s' % pad_name
+                self.num_kate_streams += 1
+
+                # Check if the Kate plugin is available
+                if not self.check_kate_plugin(pad_name):
+                    return
+
+                # add a kate decoder and a fakesink
+                katedec = gst.element_factory_make('katedec')
+                self.pipeline.add(katedec)
+                fakesink = gst.element_factory_make('fakesink')
+                self.pipeline.add(fakesink)
+                gst.element_link_many(katedec, fakesink)
+
+                # create subtitles - initialized later as we don't know language/category yet
+                subs = Kate(self.filename)
+                self.subs[pad_name] = subs
+
+                __kpad0 = katedec.get_pad('sink')
+                pad.link(__kpad0)
+                event_pad = fakesink.get_pad('sink')
+                event_pad.set_chain_function(self.__on_kate_event)
+
+                self.padmap[event_pad] = pad_name
+                self.katedec[pad_name] = katedec
+
+                katedec.set_state(gst.STATE_PLAYING)
+                fakesink.set_state(gst.STATE_PLAYING)
+
+        if mime == 'video/x-theora':
+            print 'cannot handle theora for now - it asserts in GStreamer'
+            m = gtk.MessageDialog(gtk.Window(gtk.WINDOW_TOPLEVEL),
+                                  gtk.DIALOG_DESTROY_WITH_PARENT,
+                                  gtk.MESSAGE_WARNING,
+                                  gtk.BUTTONS_CLOSE,
+                                  'The stream contains a Theora stream, and the GStreamer Theora parser is broken\n' +
+                                  'If you save the stream, the Theora stream will be lost\n')
+            m.set_title('Warning')
+            m.connect("response", lambda dlg, resp: dlg.destroy())
+            m.show()
+
+
+#==============================================================================
+    def __on_kate_event(self, pad, buffer):
+        """
+            Fills subtitles as a stream is read
+        """
+
+        pad_name = self.padmap[pad]
+        #print 'Kate event: %s on pad %s' % ( buffer, pad_name)
+        subs = self.subs[pad_name]
+        start_time_ms = (buffer.timestamp+gst.MSECOND/2) / gst.MSECOND
+        end_time_ms = (buffer.timestamp+buffer.duration+gst.MSECOND/2) / gst.MSECOND
+
+        # initialize the subs list if this is the first event
+        if subs.getUserdata() == None:
+            serial = int(string.replace(pad_name, 'serial_', '', 1),16)
+            language = self.katedec[pad_name].get_property('language')
+            category = self.katedec[pad_name].get_property('category')
+            subs.subInit(language, category, serial)
+
+        subs.subAdd(start_time_ms, end_time_ms, UserString(buffer), None)
+
+        return gst.FLOW_OK
+
+
+
+#==============================================================================
+def discover(file):
+    """
+        Every subtitle should have a discover function
+        and return true if it should handle the requested
+        file.
+    """ 
+
+    kate = KateLoader(file)
+    subs = kate.get_subs()
+    if subs:
+        return subs
+
+
+__all__=["Kate", "KateLoader", "KateSaver"]

     m = magic.open(magic.MAGIC_COMPRESS | magic.MAGIC_MIME)
     status = m.load()
     
@@ -63,7 +66,7 @@
     regex = re.compile(rawstr,re.MULTILINE| re.VERBOSE)
     
     if regex.search(data):
-        return SubRip
+        return SubRip(file)
     return
 
 class SubRip(Subtitles.Subtitles):
@@ -93,8 +96,8 @@
     ## Save subtitles.
     # Save subtitles to the file.
     # \param FN - file name.
