conduit r1660 - in trunk: . conduit/datatypes conduit/gtkui conduit/modules conduit/modules/iPodModule conduit/utils data test/python-tests test/python-tests/data



Author: jstowers
Date: Wed Aug 20 23:17:13 2008
New Revision: 1660
URL: http://svn.gnome.org/viewvc/conduit?rev=1660&view=rev

Log:
2008-08-21  Alexandre Rosenfeld  <airmind gmail com>

	* conduit/*:
	Merge GSOC08 improved iPod support branch. This adds support for
	writing iPod music,video, and transcoding using GStreamer.
	Fixes #526423 and Fixes #510279.



Added:
   trunk/conduit/utils/GstMetadata.py
   trunk/conduit/utils/MediaFile.py
Modified:
   trunk/   (props changed)
   trunk/ChangeLog
   trunk/NEWS
   trunk/conduit/datatypes/Audio.py
   trunk/conduit/datatypes/Video.py
   trunk/conduit/gtkui/SimpleConfigurator.py
   trunk/conduit/gtkui/UI.py
   trunk/conduit/modules/AudioVideoConverterModule.py
   trunk/conduit/modules/TestModule.py
   trunk/conduit/modules/iPodModule/__init__.py
   trunk/conduit/modules/iPodModule/config.glade
   trunk/conduit/modules/iPodModule/iPodModule.py
   trunk/data/conduit.glade
   trunk/test/python-tests/TestCoreConvertAudioVideo.py
   trunk/test/python-tests/data/audio.list
   trunk/test/python-tests/data/video.list

Modified: trunk/NEWS
==============================================================================
--- trunk/NEWS	(original)
+++ trunk/NEWS	Wed Aug 20 23:17:13 2008
@@ -1,5 +1,8 @@
 NEW in 0.3.14:
 ==============
+* Merge improved iPod support by Alexandre Rosenfeld. This was implemented as
+  part of GSOC08 and features
+  * Improved audio/video transcoding using GStreamer
 
 NEW in 0.3.13.1:
 ==============

Modified: trunk/conduit/datatypes/Audio.py
==============================================================================
--- trunk/conduit/datatypes/Audio.py	(original)
+++ trunk/conduit/datatypes/Audio.py	Wed Aug 20 23:17:13 2008
@@ -1,11 +1,16 @@
 import conduit
 import conduit.datatypes.File as File
+import conduit.utils.MediaFile as MediaFile
+import logging
+log = logging.getLogger("datatypes.Audio")
+
+from threading import Lock
 
 PRESET_ENCODINGS = {
-    "ogg":{"acodec":"vorbis","format":"ogg","file_extension":"ogg"},
-    "wav":{"acodec":"pcm_mulaw","format":"wav","file_extension":"wav"}
+    "ogg":{"acodec":"vorbisenc","format":"oggmux","file_extension":"ogg"},
+    "wav":{"acodec":"wavenc","file_extension":"wav"}
     }
-    
+
 def mimetype_is_audio(mimetype):
     """
     @returns: True if the given mimetype string represents an audio file
@@ -17,18 +22,73 @@
     else:
         return False
 
-class Audio(File.File):
+class Audio(MediaFile.MediaFile):
 
     _name_ = "file/audio"
 
     def __init__(self, URI, **kwargs):
-        File.File.__init__(self, URI, **kwargs)
+        MediaFile.MediaFile.__init__(self, URI, **kwargs)
+
+    def get_audio_title(self):
+        '''
+        Song title (string)
+        '''
+        return self._get_metadata('title')
 
     def get_audio_artist(self):
-        return None
+        '''
+        Song artist (string)
+        '''
+        return self._get_metadata('artist')
 
     def get_audio_album(self):
-        return None
+        '''
+        Song album (string)
+        '''
+        return self._get_metadata('album')
+
+    def get_audio_track(self):
+        return self._get_metadata('track-number')
+
+    def get_audio_tracks(self):
+
+        return self._get_metadata('track-count')
+
+    def get_audio_bitrate(self):
+        '''
+        Bitrate of the audio stream (int)
+        '''
+        return self._get_metadata('bitrate')
+
+    def get_audio_composer(self):
+        '''
+        Song composer
+        '''
+        return self._get_metadata('composer')
 
     def get_audio_duration(self):
-        return None
+        '''
+        Duration in miliseconds (int)
+        '''
+        return self._get_metadata('duration')
+
+    def get_audio_samplerate(self):
+        '''
+        Sample rate of the audio stream (int)
+        '''
+        return self._get_metadata('samplerate')
+
+    def get_audio_channels(self):
+        '''
+        Number of channels in the audio stream (int)
+        '''
+        return self._get_metadata('channels')
+
+    def get_audio_rating(self):
+        '''
+        Audio rating from 0.0 to 5.0
+        '''
+        return self._get_metadata('rating')
+
+    def get_audio_cover_location(self):
+        return self._get_metadata('cover_location')

Modified: trunk/conduit/datatypes/Video.py
==============================================================================
--- trunk/conduit/datatypes/Video.py	(original)
+++ trunk/conduit/datatypes/Video.py	Wed Aug 20 23:17:13 2008
@@ -1,15 +1,20 @@
 import conduit
 import conduit.datatypes.File as File
+import conduit.utils.MediaFile as MediaFile
 
 #The preset encodings must be robust. That means, in the case of ffmpeg,
 #you must be explicit with the options, otherwise it tries to retain sample
 #rates between the input and output files, leading to invalid rates in the output
+# "arate":44100, "abitrate":"64k"
+# "fps":15
 PRESET_ENCODINGS = {
-    "divx":{"vcodec":"mpeg4","acodec":"ac3","arate":44100,"abitrate":"64k","format":"avi","vtag":"DIVX","file_extension":"avi", "fps":15},
+    "divx":{"vcodec":"xvidenc", "acodec":"lame", "format":"avimux", "vtag":"DIVX", "file_extension":"avi", },
     #breaks on single channel audio files because ffmpeg vorbis encoder only suuport stereo
-    "ogg":{"vcodec":"theora","acodec":"vorbis","format":"ogg","file_extension":"ogg"},
+    "ogg":{"vcodec":"theoraenc", "acodec":"vorbisenc", "format":"oggmux", "file_extension":"ogg"},
     #needs mencoder or ffmpeg compiled with mp3 support
-    "flv":{"arate":22050,"abitrate":32,"format":"flv","acodec":"mp3","mencoder":True,"file_extension":"flv"}   
+    #requires gst-ffmpeg and gst-plugins-ugly
+    "flv":{"vcodec":"ffenc_flv", "acodec":"lame", "format":"ffmux_flv", "file_extension":"flv"}
+    #"arate":22050,"abitrate":32,
     }
 
 def mimetype_is_video(mimetype):
@@ -23,37 +28,15 @@
     else:
         return False
 
-class Video(File.File):
+class Video(MediaFile.MediaFile):
 
     _name_ = "file/video"
 
     def __init__(self, URI, **kwargs):
-        File.File.__init__(self, URI, **kwargs)
-        self._title = None
-        self._description = None
+        MediaFile.MediaFile.__init__(self, URI, **kwargs)
 
     def get_video_duration(self):
-        return None
+        return _get_metadata('duration')
 
     def get_video_size(self):
-        return None,None
-
-    def get_description(self):
-        """
-        @returns: the video's description
-        """
-        return self._description
-
-    def set_description(self, description):
-        self._description = description
-        
-    def __getstate__(self):
-        data = File.File.__getstate__(self)
-        data["description"] = self._description
-        return data
-
-    def __setstate__(self, data):
-        self.pb = None
-        self._description = data["description"]
-        File.File.__setstate__(self, data)
-
+        return _get_metadata('width'),_get_metadata('height')

Modified: trunk/conduit/gtkui/SimpleConfigurator.py
==============================================================================
--- trunk/conduit/gtkui/SimpleConfigurator.py	(original)
+++ trunk/conduit/gtkui/SimpleConfigurator.py	Wed Aug 20 23:17:13 2008
@@ -17,9 +17,20 @@
                     "Name" : "Setting Name",
                     "Widget" : gtk.TextView,
                     "Callback" : function,
-                    "InitialValue" : value
+                    "InitialValue" : value or function
                     }
                 ]
+    Or, alternatively:
+        
+
+        maps = [
+                    {
+                    "Name" : "Setting Name",
+                    "Kind" : "text", "check" or "list",
+                    "Callback" : function,
+                    "InitialValue" : value or function
+                    }
+                ]        
     """
     
     CONFIG_WINDOW_TITLE_TEXT = "Configure "
@@ -38,7 +49,8 @@
         self.widgetInstances = []
         self.dialogParent = window
         #the child widget to contain the custom settings
-        self.customSettings = gtk.VBox(False, 5)
+        self.customSettings = gtk.Table(rows=0, columns=2)
+        self.customSettings.set_row_spacings(8)
         
         #The dialog is loaded from a glade file
         gladeFile = os.path.join(conduit.SHARED_DATA_DIR, "conduit.glade")
@@ -71,8 +83,11 @@
                 w["Callback"](w["Widget"].get_text())
             elif isinstance(w["Widget"], gtk.CheckButton):
                 w["Callback"](w["Widget"].get_active())
+            elif w["Kind"] == "list":
+                w["Callback"](w["Widget"].get_active(), w["Widget"].get_active_text())
             else:
-                log.warn("Dont know how to retrieve value from a %s" % w["Widget"])
+                # Just return the widget, so the caller should know what to do with this
+                w["Callback"](w["Widget"])
 
         self.dialog.destroy()
         
@@ -108,27 +123,56 @@
         """
         #For each item in the mappings list create the appropriate widget
         for l in self.mappings:
-            #New instance of the widget
-            widget = l["Widget"]()
-            #all get packed into an HBox
-            hbox = gtk.HBox(False, 5)
+            if 'Kind' in l:
+                kind = l['Kind']
+                widget = {'text': gtk.Entry,
+                          'list': gtk.combo_box_new_text,
+                          'check': gtk.CheckButton}[kind]()
+            elif 'Widget' in l:
+                kind = None
+                #New instance of the widget
+                widget = l["Widget"]()
+            else:
+                raise Exception("Configuration Kind or Widget not specified")
+                
+            label_text = l["Name"]
+            if label_text[len(label_text) - 1] != ':':
+                label_text += ':'
+            #gtkEntry has its label beside it
+            label = gtk.Label(label_text)
+            label.set_alignment(0.0, 0.5)
 
+            if callable(l["InitialValue"]):
+                l["InitialValue"](widget)            
+            elif kind == 'list':
+                index = 0
+                for name in l["Values"]:
+                    widget.append_text(name)
+                    if name == l["InitialValue"]:
+                        widget.set_active(index)
+                    index += 1                
             #FIXME: I am ashamed about this ugly hackery and dupe code....
-            if isinstance(widget, gtk.Entry):
-                #gtkEntry has its label beside it
-                label = gtk.Label(l["Name"])
-                hbox.pack_start(label)
+            elif isinstance(widget, gtk.Entry):
                 widget.set_text(str(l["InitialValue"]))
             elif isinstance(widget, gtk.CheckButton):
                 #gtk.CheckButton has its label built in
-                widget = l["Widget"](l["Name"])
-                widget.set_active(bool(l["InitialValue"]))                        
-                #FIXME: There must be a better way to do this but we need some way 
-                #to identify the widget *instance* when we save the values from it
+                label = None
+                widget.set_label(l["Name"])
+                widget.set_active(bool(l["InitialValue"])) 
+                                       
+            #FIXME: There must be a better way to do this but we need some way 
+            #to identify the widget *instance* when we save the values from it            
             self.widgetInstances.append({
                                         "Widget" : widget,
+                                        "Kind" : kind,
                                         "Callback" : l["Callback"]
                                         })
-            #pack them all together
-            hbox.pack_start(widget)
-            self.customSettings.pack_start(hbox)
+                                        
+            table = self.customSettings
+            row = table.get_property('n-rows') + 1
+            table.resize(row, 2)
+            if label:
+                table.attach(label, 0, 1, row - 1, row)
+                table.attach(widget, 1, 2, row - 1, row)
+            else:
+                table.attach(widget, 0, 2, row - 1, row)

Modified: trunk/conduit/gtkui/UI.py
==============================================================================
--- trunk/conduit/gtkui/UI.py	(original)
+++ trunk/conduit/gtkui/UI.py	Wed Aug 20 23:17:13 2008
@@ -593,7 +593,8 @@
         		"John Stowers",
         		"John Carr",
         		"Thomas Van Machelen",
-        		"Jonny Lamb"])
+        		"Jonny Lamb",
+                "Alexandre Rosenfeld"])
         self.set_artists([
         		"John Stowers",
         		"mejogid",

Modified: trunk/conduit/modules/AudioVideoConverterModule.py
==============================================================================
--- trunk/conduit/modules/AudioVideoConverterModule.py	(original)
+++ trunk/conduit/modules/AudioVideoConverterModule.py	Wed Aug 20 23:17:13 2008
@@ -1,142 +1,311 @@
 import re
 import logging
+import threading
 log = logging.getLogger("modules.AVConverter")
 
 import conduit
 import conduit.utils as Utils
-import conduit.utils.CommandLineConverter as CommandLineConverter
 import conduit.TypeConverter as TypeConverter
 import conduit.datatypes.File as File
 import conduit.datatypes.Audio as Audio
 import conduit.datatypes.Video as Video
 
-if Utils.program_installed("ffmpeg"):
-    MODULES = {
-        "AudioVideoConverter" :  { "type": "converter" }
+import gobject
+import pygst
+pygst.require('0.10')
+import gst
+from gst.extend import discoverer
+
+MODULES = {
+    "AudioVideoConverter" :  { "type": "converter" }
+}
+
+'''
+GStreamer Conversion properties
+
+The parameteres to a GStreamer conversion usually require the name of an 
+GStreamer element.
+All of the availiable elements in a GStreamer installation can be found with
+the "gst-inspect" command, usually found in the gstreamer-tools package.
+If an element is missing, it probably requires the bad or ugly packages from
+GStreamer.
+These elements will be used with gst.parse_launch, which can take properties 
+with them, such as "faac bitrate=320000". Only single elements are supported
+now. You can find the syntax in the gst-launch manual ("man gst-launch").
+All the properties of each element can be found with the command
+"gst-inspect <element-name>".
+
+These are the properties the GStreamer converter can take:
+ - mux (string, optional): Name of the file muxer used to group the audio 
+    and video data, if required. Example: avimux or any of the ffmux elements
+ - vcodec (string, optional): Name of the video data encoder. If not 
+    specified, no video will be encoded.
+    Examples: x264enc, theoraenc
+ - acodec (string, optional): Name of the audio data encoder. If not 
+    specified, audio won't be availiable.
+    Examples: faac, vorbisenc
+ - width and height (int, optional): Required video dimensions. If only one is
+    specified, the other is calculated to keep the video proportional.    
+'''
+
+class GStreamerConversionPipeline(gst.Pipeline):
+    """
+    Converts between different multimedia formats.
+    This class is event-based and needs a mainloop to work properly.
+    Emits the 'converted' signal when conversion is finished.
+    Heavily based from gst.extend.discoverer
+
+    The 'converted' callback has one boolean argument, which is True if the
+    file was successfully converted.
+    """
+    
+    #TODO: Although this is based more on Discoverer, it might be better to base
+    # on some ideas from utils.GstMetadata, especially the pipeline reutilization
+    # (might be a temporary fix to the need to run the discover to get media 
+    # information then the conversion pipeline)
+
+    __gsignals__ = {
+        'converted' : (gobject.SIGNAL_RUN_FIRST,
+                       None,
+                       (gobject.TYPE_BOOLEAN, ))
         }
-else:
-    MODULES = {}
-
-class FFmpegCommandLineConverter(CommandLineConverter.CommandLineConverter):
-    def __init__(self, duration=None):
-        CommandLineConverter.CommandLineConverter.__init__(self)
-        self.duration = duration
-        self.percentage_match = re.compile('time=?(\d+\.\d+)')
-
-    def build_command(self, **kwargs):
-        kwargs['in_file'] = '"%s"'
-        kwargs['out_file'] = '"%s"'
-
-        command = "ffmpeg -i %(in_file)s "
-        #video options
-        if kwargs.get('vcodec', None):      command += "-vcodec %(vcodec)s "
-        if kwargs.get('vbitrate', None):    command += "-b %(vbitrate)s "
-        if kwargs.get('fps', None):         command += "-r %(fps)s " 
-        if kwargs.get('vtag', None):        command += "-vtag %(vtag)s "
-        if kwargs.get('width', None) and kwargs.get('height', None):
-            command += "-s %(width)sx%(height)s "
-        #audio options
-        if kwargs.get('acodec', None):      command += "-acodec %(acodec)s "
-        if kwargs.get('arate', None):       command += "-ar %(arate)s "
-#        if kwargs.get('abitrate', None):    command += "-ab %(abitrate)s "
-        if kwargs.get('achannels', None):   command += "-ac %(achannels)s "
-        #output file, overwrite and container format
-        if kwargs.get('format', None):      command += "-f %(format)s "
-        command += "-y %(out_file)s"
-
-        self.command = command % kwargs
-
-    def calculate_percentage(self, val):
-        return float(val)/self.duration*100.0
-
-    def check_cancelled(self):
-        return conduit.GLOBALS.cancelled
-
-class MencoderCommandLineConverter(CommandLineConverter.CommandLineConverter):
-    def __init__(self):
-        CommandLineConverter.CommandLineConverter.__init__(self)
-        self.percentage_match = re.compile('(\d+)%')
-
-    def build_command(self, **kwargs):
-        kwargs['in_file'] = '"%s"'
-        kwargs['out_file'] = '"%s"'
-
-        command = "mencoder %(in_file)s -o %(out_file)s "
-        #audio options
-        if kwargs.get('arate', None):       command += "-srate %(arate)s "
-        if kwargs.get('achannels', None):   command += "-channels %(achannels) "
-        #only support lavc atm
-        command += "-oac lavc "
-        if kwargs.has_key('acodec') and kwargs.has_key('abitrate'):
-            command += "-lavcopts acodec=%(acodec)s:abitrate=%(abitrate)s "
-        if kwargs.get('achannels', None):
-            command += "-af volnorm,channels=%(achannels) "
-        else:
-            command += "-af volnorm "
-        #video options (only support lavc atm)
-        command += "-ovc lavc "
-        if kwargs.has_key('vcodec') and kwargs.has_key('vbitrate'):
-            command += "-ovc lavc -lavcopts vcodec=%(vcodec)s:vbitrate=%(vbitrate)s "
-        if kwargs.get('width', None) and kwargs.get('height', None):
-            command += "-vf-add scale=%(width)s:%(height)s "
-        if kwargs.get('fps', None):         command += "-ofps %(fps)s "
-        if kwargs.get('vtag', None):        command += "-ffourcc %(vtag)s "
-
-        self.command = command % kwargs
-
-    def calculate_percentage(self, val):
-        return float(val)
-
-    def check_cancelled(self):
-        return conduit.GLOBALS.cancelled
+        
+    
+    def __init__(self, **kwargs):        
+        gst.Pipeline.__init__(self)
+        #if 'file_mux' not in kwargs:
+        #    raise Exception('Output file format not specified')        
+        self._has_video_enc = ('vcodec' in kwargs) or ('vcodec_pass1' in kwargs) or ('vcodec_pass2' in kwargs)
+        self._has_audio_enc = 'acodec' in kwargs
+        if not self._has_video_enc and not self._has_audio_enc:
+            raise Exception('At least one output must be specified')
             
-class AudioVideoConverter(TypeConverter.Converter):
+        self._pass = kwargs.get('pass', 0)
+        self._filesrc = gst.element_factory_make('filesrc')        
+        self._filesrc.set_property('location', kwargs['in_file'])
+        self._decodebin = gst.element_factory_make('decodebin')
+        self._decodebin.connect('new-decoded-pad', self._dbin_decoded_pad)
+        self._filesink = gst.element_factory_make('filesink')
+        self._filesink.set_property('location', kwargs['out_file'])
+        
+        self.add(self._filesrc, self._decodebin, self._filesink)
+        self._filesrc.link(self._decodebin)
+        
+        if self._pass == 1:
+            self._fileout = gst.element_factory_make('fakesink')
+            self.add(self._fileout)
+        elif 'format' in kwargs:
+            #TODO: File muxer could probably be found by inspection (from a mime
+            # string, for instance)
+            self._filemuxer = gst.parse_launch(kwargs['format'])
+            self.add(self._filemuxer)
+            self._filemuxer.link(self._filesink)
+            self._fileout = self._filemuxer        
+        else:
+            self._fileout = self._filesink
+        #TODO: Create video and audio encoders on demand
+        if self._has_video_enc:
+            self._video_queue = gst.element_factory_make('queue')
+            self._video_scale = gst.element_factory_make('videoscale')
+            self._video_ffmpegcolorspace = gst.element_factory_make('ffmpegcolorspace')
+            if self._pass == 1 and 'vcodec_pass1' in kwargs:
+                self._video_enc = gst.parse_launch(kwargs['vcodec_pass1'])
+            elif self._pass == 2 and 'vcodec_pass2' in kwargs:
+                self._video_enc = gst.parse_launch(kwargs['vcodec_pass2'])
+            else:
+                if self._pass != 0:
+                    log.debug("Creating generic video encoder for pass != 0")
+                self._video_enc = gst.parse_launch(kwargs['vcodec'])            
+            self.add(self._video_queue, self._video_scale, self._video_ffmpegcolorspace, self._video_enc)
+            # Dont link videoscale to ffmpegcolorspace yet
+            self._video_queue.link(self._video_scale)
+            self._video_ffmpegcolorspace.link(self._video_enc)
+            #TODO: Add dynamic video scaling, thus removing the need to run
+            # the discoverer before conversion
+            if ('width' in kwargs) or ('height' in kwargs):
+                log.debug("Video dimensions specified")
+                resolution = []                
+                if 'width' in kwargs:
+                    width = kwargs['width']
+                    resolution.append('width=%s' % (width - width % 2))
+                if 'height' in kwargs:
+                    height = kwargs['height']
+                    resolution.append('height=%s' % (height - height % 2))
+                caps = gst.caps_from_string('video/x-raw-yuv,%s;video/x-raw-yuv,%s' % (','.join(resolution), ','.join(resolution)))
+                self._video_scale.link_filtered(self._video_ffmpegcolorspace, caps)
+            else:
+                self._video_scale.link(self._video_ffmpegcolorspace)
+            # Pad linked to decodebin in decoded_pad
+            self._video_pad = self._video_queue.get_pad('sink')
+            # Final element linked to file muxer in decoded_pad
+            self._video_src = self._video_enc
+        else:
+            self._video_pad = None
+        if self._has_audio_enc and self._pass != 1:
+            self._audio_queue = gst.element_factory_make('queue')
+            #TODO: Add audio rate and sampler            
+            self._audio_convert = gst.element_factory_make('audioconvert')
+            self._audio_resample = gst.element_factory_make('audioresample')
+            self._audio_rate = gst.element_factory_make('audiorate')            
+            self._audio_enc = gst.parse_launch(kwargs['acodec'])
+            self.add(self._audio_queue, self._audio_convert, self._audio_resample, self._audio_rate, self._audio_enc)
+            gst.element_link_many(self._audio_queue, self._audio_convert, self._audio_resample, self._audio_rate, self._audio_enc)
+            # Pad linked to decodebin
+            self._audio_pad = self._audio_queue.get_pad('sink')
+            # Final element linked to file muxer in decoded_pad
+            self._audio_src = self._audio_enc
+        else:
+            self._audio_pad = None
+            
+    def _finished(self, success=False):        
+        log.debug("Conversion finished")
+        self._success = success
+        self.bus.remove_signal_watch()
+        gobject.idle_add(self._stop)
+        gobject.source_remove(self.watch)
+        return False
+
+    def _stop(self):
+        log.debug('Conversion stop')
+        self.set_state(gst.STATE_READY)
+        self.emit('converted', self._success)
+
+    def _bus_message_cb(self, bus, message):
+        if message.type == gst.MESSAGE_EOS:
+            #TODO: Any other possibility for end-of-stream other then successfull
+            # conversion?
+            log.debug("Conversion sucessfull")
+            self._finished(True)
+        #elif message.type == gst.MESSAGE_TAG:
+        #    for key in message.parse_tag().keys():
+        #        self.tags[key] = message.structure[key]
+        elif message.type == gst.MESSAGE_ERROR:
+            log.debug("Conversion error")
+            self._finished()        
+            
+    def _dbin_decoded_pad(self, dbin, pad, is_last):
+        caps = pad.get_caps()
+        log.debug("Caps found: %s" % caps.to_string())        
+        if caps.to_string().startswith('video'):
+            if self._video_pad and not self._video_pad.is_linked():
+                log.debug("Linking video encoder: %s" % (self._video_pad))
+                pad.link(self._video_pad)
+                self._video_src.link(self._fileout)
+            elif self._video_pad:
+                log.debug("Video encoder already linked, probably multiple video streams")
+            else:
+                log.debug("Video caps found, but no video encoder")
+        elif caps.to_string().startswith('audio'):
+            if self._audio_pad and not self._audio_pad.is_linked():
+                log.debug("Linking audio encoder")
+                pad.link(self._audio_pad)
+                self._audio_src.link(self._fileout)
+            elif self._audio_pad:
+                log.debug("Audio encoder already linked, probably multiple audio streams")
+            else:
+                log.debug("Audio caps found, but no audio encoder")
+                
+    def progress(self):        
+        try:
+            (pos, format) = self.query_position(gst.FORMAT_TIME)
+            (dur, format) = self.query_duration(gst.FORMAT_TIME)
+            log.debug("Conversion progress %.2f%%" % (float(pos)*100.0/dur))
+            return (pos/float(gst.SECOND), dur/float(gst.SECOND))
+        except gst.QueryError:
+            log.debug("QUERY ERROR")
+            return (0.0, 0.0)
+                
+    def convert(self):
+        gst.debug_set_default_threshold(2)
+        self.bus = self.get_bus()
+        self.bus.add_signal_watch()
+        self.bus.connect("message", self._bus_message_cb)
+        log.debug("Starting conversion")
+        self.watch  = gobject.timeout_add(2000, self.progress)
+        if not self.set_state(gst.STATE_PLAYING):
+            self._finished()
+            
+class GStreamerConverter():
+    def __init__(self, filename):
+        self.filename = filename
+
+    def get_stream_info(self, needs_audio = False, needs_video = False):                
+        def discovered(discoverer, valid):
+            self.valid = valid
+            event.set()        
+        event = threading.Event()    
+        log.debug("Getting stream information file: %s" % self.filename)
+        self.info = discoverer.Discoverer(self.filename)
+        self.info.connect('discovered', discovered)
+        self.info.discover()
+        event.wait()        
+        if not self.valid:
+            raise Exception('Not a valid media file')
+        if needs_video and not self.info.is_video:
+            raise Exception("Not a valid video file")
+        if needs_audio and not self.info.is_audio:
+            raise Exception("Not a valid audio file")            
+        if self.info.is_video:
+            return (self.info.videowidth, self.info.videoheight, \
+                self.info.videolength / gst.SECOND)
+        elif self.info.is_audio:            
+            return (self.info.audiolength / gst.SECOND)
+        else:
+            raise Exception
+            
+    def _run_pipeline(self, **kwargs):
+        def converted(converter, success):
+            if not success:
+                raise Exception
+            self.success = success
+            event.set()            
+        event = threading.Event()
+        pipeline = GStreamerConversionPipeline(**kwargs)
+        pipeline.connect("converted", converted)
+        pipeline.convert()
+        event.wait()        
+        return self.success
+
+    def convert(self, **kwargs):
+        if kwargs.get('twopass', False):
+            kwargs['pass'] = 1
+            self._run_pipeline(self, **kwargs)
+            kwargs['pass'] = 2
+            return self._run_pipeline(self, **kwargs)
+        else:
+            return self._run_pipeline(self, **kwargs)
 
-    #These commands are run to determine attributes about the file 
-    #(such as size and duration) prior to transcode. They should be
-    #Robust and work with ALL input files, even if the transode step may
-    #later fail
 
-    VIDEO_INSPECT_COMMAND = 'ffmpeg -an -y -t 0:0:0.001 -i "%s" -f image2 "%s" 2>&1'
-    AUDIO_INSPECT_COMMAND = 'ffmpeg -fs 1 -y -i "%s" -f wav "%s" 2>&1'
+class AudioVideoConverter(TypeConverter.Converter):
 
     def __init__(self):
         self.conversions =  {
-                            "file/video,file/video"     :   self.transcode_video,    
+                            "file/video,file/video"     :   self.transcode_video,
                             "file,file/video"           :   self.file_to_video,
-                            "file/audio,file/audio"     :   self.transcode_audio,    
+                            "file/audio,file/audio"     :   self.transcode_audio,
                             "file,file/audio"           :   self.file_to_audio
                             }
-                            
+
     def transcode_video(self, video, **kwargs):
-        mimetype = video.get_mimetype()
-        if not Video.mimetype_is_video(mimetype):
-            log.debug("File %s is not video type: %s" % (video,mimetype))
-            return None
+        #mimetype = video.get_mimetype()
+        #if not Video.mimetype_is_video(mimetype):
+        #    log.debug("File %s is not video type: %s" % (video,mimetype))
+        #    return None
         input_file = video.get_local_uri()
         
-        #run ffmpeg over the video to work out its format, and duration
-        c = CommandLineConverter.CommandLineConverter()
-        c.build_command(AudioVideoConverter.VIDEO_INSPECT_COMMAND)
-        ok,output = c.convert(input_file,"/dev/null",save_output=True)
+        log.debug("Creating GStreamer converter")
 
-        if not ok:
-            log.debug("Error getting video information\n%s" % output)
-            return None
+        gst_converter = GStreamerConverter(input_file) 
+        #try:
+        log.debug("Getting video information")
+        (w, h, duration) = gst_converter.get_stream_info(needs_video = True)
+        #except:
+        #    log.debug("Error getting video information")
+        #    return None
 
-        #extract the video parameters    
-        pat = re.compile(r'Input.*?Duration: ([\d:]*\.*\d*).*?Stream #\d\.\d: Video:.*?(\d+)x(\d+)',re.DOTALL)
-        try:
-            duration_string,w,h = re.search(pat,output).groups()
-            #make duration into seconds
-            ho,m,s = duration_string.split(':')
-            duration = (60.0*60.0*float(ho)) + (60*float(m)) + float(s)
-        except AttributeError:
-            log.debug("Error parsing ffmpeg output")
-            return None
         log.debug("Input Video %s: size=%swx%sh, duration=%ss" % (input_file,w,h,duration))
 
-        if kwargs.get('width',None) != None and kwargs.get('height',None) != None:
+        if 'width' in kwargs and 'height' in kwargs:
             kwargs['width'],kwargs['height'] = Utils.get_proportional_resize(
                             desiredW=int(kwargs['width']),
                             desiredH=int(kwargs['height']),
@@ -144,30 +313,30 @@
                             currentH=int(h)
                             )
 
+        #TODO: Test folder_location code
+        #if kwargs.has_key("folder_location"):
+        #    output_file = kwargs.has_key("folder_location")
+        #    if not os.path.isdir(output_file):
+        #        log.debug("Output location not a folder")
+        #        return None
+        #    output_file = os.path.join(output_file, os.path.basename(input_file))
+        #    log.debug("Using output_file = %s", output_file)
+        #else:
+        
         #create output file
         output_file = video.to_tempfile()
         if kwargs.has_key("file_extension"):
             video.force_new_file_extension(".%s" % kwargs["file_extension"])
-
-        #convert the video
-        if kwargs.get("mencoder", False) and Utils.program_installed("mencoder"):
-            c = MencoderCommandLineConverter()
-        else:    
-            c = FFmpegCommandLineConverter(duration=duration)
-        c.build_command(**kwargs)
-        ok,output = c.convert(
-                        input_file,
-                        output_file,
-                        callback=lambda x: log.debug("Trancoding video %s%% complete" % x),
-                        save_output=True
-                        )
-
-        if not ok:
+        kwargs['in_file'] = input_file
+        kwargs['out_file'] = output_file
+        sucess = gst_converter.convert(**kwargs)
+        
+        if not sucess:
             log.debug("Error transcoding video\n%s" % output)
             return None
 
         return video
-        
+
     def transcode_audio(self, audio, **kwargs):
         mimetype = audio.get_mimetype()
         if not Audio.mimetype_is_audio(mimetype):
@@ -175,48 +344,32 @@
             return None
         input_file = audio.get_local_uri()
 
-        #run ffmpeg over the video to work out its format, and duration
-        c = CommandLineConverter.CommandLineConverter()
-        c.build_command(AudioVideoConverter.AUDIO_INSPECT_COMMAND)
-        ok,output = c.convert(input_file,"/dev/null",save_output=True)
-
-        if not ok:
-            log.debug("Error getting audio information\n%s" % output)
-            return None
 
-        #extract the video parameters    
-        pat = re.compile(r'Input.*?Duration: ([\d:]*\.*\d*)',re.DOTALL)
+        gst_converter = GStreamerConverter(input_file)
         try:
-            duration_string = re.search(pat,output).group(1)
-            #make duration into seconds
-            h,m,s = duration_string.split(':')
-            duration = (60.0*60.0*float(h)) + (60*float(m)) + float(s)
-        except AttributeError:
-            log.debug("Error parsing ffmpeg output")
+            duration = gst_converter.get_stream_info(needs_audio = True)
+        except:
+            log.debug("Error getting audio information")
             return None
+
         log.debug("Input Audio %s: duration=%ss" % (input_file,duration))
-        
+
         #create output file
         output_file = audio.to_tempfile()
         if kwargs.has_key("file_extension"):
             audio.force_new_file_extension(".%s" % kwargs["file_extension"])
 
         #convert audio
-        c = FFmpegCommandLineConverter(duration=duration)
-        c.build_command(**kwargs)
-        ok,output = c.convert(
-                        input_file,
-                        output_file,
-                        callback=lambda x: log.debug("Trancoding audio %s%% complete" % x),
-                        save_output=True
-                        )
-
-        if not ok:
+        kwargs['in_file'] = input_file
+        kwargs['out_file'] = output_file
+        sucess = gst_converter.convert(**kwargs)
+        
+        if not sucess:
             log.debug("Error transcoding audio\n%s" % output)
             return None
 
         return audio
-        
+
     def file_to_audio(self, f, **kwargs):
         mimetype = f.get_mimetype()
         if Audio.mimetype_is_audio(mimetype):
@@ -244,4 +397,3 @@
                 return v
         else:
             return None
-

Modified: trunk/conduit/modules/TestModule.py
==============================================================================
--- trunk/conduit/modules/TestModule.py	(original)
+++ trunk/conduit/modules/TestModule.py	Wed Aug 20 23:17:13 2008
@@ -16,6 +16,7 @@
 import conduit.dataproviders.SimpleFactory as SimpleFactory
 import conduit.dataproviders.Image as Image
 import conduit.dataproviders.File as FileDataProvider
+import conduit.modules.iPodModule.iPodModule as iPodModule
 import conduit.Exceptions as Exceptions
 import conduit.Web as Web
 from conduit.datatypes import Rid, DataType, Text, Video, Audio, File
@@ -36,6 +37,8 @@
     "TestTwoWay" :              { "type": "dataprovider" },
     "TestFailRefresh" :         { "type": "dataprovider" },
     "TestSinkNeedConfigure" :   { "type": "dataprovider" },
+    "TestiPodMusic" :           { "type": "dataprovider" },
+    "TestiPodVideo" :           { "type": "dataprovider" },
     "TestFactory" :             { "type": "dataprovider-factory" },
 #    "TestFactoryRemoval" :      { "type": "dataprovider-factory" },
 #    "TestSimpleFactory" :       { "type": "dataprovider-factory" },
@@ -687,6 +690,12 @@
             raise Exceptions.SynchronizeConflictError(conduit.datatypes.COMPARISON_UNKNOWN, data, newData)
         return newData.get_rid()
 
+class TestiPodMusic(iPodModule.IPodMusicTwoWay):
+    pass
+
+class TestiPodVideo(iPodModule.IPodVideoTwoWay):
+    pass
+
 class TestConverter(TypeConverter.Converter):
     def __init__(self):
         self.conversions =  {

Modified: trunk/conduit/modules/iPodModule/__init__.py
==============================================================================
--- trunk/conduit/modules/iPodModule/__init__.py	(original)
+++ trunk/conduit/modules/iPodModule/__init__.py	Wed Aug 20 23:17:13 2008
@@ -0,0 +1 @@
+

Modified: trunk/conduit/modules/iPodModule/config.glade
==============================================================================
--- trunk/conduit/modules/iPodModule/config.glade	(original)
+++ trunk/conduit/modules/iPodModule/config.glade	Wed Aug 20 23:17:13 2008
@@ -2,7 +2,7 @@
 <!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
 <!--*- mode: xml -*-->
 <glade-interface>
-  <widget class="GtkDialog" id="PhotoConfigDialog">
+  <widget class="GtkDialog" id="iPodConfigDialog">
     <property name="visible">True</property>
     <property name="title" translatable="yes">iPod Photos</property>
     <property name="resizable">False</property>
@@ -13,12 +13,11 @@
       <widget class="GtkVBox" id="vbox30">
         <property name="visible">True</property>
         <child>
-          <widget class="GtkLabel" id="albumlabel">
+          <widget class="GtkLabel" id="label1">
             <property name="visible">True</property>
             <property name="xalign">0</property>
-            <property name="xpad">2</property>
-            <property name="ypad">2</property>
-            <property name="label" translatable="yes">Album:</property>
+            <property name="label" translatable="yes">&lt;b&gt;Encoding&lt;/b&gt;</property>
+            <property name="use_markup">True</property>
           </widget>
           <packing>
             <property name="expand">False</property>
@@ -27,30 +26,14 @@
           </packing>
         </child>
         <child>
-          <widget class="GtkHBox" id="hbox1">
+          <widget class="GtkAlignment" id="alignment1">
             <property name="visible">True</property>
+            <property name="xalign">0</property>
+            <property name="left_padding">16</property>
             <child>
-              <widget class="GtkComboBoxEntry" id="album_combobox">
-                <property name="visible">True</property>
-                <child internal-child="entry">
-                  <widget class="GtkEntry" id="comboboxentry-entry1">
-                  </widget>
-                </child>
-              </widget>
-            </child>
-            <child>
-              <widget class="GtkButton" id="delete_button">
+              <widget class="GtkComboBox" id="tagcombobox">
                 <property name="visible">True</property>
-                <property name="can_focus">True</property>
-                <property name="label">gtk-delete</property>
-                <property name="use_stock">True</property>
-                <property name="response_id">0</property>
               </widget>
-              <packing>
-                <property name="expand">False</property>
-                <property name="fill">False</property>
-                <property name="position">1</property>
-              </packing>
             </child>
           </widget>
           <packing>

Modified: trunk/conduit/modules/iPodModule/iPodModule.py
==============================================================================
--- trunk/conduit/modules/iPodModule/iPodModule.py	(original)
+++ trunk/conduit/modules/iPodModule/iPodModule.py	Wed Aug 20 23:17:13 2008
@@ -2,7 +2,7 @@
 Provides a number of dataproviders which are associated with
 removable devices such as USB keys.
 
-It also includes classes specific to the ipod. 
+It also includes classes specific to the ipod.
 This file is not dynamically loaded at runtime in the same
 way as the other dataproviders as it needs to be loaded all the time in
 order to listen to HAL events
@@ -13,6 +13,12 @@
 import os
 import pickle
 import logging
+import time
+import socket
+import locale
+import weakref
+import threading
+DEFAULT_ENCODING = locale.getpreferredencoding()
 log = logging.getLogger("modules.iPod")
 
 import conduit
@@ -24,9 +30,13 @@
 import conduit.datatypes.Contact as Contact
 import conduit.datatypes.Event as Event
 import conduit.datatypes.File as File
+import conduit.datatypes.Audio as Audio
+import conduit.datatypes.Video as Video
+
+from gettext import gettext as _
 
 MODULES = {
-        "iPodFactory" :         { "type":   "dataprovider-factory"  }
+    "iPodFactory" :         { "type":   "dataprovider-factory"  },
 }
 
 try:
@@ -44,7 +54,7 @@
         f = File.File(uri)
         if not f.exists():
             break
-    
+
     temp = Utils.new_tempfile(txt)
     temp.transfer(uri, True)
     temp.set_UID(filename)
@@ -55,7 +65,7 @@
         if props.has_key("info.parent") and props.has_key("info.parent")!="":
             prop2 = self._get_properties(props["info.parent"])
             if prop2.has_key("storage.model") and prop2["storage.model"]=="iPod":
-               return True
+                return True
         return False
 
     def get_category(self, udi, **kwargs):
@@ -66,16 +76,16 @@
 
     def get_dataproviders(self, udi, **kwargs):
         if LIBGPOD_PHOTOS:
-            #Read information about the ipod, like if it supports 
+            #Read information about the ipod, like if it supports
             #photos or not
             d = gpod.itdb_device_new()
             gpod.itdb_device_set_mountpoint(d,kwargs['mount'])
             supportsPhotos = gpod.itdb_device_supports_photo(d)
             gpod.itdb_device_free(d)
             if supportsPhotos:
-                return [IPodNoteTwoWay, IPodContactsTwoWay, IPodCalendarTwoWay, IPodPhotoSink]
+                return [IPodMusicTwoWay, IPodVideoTwoWay, IPodNoteTwoWay, IPodContactsTwoWay, IPodCalendarTwoWay, IPodPhotoSink]
 
-        return [IPodNoteTwoWay, IPodContactsTwoWay, IPodCalendarTwoWay]
+        return [IPodMusicTwoWay, IPodVideoTwoWay, IPodNoteTwoWay, IPodContactsTwoWay, IPodCalendarTwoWay]
 
 
 class IPodBase(DataProvider.TwoWay):
@@ -92,7 +102,7 @@
         #Also checks directory exists
         if not os.path.exists(self.dataDir):
             os.mkdir(self.dataDir)
-        
+
         #When acting as a source, only notes in the Notes dir are
         #considered
         for f in os.listdir(self.dataDir):
@@ -118,10 +128,10 @@
 
     def _get_unique_filename(self, directory):
         """
-        Returns the name of a non-existant file on the 
+        Returns the name of a non-existant file on the
         ipod within directory
-        
-        @param directory: Name of the directory within the device root to make 
+
+        @param directory: Name of the directory within the device root to make
         the random file in
         """
         done = False
@@ -130,15 +140,15 @@
             if not os.path.exists(f):
                 done = True
         return f
-        
+
 class IPodNoteTwoWay(IPodBase):
     """
-    Stores Notes on the iPod. 
+    Stores Notes on the iPod.
     Rather than requiring a perfect transform to and from notes to the
-    ipod note format I also store the original note data in a 
+    ipod note format I also store the original note data in a
     .conduit directory in the root of the iPod.
 
-    Notes are saved as title.txt and a copy of the raw note is saved as 
+    Notes are saved as title.txt and a copy of the raw note is saved as
     title.note
 
     LUID is the note title
@@ -150,7 +160,7 @@
     _in_type_ = "note"
     _out_type_ = "note"
     _icon_ = "tomboy"
-    
+
     # datatypes.Note doesn't care about encoding,
     # lets be naive and assume that all notes are utf-8
     ENCODING_DECLARATION = '<?xml encoding="utf-8"?>'
@@ -158,7 +168,7 @@
     def __init__(self, *args):
         IPodBase.__init__(self, *args)
 
-        self.dataDir = os.path.join(self.mountPoint, 'Notes')        
+        self.dataDir = os.path.join(self.mountPoint, 'Notes')
         self.objects = []
 
     def _get_shadow_dir(self):
@@ -179,7 +189,7 @@
                 n = pickle.load(raw)
                 raw.close()
                 return n
-            except: 
+            except:
                 raw.close()
 
         noteURI = os.path.join(self.dataDir, uid)
@@ -195,7 +205,7 @@
         n.set_mtime(noteFile.get_mtime())
         n.set_open_URI(noteURI)
         return n
-    
+
     def _save_note_to_ipod(self, uid, note):
         """
         Save a simple iPod note in /Notes
@@ -208,11 +218,11 @@
         if not self.ENCODING_DECLARATION in contents:
             contents = ''.join([self.ENCODING_DECLARATION, contents])
         ipodnote = Utils.new_tempfile(contents)
-        
+
         ipodnote.transfer(os.path.join(self.dataDir,uid), overwrite=True)
         ipodnote.set_mtime(note.get_mtime())
         ipodnote.set_UID(uid)
-        
+
         #the raw pickled note for sync
         raw = open(os.path.join(self._get_shadow_dir(),uid),'wb')
         pickle.dump(note, raw, -1)
@@ -224,7 +234,7 @@
         #Check if both the shadow copy and the ipodified version exists
         shadowDir = self._get_shadow_dir()
         return os.path.exists(os.path.join(shadowDir,uid)) and os.path.exists(os.path.join(self.dataDir,uid))
-                
+
     def get(self, LUID):
         DataProvider.TwoWay.get(self, LUID)
         return self._get_note_from_ipod(LUID)
@@ -246,11 +256,11 @@
                     #only overwrite if newer
                     log.warn("OVERWRITE IF NEWER NOT IMPLEMENTED")
                     return self._save_note_to_ipod(LUID, note)
-    
+
         #make a new note
         log.warn("CHECK IF EXISTS, COMPARE, SAVE")
         return self._save_note_to_ipod(note.title, note)
-    
+
     def delete(self, LUID):
         IPodBase.delete(self, LUID)
 
@@ -268,7 +278,7 @@
     _icon_ = "contact-new"
 
     def __init__(self, *args):
-        IPodBase.__init__(self, *args)        
+        IPodBase.__init__(self, *args)
         self.dataDir = os.path.join(self.mountPoint, 'Contacts')
 
     def get(self, LUID):
@@ -291,7 +301,7 @@
             f.transfer(os.path.join(self.dataDir, LUID), overwrite=True)
             f.set_UID(LUID)
             return f.get_rid()
-        
+
         return _string_to_unqiue_file(contact.get_vcard_string(), self.dataDir, 'contact')
 
 class IPodCalendarTwoWay(IPodBase):
@@ -338,7 +348,6 @@
     _in_type_ = "file/photo"
     _out_type_ = "file/photo"
     _icon_ = "image-x-generic"
-    _configurable_ = True
 
     SAFE_PHOTO_ALBUM = "Photo Library"
 
@@ -347,10 +356,10 @@
         self.db = gpod.PhotoDatabase(self.mountPoint)
         self.albumName = "Conduit"
         self.album = None
-        
+
     def _set_sysinfo(self, modelnumstr, model):
         gpod.itdb_device_set_sysinfo(self.db._itdb.device, modelnumstr, model)
-        
+
     def _get_photo_album(self, albumName):
         for album in self.db.PhotoAlbums:
             if album.name == albumName:
@@ -367,14 +376,14 @@
         else:
             album = self.db.new_PhotoAlbum(title=albumName)
         return album
-        
+
     def _get_photo_by_id(self, id):
         for album in self.db.PhotoAlbums:
             for photo in album:
                 if str(photo['id']) == str(id):
                     return photo
         return None
-        
+
     def _delete_album(self, albumName):
         if albumName == self.SAFE_PHOTO_ALBUM:
             log.warn("Cannot delete album: %s" % self.SAFE_PHOTO_ALBUM)
@@ -383,7 +392,7 @@
             for photo in album[:]:
                 album.remove(photo)
             self.db.remove(album)
-            
+
     def _empty_all_photos(self):
         for photo in self.db.PhotoAlbums[0][:]:
             self.db.remove(photo)
@@ -416,7 +425,7 @@
             self.db.remove(photo)
             gpod.itdb_photodb_write(self.db._itdb, None)
 
-    def configure(self, window):    
+    def configure(self, window):
         import gobject
         import gtk
         def build_album_model(albumCombo):
@@ -441,11 +450,11 @@
             if albumName:
                 self._delete_album(albumName)
                 build_album_model(albumCombo)
- 
+
         #get a whole bunch of widgets
         tree = Utils.dataprovider_glade_get_widget(
-                        __file__, 
-                        "config.glade", 
+                        __file__,
+                        "config.glade",
                         "PhotoConfigDialog")
         albumCombo = tree.get_widget("album_combobox")
         delete_button = tree.get_widget("delete_button")
@@ -456,19 +465,19 @@
         cell = gtk.CellRendererText()
         albumCombo.pack_start(cell, True)
         albumCombo.set_text_column(0)
-        
+
         #setup widgets
         build_album_model(albumCombo)
         delete_button.connect('clicked', delete_click, albumCombo)
 
-        # run dialog 
+        # run dialog
         dlg = tree.get_widget("PhotoConfigDialog")
         response = Utils.run_dialog(dlg, window)
 
         if response == True:
             #get the values from the widgets
             self.albumName = albumCombo.get_active_text()
-        dlg.destroy()    
+        dlg.destroy()
 
         del self.album_store
 
@@ -477,5 +486,373 @@
 
     def uninitialize(self):
         self.db.close()
-                
 
+STR_CONV = lambda v: unicode(v).encode('UTF-8','replace')
+INT_CONV = lambda v: int(v)
+
+class IPodFileBase:
+    # Supported tags from the Audio class supported in the iPod database
+    SUPPORTED_TAGS = ['title', 'artist', 'album', 'composer', 'rating',
+        'genre', 'track-number', 'track-count', 'bitrate', 'duration',
+        'samplerate']
+    # Conversion between Audio names and iPod names in tags
+    KEYS_CONV = {'duration': 'tracklen',
+                 'track-number': 'track_nr',
+                 'track-count': 'tracks'}
+    # Convert values into their native types
+    VALUES_CONV = {
+        'rating': lambda v: float(v) / 0.05,
+        'samplerate': INT_CONV,
+        'bitrate': INT_CONV,
+        'track-number': INT_CONV,
+        'track-count': INT_CONV,
+        'duration': INT_CONV,
+        'width': INT_CONV,
+        'height': INT_CONV
+    }
+
+    def __init__(self, db):
+        self.db = db
+        self.track = self.db.new_Track()
+
+    def set_info_from_file(self, f):
+        # Missing: samplerate (int), samplerate2 (float), bitrate (int),
+        # composer (str), filetype (str, "MPEG audio file"), mediatype (int, 1)
+        # tracks (int)
+        # unk126 (int, "65535"), unk144 (int, "12"),
+        tags = f.get_media_tags()
+        for key, value in tags.iteritems():
+            if key not in self.SUPPORTED_TAGS:
+                continue
+            if key in self.VALUES_CONV:
+                # Convert values into nativa types
+                tag_value = self.VALUES_CONV[key](value)
+            else:
+                # Encode into UTF-8
+                tag_value = STR_CONV(value)
+            if key in self.KEYS_CONV:
+                tag_name = self.KEYS_CONV[key]
+            else:
+                tag_name = key
+            self.track[tag_name] = tag_value
+        print self.track['title']
+        if self.track['title'] is None:
+            self.track['title'] = os.path.basename(f.get_local_uri())
+            print self.track['title']
+        self.track['time_modified'] = os.stat(f.get_local_uri()).st_mtime
+        self.track['time_added'] = int(time.time())
+        self.track['userdata'] = {'transferred': 0,
+                                  'hostname': socket.gethostname(),
+                                  'charset': DEFAULT_ENCODING}
+        self.track._set_userdata_utf8('filename', f.get_local_uri())
+
+    #FIXME: Remove this. Use native operations from Conduit instead.
+    def copy_ipod(self):
+        self.track.copy_to_ipod()
+
+class IPodAudio(Audio.Audio, IPodFileBase):
+    def __init__(self, f, db, **kwargs):
+        Audio.Audio.__init__(self, f.get_local_uri())
+        IPodFileBase.__init__(self, db)
+        self.set_info_from_audio(f)
+
+    def set_info_from_audio(self, audio):
+        self.set_info_from_file(audio)
+        self.track['mediatype'] = gpod.ITDB_MEDIATYPE_AUDIO
+        cover_location = audio.get_audio_cover_location()
+        if cover_location:
+            self.track.set_coverart_from_file(str(cover_location))
+
+class IPodVideo(Video.Video, IPodFileBase):
+    def __init__(self, f, db, **kwargs):
+        Video.Video.__init__(self, f.get_local_uri())
+        IPodFileBase.__init__(self, db)
+        log.debug('Video kind selected: %s' % (kwargs['video_kind']))
+        self.video_kind = kwargs['video_kind']
+        self.set_info_from_video(f)
+
+    def set_info_from_video(self, video):
+        self.set_info_from_file(video)
+        #FIXME: Movie should be a choice between Movie, MusicVideo, TvShow and Podcast
+        self.track['mediatype'] = {'movie': gpod.ITDB_MEDIATYPE_MOVIE,
+                                   'musicvideo': gpod.ITDB_MEDIATYPE_MUSICVIDEO,
+                                   'tvshow': gpod.ITDB_MEDIATYPE_TVSHOW,
+                                   'podcast': gpod.ITDB_MEDIATYPE_PODCAST
+                                   } [self.video_kind]
+
+class DBCache:
+    '''
+    Keeps a list of open GPod databases.
+
+    Keeps one database open for each mount-point.
+    Automatically disposes unused databases.
+    '''
+    __db_list = weakref.WeakValueDictionary()
+    __db_locks = weakref.WeakKeyDictionary()
+    __lock = threading.Lock()
+
+    @classmethod
+    def get_db(self, mount_point):
+        self.__lock.acquire()
+        try:
+            if mount_point in self.__db_list:
+                log.debug('Getting DB in cache for %s' % (mount_point))
+                db = self.__db_list[mount_point]
+                #self.__db_locks[db][1] += 1
+            else:
+                if mount_point:
+                    log.debug('Creating DB for %s' % mount_point)
+                    db = gpod.Database(mount_point)
+                else:
+                    log.debug('Creating local DB')
+                    db = gpod.Database(local=True)
+                self.__db_list[mount_point] = db
+                self.__db_locks[db] = threading.Lock()
+            return db
+        finally:
+            self.__lock.release()
+
+    @classmethod
+    def release_db(self, db):
+        assert db in self.__db_locks
+        log.debug('Releasing DB for %s' % db)
+        #self.__db_locks[db][1] -= 1
+
+    @classmethod
+    def lock_db(self, db):
+        assert db in self.__db_locks
+        #if self.__db_locks[db][1] == 1:
+        #    log.debug('Not locking DB for %s' % db)
+        #    return
+        log.debug('Locking DB %s' % db)
+        self.__db_locks[db].acquire()
+
+    @classmethod
+    def unlock_db(self, db):
+        assert db in self.__db_locks
+        log.debug('Unlocking DB %s' % db)
+        #lock = self.__db_locks[db][0]
+        #if lock.locked():
+        self.__db_locks[db].release()
+
+class IPodMediaTwoWay(IPodBase):
+    FORMAT_CONVERSION_STRING = _("Encoding")
+
+    def __init__(self, *args):
+        if len(args) != 0:
+            IPodBase.__init__(self, *args)
+            self.db = DBCache.get_db(self.mountPoint)
+        else:
+            # Use local database for testing
+            DataProvider.TwoWay.__init__(self)
+            self.db = DBCache.get_db(None)
+            self.uid = "Local"
+        #self.tracks = {}
+        self.tracks_id = {}
+        self.track_args = {}
+
+    def refresh(self):
+        DataProvider.TwoWay.refresh(self)
+        self.tracks = {}
+        self.tracks_id = {}
+        DBCache.lock_db(self.db)
+        try:
+            def add_track(track):
+                self.tracks_id[track['dbid']] = track
+                #FIXME: We dont need this do we?
+                #self.tracks[(track['artist'], track['title'])] = track
+            [add_track(track) for track in self.db \
+                if track['mediatype'] in self._mediatype_]
+        finally:
+            DBCache.unlock_db(self.db)
+
+    def get_all(self):
+        return self.tracks_id.keys()
+
+    def get(self, LUID = None):
+        DBCache.lock_db(self.db)
+        try:
+            track = self.tracks_id[LUID]
+            if track and track.ipod_filename() and os.path.exists(track.ipod_filename()):
+                f = self._mediafile_(URI=track.ipod_filename())
+                f.set_UID(LUID)
+                f.set_open_URI(track.ipod_filename())
+                if track['artist'] and track['title']:
+                    f.force_new_filename("%(artist)s - %(title)s" % track + \
+                        os.path.splitext(track.ipod_filename())[1])
+                return f
+        finally:
+            DBCache.unlock_db(self.db)
+        return None
+
+    def put(self, f, overwrite, LUID=None):
+        DBCache.lock_db(self.db)
+        try:
+            media_file = self._ipodmedia_(f, self.db, **self.track_args)
+            #FIXME: We keep the db locked while we copy the file. Not good
+            media_file.copy_ipod()
+            #FIXME: Writing the db here is for debug only. Closing does not actually
+            # close the db, it only writes it's contents to disk.
+            
+            # Sometimes, if we only close the db when the sync is over, it might
+            # take a long time to close the db, because many files are being 
+            # copied to the iPod. Closing the DB every time not only keeps
+            # this time small, but also keeps the db more consistent in case of 
+            # a crash. But it also incurs a big overhead. 
+            # Maybe a batch update could be a better solution (close after 5 tracks?)
+            self.db.close()
+            return media_file
+        finally:
+            DBCache.unlock_db(self.db)
+
+    def delete(self, LUID):
+        track = self.tracks_id[LUID]
+        if track:
+            DBCache.lock_db(db)
+            try:
+                self.db.remove(track)
+                self.db.close()
+            finally:
+                DBCache.unlock_db(db)
+
+    def get_config_items(self):
+        import gtk
+        def dict_update(a, b):
+            a.update(b)
+            return a
+        #Get an array of encodings, so it can be indexed inside a combobox
+        self.config_encodings = [dict_update({'name': name}, value) for name, value in self.encodings.iteritems()]
+        initial = None
+        for encoding in self.config_encodings:
+            if encoding['name'] == self.encoding:
+                initial = encoding.get('description', None) or encoding['name']
+
+        def selectEnc(index, text):
+            self.encoding = self.config_encodings[index]['name']
+            log.debug('Encoding %s selected' % self.encoding)
+            
+        return [
+                    {
+                    "Name" : self.FORMAT_CONVERSION_STRING,
+                    "Kind" : "list",
+                    "Callback" : selectEnc,
+                    "Values" : [encoding.get('description', None) or encoding['name'] for encoding in self.config_encodings],
+                    "InitialValue" : initial
+                    }
+                ]        
+
+    def configure(self, window):
+        import conduit.gtkui.SimpleConfigurator as SimpleConfigurator
+
+        dialog = SimpleConfigurator.SimpleConfigurator(window, self._name_, self.get_config_items())
+        dialog.run()
+
+    def set_configuration(self, config):
+        if 'encoding' in config:
+            self.encoding = config['encoding']
+
+    def get_configuration(self):
+        return {'encoding':self.encoding}
+
+    def get_input_conversion_args(self):
+        try:
+            return self.encodings[self.encoding]
+        except KeyError:
+            return {}
+
+    def uninitialize(self):
+        self.db.close()
+        DBCache.release_db(self.db)
+        self.db = None
+
+IPOD_AUDIO_ENCODINGS = {
+    "mp3": {"description": "Mp3", "acodec": "lame", "file_extension": "mp3"},
+    #FIXME: Does AAC needs a MP4 mux?
+    "aac": {"description": "AAC", "acodec": "faac", "file_extension": "m4a"},
+    }
+
+class IPodMusicTwoWay(IPodMediaTwoWay):
+
+    _name_ = "iPod Music"
+    _description_ = "Sync your iPod music"
+    _module_type_ = "twoway"
+    _in_type_ = "file/audio"
+    _out_type_ = "file/audio"
+    _icon_ = "audio-x-generic"
+    _configurable_ = True
+
+    _mediatype_ = (gpod.ITDB_MEDIATYPE_AUDIO,)
+    _mediafile_ = Audio.Audio
+    _ipodmedia_ = IPodAudio
+
+    def __init__(self, *args):
+        IPodMediaTwoWay.__init__(self, *args)
+        self.encodings = IPOD_AUDIO_ENCODINGS
+        self.encoding = 'aac'
+
+IPOD_VIDEO_ENCODINGS = {
+    "mp4_x264":{"description": "MP4 (H.264)","vcodec":"x264enc", "acodec":"faac", "format":"ffmux_mp4", "file_extension":"m4v", "width": 320, "height": 240},
+    "mp4_xvid":{"description": "MP4 (XVid)","vcodec":"xvidenc", "acodec":"faac", "format":"ffmux_mp4", "file_extension":"m4v", "width": 320, "height": 240},
+    }
+
+class IPodVideoTwoWay(IPodMediaTwoWay):
+
+    _name_ = "iPod Video"
+    _description_ = "Sync your iPod videos"
+    _module_type_ = "twoway"
+    _in_type_ = "file/video"
+    _out_type_ = "file/video"
+    _icon_ = "video-x-generic"
+    _configurable_ = True
+
+    _mediatype_ = (gpod.ITDB_MEDIATYPE_MUSICVIDEO,
+                   gpod.ITDB_MEDIATYPE_MOVIE,
+                   gpod.ITDB_MEDIATYPE_TVSHOW)
+    _mediafile_ = Video.Video
+    _ipodmedia_ = IPodVideo
+
+    def __init__(self, *args):
+        IPodMediaTwoWay.__init__(self, *args)
+        self.encodings = IPOD_VIDEO_ENCODINGS
+        self.encoding = 'mp4_x264'
+        self.video_kind = 'movie'
+        self._update_track_args()
+        
+    def _update_track_args(self):
+        self.track_args['video_kind'] = self.video_kind
+
+    def get_config_items(self):
+        video_kinds = [('Movie', 'movie'), 
+                       ('Music Video', 'musicvideo'),
+                       ('TV Show', 'tvshow')]
+        initial = None
+        for description, name in video_kinds:
+            if name == self.video_kind:
+                initial = description
+
+        def selectKind(index, text):
+            self.video_kind = video_kinds[index][1]
+            self._update_track_args()
+
+        items = IPodMediaTwoWay.get_config_items(self)
+        items.append( 
+                        {
+                            "Name" : "Video Kind",
+                            "Kind" : "list",
+                            "Callback" : selectKind,
+                            "Values" : [description for description, name in video_kinds],
+                            "InitialValue" : initial
+                        } 
+                    )             
+                    
+        return items
+    
+    def set_configuration(self, config):
+        IPodMediaTwoWay.set_configuration(self, config)
+        if 'video_kind' in config:
+            self.encoding = config['video_kind']
+
+    def get_configuration(self):
+        config = IPodMediaTwoWay.get_configuration(self)
+        config.update({'encoding':self.encoding})
+        return config

Added: trunk/conduit/utils/GstMetadata.py
==============================================================================
--- (empty file)
+++ trunk/conduit/utils/GstMetadata.py	Wed Aug 20 23:17:13 2008
@@ -0,0 +1,704 @@
+# -*- coding: utf-8 -*-
+# Elisa - Home multimedia server
+# Copyright (C) 2006-2008 Fluendo Embedded S.L. (www.fluendo.com).
+# All rights reserved.
+#
+# This file is available under one of two license agreements.
+#
+# This file is licensed under the GPL version 2.
+# See "LICENSE.GPL" in the root of this distribution including a special
+# exception to use Elisa with Fluendo's plugins.
+#
+# The GPL part of Elisa is also available under a commercial licensing
+# agreement from Fluendo.
+# See "LICENSE.Elisa" in the root directory of this distribution package
+# for details on that license.
+
+import gobject
+gobject.threads_init()
+import pygst
+pygst.require('0.10')
+import gst
+import logging
+import os
+import sys
+import time
+from threading import Lock
+from Queue import Queue
+import platform
+
+import md5
+
+if platform.system() == 'windows':
+    import win32process
+
+SEEK_SCHEDULED = 'scheduled'
+SEEK_DONE = 'done'
+
+THUMBNAIL_DIR = os.path.join(os.path.expanduser("~"), ".thumbnails", 'large')
+THUMBNAIL_SIZE = 256
+
+__maintainer__ = 'Alessandro Decina <alessandro fluendo com>'
+
+supported_metadata_keys = set(['artist', 'album', 'song', 'track', 'thumbnail'])
+media_type_keys = set(['uri', 'file_type', 'mime_type'])
+thumbnail_keys = set(['uri', 'thumbnail'])
+supported_keys = supported_metadata_keys.union(media_type_keys)
+supported_schemes = ['file', 'http']
+
+class MetadataError(Exception):
+    pass
+
+class InitializeFailure(MetadataError):
+    pass
+
+class TimeoutError(MetadataError):
+    pass
+
+class GstMetadataError(MetadataError):
+    pass
+
+class UriError(MetadataError):
+    pass
+
+def able_to_handle(supported_schemes, supported_keys, metadata):
+    uri = metadata.get('uri')
+    if not uri or uri.scheme not in supported_schemes:
+        return False
+
+    keys = set(metadata.keys())
+    if uri.scheme == 'file' and os.path.isdir(uri.path) and \
+            keys != media_type_keys:
+        return False
+
+    request_keys = supported_keys.intersection(metadata.keys())
+    request_empty_keys = \
+            [key for key in request_keys if metadata[key] is None]
+
+    if request_empty_keys:
+        return True
+
+    return False
+
+class MetadataProvider(object):
+    pass
+
+class Loggable(object):
+    def debug(self, msg):
+        logging.warning(msg)
+        #logging.debug(msg)
+
+    def log(self, msg):
+        logging.warning(msg)
+        #logging.info(msg)
+
+class GstMetadataPipeline(Loggable):
+    reuse_elements = True
+    timeout = 2
+    thumb_timeout = 1
+
+    def __init__(self):
+        super(GstMetadataPipeline, self).__init__()
+        self._pipeline = None
+        self._src = None
+        self._decodebin = None
+        self._ffmpegcolorspace = None
+        self._imgthumbbin = None
+        self._videothumbbin = None
+        self._plugged_elements = []
+        self._frame_locations = [1.0 / 3.0, 2.0 / 3.0, 0.1, 0.9, 0.5]
+
+        # other instance variables that need to be reset for each new metadata
+        # request are set directly in _reset()
+
+    def clean(self):
+        self._clean_pipeline(finalize=True)
+
+        if self._timeout_call is not None:
+            self._timeout_call.cancel()
+            self._timeout_call = None
+
+        if self._seek_call is not None:
+            self._seek_call.cancel()
+            self._seek_call = None
+
+    def initialize(self):
+        self._reset()
+
+    def _clean_pipeline(self, finalize=False):
+        # reset the pipeline to READY
+        if self._pipeline is not None:
+            self._bus.set_flushing(True)
+            self._pipeline.set_state(gst.STATE_READY)
+
+        if self._src is not None:
+            self._pipeline.remove(self._src)
+            self._src.set_state(gst.STATE_NULL)
+            self._src = None
+
+        if not self.reuse_elements or finalize:
+            # destroy the pipeline
+            if self._pipeline is not None:
+                self._bus.set_flushing(True)
+                self._pipeline.set_state(gst.STATE_NULL)
+                self._pipeline = None
+                self._decodebin = None
+                self._ffmpegcolorspace = None
+                self._imgthumbbin = None
+                self._videothumbbin = None
+                self._plugged_elements = []
+        else:
+            # reusing decodebin leads to problems
+            if self._decodebin is not None:
+                self._typefind.unlink(self._decodebin)
+                self._decodebin.set_state(gst.STATE_NULL)
+                self._pipeline.remove(self._decodebin)
+                self._decodebin = None
+
+            # remove dynamically plugged elements
+            for element in self._plugged_elements:
+                self._pipeline.remove(element)
+                element.set_state(gst.STATE_NULL)
+            self._plugged_elements = []
+
+    def _build_pipeline(self):
+        self._pipeline = gst.Pipeline()
+        self._bus = self._pipeline.get_bus()
+        self._bus.add_signal_watch()
+        self._bus.connect('message::application',
+                self._bus_message_application_cb)
+        self._bus.connect('message::error', self._bus_message_error_cb)
+        self._bus.connect('message::eos', self._bus_message_eos_cb)
+        self._bus.connect('message::tag', self._bus_message_tag_cb)
+        self._bus.connect('message::state-changed',
+                self._bus_message_state_changed_cb)
+        self._src = None
+        self._typefind = gst.element_factory_make('typefind')
+        self._typefind.connect('have-type', self._typefind_have_type_cb)
+        pad = self._typefind.get_pad('src')
+        self._pipeline.add(self._typefind)
+
+        self._pipeline.set_state(gst.STATE_READY)
+
+    def _reset(self):
+        # NOTE: we call gst_element_set_state so we MUST NOT be called from the
+        # streaming thread
+
+        # destroy the current pipeline if reuse_elements == False, otherwise
+        # clean it so that it can be reused
+        self._clean_pipeline()
+        if self._pipeline is None:
+            # we're either being called from initialize() or
+            # self.reuse_elements == False
+            self._build_pipeline()
+
+        # the metadata dictionary of the current request
+        self._req_metadata = None
+        # the uri value in the metadata dictionary
+        self._req_uri = None
+        # the deferred that we callback when we finish loading stuff in
+        # self._req_metadata
+        self._req_callback = None
+
+        # the caps as given by the typefind::have-type signal
+        self._typefind_caps = None
+        self._typefind_file_type = None
+        self._typefind_mime_type = None
+
+        # the video/audio/image caps that we get from decodebin pads when
+        # we plug decodebin
+        self._video_caps = None
+        self._audio_caps = None
+        self._image_caps = None
+
+        # the taglist containing all the tags for the stream
+        self._tags = gst.TagList()
+
+        # the duration of the current stream, used to seek when doing a
+        # thumbnail
+        self._duration = None
+        self._seek_status = None
+        self._seek_location_index = 0
+        self._seek_call = None
+
+        self._timeout_call = None
+
+        # timestamps used for logging purposes
+        self._start_timestamp = 0
+        self._end_timestamp = 0
+
+    def _bus_message_error_cb(self, bus, message):
+        gerror, debug = message.parse_error()
+        if self._typefind_file_type is not None or \
+                self._video_caps is not None or \
+                self._audio_caps is not None or \
+                self._image_caps is not None:
+            # we got an error going to PAUSED but we still can report the info
+            # that we got from have_type_cb
+            self.debug('error going to paused %s: %s', gerror.message, debug)
+            self._clean_thumbnail()
+            self._done()
+        else:
+            self._failed(GstMetadataError('error'
+                    ' domain: %r code: %r message: %s debug: %s' %
+                    (gerror.domain, gerror.code, gerror.message, debug)))
+
+    def _bus_message_application_cb(self, bus, message):
+        if message.structure.get_name() == 'metadata-done':
+            self._done()
+            return
+
+    def _bus_message_eos_cb(self, bus, message):
+        self.log('got EOS')
+
+        self._done()
+
+    def _bus_message_tag_cb(self, bus, message):
+        taglist = message.parse_tag()
+        self._tags = self._tags.merge(taglist, gst.TAG_MERGE_APPEND)
+
+    def _bus_message_state_changed_cb(self, bus, message):
+        if message.src is not self._pipeline:
+            return
+
+        prev, current, pending = message.parse_state_changed()
+        if prev == gst.STATE_READY and current == gst.STATE_PAUSED and \
+                self._decodebin is not None and \
+                self._decodebin.get_pad('sink').is_linked():
+            self.debug('reached PAUSED')
+
+            if self._video_caps is None and self._image_caps is None and \
+                self._typefind_file_type not in ('video', 'image'):
+                # we have the tags at this point
+                self._done()
+
+    def _typefind_have_type_cb(self, typefind, probability, caps):
+        self.debug('have type %s' % caps)
+
+        # self._typefind_caps = caps is broken, bug in the bindings
+        # FIXME: fix the bug and change this asap
+        self._typefind_caps = caps.copy()
+        gst_mime_type = self._typefind_mime_type = self._typefind_caps[0].get_name()
+        file_type = self._typefind_file_type = gst_mime_type.split('/')[0]
+
+        # NB: id3 tags most of the time are used with mp3 (even if it isn't
+        # uncommon to find them with AIFF or WAV). Given that mp3 is by far the
+        # most used audio format at the moment we make the common case fast here
+        # by assuming that the file_type is audio. By doing this we also set the
+        # mime_type to application/x-id3, but this doesn't matter at the moment
+        # since we don't use the mime_type anywhere.
+        if gst_mime_type == 'application/x-id3':
+            file_type = self._typefind_file_type = 'audio'
+        elif gst_mime_type == 'audio/x-m4a':
+            # FIXME: see http://bugzilla.gnome.org/show_bug.cgi?id=340375 and use this
+            # hack until we write our typefinder for this
+            file_type = None
+
+        req_keys = set(self._req_metadata.keys())
+        if (req_keys == media_type_keys and \
+                file_type in ('video', 'audio', 'image'))or \
+                (file_type in ('video', 'image') and \
+                (not 'thumbnail' in req_keys or self._have_thumbnail())):
+            self.debug('got media_type for %s, NOT going to paused',
+                    self._req_uri)
+            # we are in the streaming thread so we post a message on the bus
+            # here and when we read it from the main thread we call _done()
+            structure = gst.Structure('metadata-done')
+            self._bus.post(gst.message_new_application(self._pipeline, structure))
+            return
+
+        # we need tags and/or a thumbnail
+        self.debug('we need to go to PAUSED, plugging decodebin '
+                '(file_type: %s)' % file_type)
+        self._plug_decodebin()
+
+    def _plug_decodebin(self):
+        if self._decodebin is None:
+            self._decodebin = gst.element_factory_make('decodebin')
+            self._decodebin.connect('new-decoded-pad',
+                    self._decodebin_new_decoded_pad_cb)
+            self._decodebin.connect('unknown-type',
+                    self._decodebin_unknown_type_cb)
+            self._pipeline.add(self._decodebin)
+
+        self._typefind.link(self._decodebin)
+        pad = self._typefind.get_pad('src')
+        self._decodebin.set_state(gst.STATE_PAUSED)
+
+    def _check_thumbnail_directory(self):
+        if not os.path.exists(THUMBNAIL_DIR):
+            try:
+                os.makedirs(THUMBNAIL_DIR, 0700)
+            except OSError, e:
+                msg = "Could not make directory %r: %s. Thumbnail not saved." % (directory, e)
+                self.warning(msg)
+                raise ThumbnailError(self._req_uri, msg)
+
+    def _boring_cb(self, obj, buffer):
+        self.debug('boring buffer')
+        self._seek_next_thumbnail_location()
+
+    def _plug_video_thumbnailbin(self, video_pad):
+        self.debug('pluging video thumbbin')
+
+        self._check_thumbnail_directory()
+
+        if self._videothumbbin is None:
+            self._videothumbbin = PngVideoSnapshotBin()
+            self._videothumbbin.connect('boring', self._boring_cb)
+            self._pipeline.add(self._videothumbbin)
+
+        thumbbin = self._videothumbbin
+
+        filesink = gst.element_factory_make('filesink')
+        self._pipeline.add(filesink)
+        filesink.props.location = get_thumbnail_location(self._req_uri)
+
+        video_pad.link(thumbbin.get_pad('sink'))
+        thumbbin.get_pad('src').link(filesink.get_pad('sink'))
+
+        thumbbin.set_state(gst.STATE_PAUSED)
+        filesink.set_state(gst.STATE_PAUSED)
+
+        self._plugged_elements.append(filesink)
+        self.debug('video thumbbin plugged')
+
+    def _plug_image_thumbnailbin(self, image_pad):
+        self.debug('plugging image thumbbin')
+
+        self._check_thumbnail_directory()
+
+        if self._imgthumbbin is None:
+        # we can't register the element on old gst-python versions so we can't
+        # use gst_element_factory_make
+        #    self._imgthumbbin = gst.element_factory_make('pngimagesnapshot')
+            self._imgthumbbin = PngImageSnapshotBin()
+            self._pipeline.add(self._imgthumbbin)
+        thumbbin = self._imgthumbbin
+
+        filesink = gst.element_factory_make('filesink')
+        self._pipeline.add(filesink)
+        filesink.props.location = get_thumbnail_location(self._req_uri)
+
+        image_pad.link(thumbbin.get_pad('sink'))
+        thumbbin.get_pad('src').link(filesink.get_pad('sink'))
+
+        thumbbin.set_state(gst.STATE_PAUSED)
+        filesink.set_state(gst.STATE_PAUSED)
+
+        self._plugged_elements.append(filesink)
+        self.debug('image thumbbin plugged')
+
+    #def _have_thumbnail(self):
+    #    location = get_thumbnail_location(self._req_uri)
+    #    if os.path.exists(location):
+    #        stat = os.stat(location)
+    #        if stat.st_size != 0:
+    #            return True
+
+    #    return False
+
+    def _find_decoder(self, pad):
+        target = pad.get_target()
+        element = target.get_parent()
+        klass = element.get_factory().get_klass()
+        if 'Decoder' in klass:
+            return element
+        return None
+
+    def _get_type_from_decoder(self, decoder):
+        klass = decoder.get_factory().get_klass()
+        parts = klass.split('/', 2)
+        if len(parts) != 3:
+            return None
+
+        return parts[2].lower()
+
+    def _seek_next_thumbnail_location(self):
+        self._seek_status = SEEK_SCHEDULED
+
+        #self._seek_call = \
+        #    reactor.callLater(0, self._seek_next_thumbnail_location_real)
+
+    def _seek_next_thumbnail_location_real(self):
+        self._seek_call = None
+        self._seek_status = SEEK_DONE
+
+        if self._duration is None:
+            # first seek, get the duration
+            try:
+                self._duration, format = self._pipeline.query_duration(gst.FORMAT_TIME)
+            except gst.QueryError, e:
+                self.debug('duration query failed: %s', e)
+
+                return
+
+            if self._duration == -1:
+                self.debug('invalid duration, not seeking')
+                return
+
+            self.debug('stream duration %s' % self._duration)
+
+        if self._seek_location_index == len(self._frame_locations):
+            self.debug('no more seek locations')
+            return self._failed(ThumbnailError('no more seek locations'))
+
+        location = self._frame_locations[self._seek_location_index]
+        self.debug('seek to location %d, time %s duration %s' %
+                (self._seek_location_index,
+                gst.TIME_ARGS(int(location * self._duration)),
+                gst.TIME_ARGS(self._duration)))
+        self._seek_location_index += 1
+
+        res = self._pipeline.seek(1.0, gst.FORMAT_TIME,
+                gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_KEY_UNIT,
+                gst.SEEK_TYPE_SET, int(location * self._duration),
+                gst.SEEK_TYPE_NONE, 0)
+
+        self.debug('seek done res %s' % res)
+
+    def _close_pad(self, pad):
+        queue = gst.element_factory_make('queue')
+        # set the queue leaky so that if we take some time to do the thumbnail
+        # the demuxer doesnt' block on full queues
+        queue.props.leaky = 1
+        sink = gst.element_factory_make('fakesink')
+        self._pipeline.add(queue, sink)
+        # add sink before queue so when we iterate over the elements to clean
+        # them we clean the sink first and unblock the queue if it's blocked
+        # prerolling
+        self._plugged_elements.append(sink)
+        self._plugged_elements.append(queue)
+        pad.link(queue.get_pad('sink'))
+        queue.link(sink)
+        queue.set_state(gst.STATE_PAUSED)
+        sink.set_state(gst.STATE_PAUSED)
+
+    def _get_pad_type(self, pad):
+        decoder = self._find_decoder(pad)
+        if decoder:
+            return self._get_type_from_decoder(decoder)
+
+        return pad.get_caps()[0].get_name().split('/', 1)[0]
+
+    def _get_pad_caps(self, pad):
+        decoder = self._find_decoder(pad)
+        if decoder:
+            return decoder.get_pad('sink').get_caps()
+
+        return pad.get_caps()
+
+    def _decodebin_new_decoded_pad_cb(self, decodebin, pad, is_last):
+        self.debug('new decoded pad %s, caps %s, is_last %s' % (pad,
+                pad.get_caps(), is_last))
+
+        typ = self._get_pad_type(pad)
+        caps = self._get_pad_caps(pad)
+
+        if typ == 'audio':
+            if self._audio_caps is None:
+                self._audio_caps = caps
+        elif typ == 'video':
+            if self._video_caps is None:
+                self._video_caps = caps
+                # do a thumbnail of the first video track
+        #        self._plug_video_thumbnailbin(pad)
+        elif typ == 'image':
+            if self._image_caps is None:
+                self._image_caps = caps
+        #        self._plug_image_thumbnailbin(pad)
+
+        if not pad.is_linked():
+            self._close_pad(pad)
+
+    def _decodebin_unknown_type_cb(self, decodebin, pad, caps):
+        self.debug('unknown pad %s, caps %s' % (pad, caps))
+
+    def _plug_src(self, uri):
+        src = gst.element_make_from_uri(gst.URI_SRC, str(uri))
+        # FIXME: workaround for jpegdec that does a gst_buffer_join for each
+        # gst_pad_chain.
+        #src.props.blocksize = 3 * 1024 * 1024
+
+        return src
+
+    def get_metadata(self, requested_metadata, callback):
+        #assert self._timeout_call is None
+
+        self._req_metadata = requested_metadata
+        self._req_uri = requested_metadata['uri']
+        #self._req_defer = defer.Deferred()
+        self._req_callback = callback
+
+        self.debug('getting metadata %s' % self._req_metadata)
+
+        self._start_timestamp = time.time()
+
+        self._src = self._plug_src(self._req_uri)
+        self._pipeline.add(self._src)
+        self._src.link(self._typefind)
+
+        #self._timeout_call = reactor.callLater(self.timeout, self._timeout)
+
+        # reset the bus in case this is not the first request
+        self._bus.set_flushing(False)
+        self._pipeline.set_state(gst.STATE_PLAYING)
+
+        #return self._req_defer
+
+    def _get_media_type_from_caps(self, caps):
+        res = {}
+        mime_type = caps[0].get_name()
+        file_type = mime_type.split('/', 1)[0]
+
+        return {'file_type': file_type, 'mime_type': mime_type}
+
+    def _done(self):
+        #if not self._timeout_call.called:
+        #    self._timeout_call.cancel()
+
+        # we can't check self._seek_call.called here because we don't know if we
+        # scheduled a seek call at all
+        #if self._seek_call is not None:
+        #    self._seek_call.cancel()
+        #    self._seek_call = None
+
+        self._end_timestamp = time.time()
+
+        metadata = self._req_metadata
+        metadata_callback = self._req_callback
+
+        available_metadata = {}
+        for caps in (self._video_caps, self._audio_caps,
+                self._image_caps):
+            if caps is not None:
+                available_metadata.update(self._get_media_type_from_caps(caps))
+                break
+
+        # fallback to typefind caps
+        if available_metadata.get('file_type') is None:
+            available_metadata['file_type'] = self._typefind_file_type
+            available_metadata['mime_type'] = self._typefind_mime_type
+
+        #if available_metadata['file_type'] in ('video', 'image') and \
+        #    self._have_thumbnail():
+        #    available_metadata['thumbnail'] = \
+        #            get_thumbnail_location(self._req_uri)
+
+        tags = self._tags
+
+        try:
+            del tags['extended-comment']
+        except KeyError:
+            pass
+
+        #tag_keys = tags.keys()
+        #for gst_key, elisa_key in (('track-number', 'track'),
+        #            ('title', 'song')):
+        #    try:
+        #        available_metadata[elisa_key] = tags[gst_key]
+        #    except KeyError:
+        #        pass
+
+        #for key in tag_keys:
+        #    value = tags[key]
+            # FIXME: this was an old assumption, let's keep it until we update
+            # all the old code
+        #    if isinstance(value, list):
+        #        try:
+        #            value = value[0]
+        #        except IndexError:
+        #            continue
+
+        #    available_metadata[key] = value
+
+        for tag_key in tags.keys():
+            available_metadata[tag_key] = tags[tag_key]
+
+        #for key, value in available_metadata.iteritems():
+        #    try:
+        #        if metadata[key] is None:
+        #            metadata[key] = value
+        #    except KeyError:
+        #        pass
+        metadata = available_metadata
+
+        self.debug('finished getting metadata %s, elapsed time %s' %
+                (metadata, self._end_timestamp - self._start_timestamp))
+
+        self._reset()
+        metadata_callback(metadata)
+
+    def _timeout(self, thumb_timeout=False):
+        self.debug('timeout thumb %s video caps %s',
+                thumb_timeout, self._video_caps)
+
+        if not thumb_timeout and (self._typefind_file_type == 'video' or
+                self._video_caps is not None):
+            # give some more time to the pipline if we are trying to make a
+            # thumbnail
+            #self._timeout_call = \
+            #    reactor.callLater(self.thumb_timeout, self._timeout, True)
+        #else:
+            self._clean_thumbnail()
+
+            keys = set(self._req_metadata.keys())
+            if keys != thumbnail_keys and \
+                    (self._typefind_file_type is not None or \
+                    self._video_caps is not None or \
+                    self._audio_caps is not None or \
+                    self._image_caps is not None):
+                # timeout while going to paused. This can happen on really slow
+                # machines while doing the thumbnail. Even if we didn't do the
+                # thumbnail, we have some clue about the media type here.
+                self._done()
+            else:
+                self._failed(TimeoutError('timeout'))
+
+    def _clean_thumbnail(self):
+        # if we fail doing a thumbnail we need to remove the file
+        if self._imgthumbbin is not None or self._videothumbbin is not None:
+            location = get_thumbnail_location(self._req_uri)
+            try:
+                os.unlink(location)
+            except OSError:
+                pass
+
+    def _failed(self, error):
+        # cancel delayed calls
+        #if not self._timeout_call.called:
+        #    self._timeout_call.cancel()
+
+        #if self._seek_call is not None:
+        #    self._seek_call.cancel()
+        #    self._seek_call = None
+
+        self._end_timestamp = time.time()
+
+        metadata = self._req_metadata
+        metadata_callback = self._req_callback
+        #self.debug('error getting metadata %s, error: %s, '
+        #        'elapsed time: %s, timeout %s' % (metadata, error,
+        #        self._end_timestamp - self._start_timestamp,
+        #        self._timeout_call.called))
+        self.debug('error getting metadata %s, error: %s, '
+                'elapsed time: %s' % (metadata, error,
+                self._end_timestamp - self._start_timestamp))
+
+
+        #self._clean_thumbnail()
+
+        self._reset()
+
+        metadata_callback(None)
+
+class GstMetadata:
+    def __init__(self):
+        self.queue = Queue()
+        self.pipeline = GstMetadataPipeline()
+        self.pipeline.initialize()
+
+    def get_metadata(self, uri):
+        self.pipeline.get_metadata({'uri': 'file://'+os.path.abspath(uri)}, self.queue.put)
+        metadata = self.queue.get()
+        return metadata

Added: trunk/conduit/utils/MediaFile.py
==============================================================================
--- (empty file)
+++ trunk/conduit/utils/MediaFile.py	Wed Aug 20 23:17:13 2008
@@ -0,0 +1,70 @@
+import conduit
+import conduit.datatypes.File as File
+import logging
+log = logging.getLogger("datatypes.Audio")
+
+import pygst
+pygst.require('0.10')
+import gst
+from gst.extend import discoverer
+import threading
+from threading import Lock
+
+class MediaFile(File.File):
+
+    def __init__(self, URI, **kwargs):
+        File.File.__init__(self, URI, **kwargs)
+
+    def _create_gst_metadata(self):
+        '''
+        Get metadata from GStreamer
+        '''
+        event = threading.Event()
+        def discovered(discoverer, valid):
+            if not valid:
+                log.debug("Media file not valid")
+                #FIXME: What exception should be raised here?
+                raise Exception
+            event.set()
+        # FIXME: Using Discoverer for now, but we should switch to utils.GstMetadata
+        #        when we get thumbnails working on it.
+        info = discoverer.Discoverer(self.get_local_uri())
+        info.connect('discovered', discovered)
+        info.discover()
+        # Wait for discover to finish (which is async and emits discovered)
+        event.wait()
+        tags = info.tags
+        if info.is_video:
+            tags['width'] = info.videowidth
+            tags['height'] = info.videoheight
+            tags['videorate'] = info.videorate
+            tags['duration'] = info.videolength / gst.MSECOND
+        if info.is_audio:
+            tags['duration'] = info.audiolength / gst.MSECOND
+            tags['samplerate'] = info.audiorate
+            tags['channels'] = info.audiochannels
+        return tags
+
+    def _get_metadata(self, name):
+        tags = self.get_media_tags()
+        if name in tags:
+            return tags[name]
+        else:
+            return None
+
+    def __getattr__(self, name):
+        # Get metadata only when needed
+        if name == 'gst_tags':
+            tags = self.gst_tags = self._create_gst_metadata()
+            return tags
+        else:
+            raise AttributeError
+
+    def get_media_tags(self):
+        '''
+        Get a dict containing all availiable metadata.
+
+        Descendants should override this function to provide their own tags,
+        or merge with these tags.
+        '''
+        return self.gst_tags

Modified: trunk/data/conduit.glade
==============================================================================
--- trunk/data/conduit.glade	(original)
+++ trunk/data/conduit.glade	Wed Aug 20 23:17:13 2008
@@ -664,6 +664,7 @@
     <child internal-child="vbox">
       <widget class="GtkVBox" id="configVBox">
         <property name="visible">True</property>
+        <property name="spacing">8</property>
         <child>
           <placeholder/>
         </child>

Modified: trunk/test/python-tests/TestCoreConvertAudioVideo.py
==============================================================================
--- trunk/test/python-tests/TestCoreConvertAudioVideo.py	(original)
+++ trunk/test/python-tests/TestCoreConvertAudioVideo.py	Wed Aug 20 23:17:13 2008
@@ -1,6 +1,11 @@
 from common import *
 
 import traceback
+import gobject
+import threading
+
+import gtk
+gtk.gdk.threads_init()
 
 import conduit.datatypes.File as File
 import conduit.datatypes.Video as Video
@@ -16,25 +21,37 @@
 
 TEST = (
 #name.list          #encodings to test  #available encodings
-("video",           ('divx',),          Video.PRESET_ENCODINGS      ),
+("video",           ('divx','flv','ogg'),          Video.PRESET_ENCODINGS      ),
 ("audio",           ('ogg',),           Audio.PRESET_ENCODINGS      ),
 )
 
-for name, test_encodings, all_encodings in TEST:
-    files = get_external_resources(name)
-    for description,uri in files.items():
-        f = File.File(uri)
-        ok("%s: File %s exists" % (name,uri), f.exists())
-        for encoding in test_encodings:
-            args = all_encodings[encoding]
-            ok("%s: Testing encoding of %s -> %s" % (name,description,encoding), True)
-            to_type = "file/%s?%s" % (name,Utils.encode_conversion_args(args))
-            try:
-                newdata = tc.convert("file",to_type, f)
-                ok("%s: Conversion OK" % name, newdata != None and newdata.exists(), False)
-            except Exceptions.ConversionError:
-                ok("%s: Conversion OK" % name, False, False)
+def convert():    
+    for name, test_encodings, all_encodings in TEST:
+        files = get_external_resources(name)
+        for description,uri in files.items():
+            f = File.File(uri)
+            ok("%s: File %s exists" % (name,uri), f.exists())
+            for encoding in test_encodings:
+                args = all_encodings[encoding]
+                ok("%s: Testing encoding of %s -> %s" % (name,description,encoding), True)
+                
+                to_type = "file/%s?%s" % (name,Utils.encode_conversion_args(args))
+                try:
+                    newdata = tc.convert("file",to_type, f)
+                    ok("%s: Conversion OK" % name, newdata != None and newdata.exists(), False)
+                except Exceptions.ConversionError:
+                    ok("%s: Conversion Failed" % name, False, False)
+    finished()           
+    mainloop.quit()
+    return False
+
+def idle_cb():
+    threading.Thread(target=convert).start()
+
+mainloop = gobject.MainLoop()
+gobject.idle_add(idle_cb)
+mainloop.run()
+
 
-finished()
 
 

Modified: trunk/test/python-tests/data/audio.list
==============================================================================
--- trunk/test/python-tests/data/audio.list	(original)
+++ trunk/test/python-tests/data/audio.list	Wed Aug 20 23:17:13 2008
@@ -13,3 +13,6 @@
 mp3=/home/john/testing/test-data/jonobacon-freesoftwaresong.mp3
 ogg=/home/john/testing/test-data/jonobacon-freesoftwaresong.ogg
 
+[airmind gemini]
+mp3=/home/airmind/Projects/gsoc08/jonobacon-freesoftwaresong.mp3
+ogg=/home/airmind/Projects/gsoc08/jonobacon-freesoftwaresong.ogg

Modified: trunk/test/python-tests/data/video.list
==============================================================================
--- trunk/test/python-tests/data/video.list	(original)
+++ trunk/test/python-tests/data/video.list	Wed Aug 20 23:17:13 2008
@@ -5,8 +5,8 @@
 #Note: Items in the default section should always be accessible
 #Note: If two items have the same key, the one in your section replaces
 #the one in the default section
-[DEFAULT]
-ogg=http://files.conduit-project.org/Conduit-0.3.0-screencast-small.ogg
+#[DEFAULT]
+#ogg=http://files.conduit-project.org/Conduit-0.3.0-screencast-small.ogg
 
 [john nzjrs-desktop]
 ogg=/home/john/testing/test-data/Conduit-0.3.0-screencast-small.ogg
@@ -15,3 +15,9 @@
 mov=ftp://anonymous 192 168 1 1/Disk-1/Videos/alternativefreedomtrailer.mov
 wmv=ftp://anonymous 192 168 1 1/Disk-1/Videos/Daily Show/tds-question-mark.wmv
 
+[airmind gemini]
+#flv=/home/airmind/Projects/gsoc08/pranks.flv
+avi=/home/airmind/Projects/gsoc08/teste.avi
+#avi2=/home/airmind/Projects/gsoc08/src.avi
+#ogg2=/home/airmind/Projects/gsoc08/src.ogg
+ogg=/home/airmind/Projects/gsoc08/Conduit-0.3.0-screencast-small.ogg



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]