conduit r1693 - in trunk: . conduit conduit/dataproviders conduit/datatypes conduit/gtkui conduit/hildonui conduit/modules conduit/modules/AmazonS3Module conduit/modules/GoogleModule conduit/modules/iPodModule conduit/utils data help help/de help/fr po scripts test/python-tests test/python-tests/data



Author: jstowers
Date: Fri Aug 29 23:38:28 2008
New Revision: 1693
URL: http://svn.gnome.org/viewvc/conduit?rev=1693&view=rev

Log:
Merge from trunk

Added:
   trunk/conduit/modules/AmazonS3Module/
   trunk/conduit/modules/AmazonS3Module/AmazonS3Module.py
   trunk/conduit/modules/AmazonS3Module/config.glade
   trunk/conduit/utils/GstMetadata.py
   trunk/conduit/utils/MediaFile.py
   trunk/data/amazon.png
   trunk/help/de/
   trunk/help/de/de.po
   trunk/help/fr/
   trunk/help/fr/fr.po
   trunk/po/pl.po
Modified:
   trunk/   (props changed)
   trunk/ChangeLog
   trunk/NEWS
   trunk/conduit/Vfs.py
   trunk/conduit/dataproviders/File.py
   trunk/conduit/datatypes/Audio.py
   trunk/conduit/datatypes/Video.py
   trunk/conduit/defs.py.in
   trunk/conduit/gtkui/Canvas.py
   trunk/conduit/gtkui/Database.py
   trunk/conduit/gtkui/SimpleConfigurator.py
   trunk/conduit/gtkui/UI.py
   trunk/conduit/hildonui/Canvas.py
   trunk/conduit/modules/AudioVideoConverterModule.py
   trunk/conduit/modules/GoogleModule/GoogleModule.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/help/ChangeLog
   trunk/help/Makefile.am
   trunk/po/ChangeLog
   trunk/po/LINGUAS
   trunk/po/ar.po
   trunk/po/es.po
   trunk/po/fi.po
   trunk/scripts/release.sh
   trunk/test/python-tests/TestCoreConvertAudioVideo.py
   trunk/test/python-tests/TestSyncFileFolder.py
   trunk/test/python-tests/data/audio.list
   trunk/test/python-tests/data/video.list

Modified: trunk/NEWS
==============================================================================
--- trunk/NEWS	(original)
+++ trunk/NEWS	Fri Aug 29 23:38:28 2008
@@ -1,5 +1,20 @@
 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:
+==============
+* Brown paper bag release
+
+* Fixed #546161, Remove unecessary scrollbar on Canvas (John Stowers)
+* Fixed #546273, Error syncing files with Flickr (John Stowers)
+* Fixed #546343, Web browser fails when installed (John Stowers)
+
+Translations:
+* Updated gl: Jorge Gonzalez
+* Updated es: Jorge Gonzalez
 
 NEW in 0.3.13:
 ==============

Modified: trunk/conduit/Vfs.py
==============================================================================
--- trunk/conduit/Vfs.py	(original)
+++ trunk/conduit/Vfs.py	Fri Aug 29 23:38:28 2008
@@ -48,7 +48,7 @@
     Joins multiple uri components. Performs safely if the first
     argument contains a uri scheme
     """
-    assert type(first) == str
+    first = _ensure_type(first)
     return os.path.join(first,*rest)
     #idx = first.rfind("://")
     #if idx == -1:
@@ -61,8 +61,8 @@
     """
     Returns the relative path fromURI --> toURI
     """
-    assert type(fromURI) == str
-    assert type(toURI) == str
+    fromURI = _ensure_type(fromURI)
+    toURI = _ensure_type(toURI)
     rel = toURI.replace(fromURI,"")
     #strip leading /
     if rel[0] == os.sep:

Modified: trunk/conduit/dataproviders/File.py
==============================================================================
--- trunk/conduit/dataproviders/File.py	(original)
+++ trunk/conduit/dataproviders/File.py	Fri Aug 29 23:38:28 2008
@@ -118,7 +118,8 @@
         for oid,uri,groupname in self.db.select("SELECT oid,URI,GROUP_NAME FROM config WHERE TYPE = ?",(TYPE_FOLDER,)):
             self.make_thread(
                     uri, 
-                    False,  #FIXME: Dont include hidden?
+                    False,  #include hidden
+                    False,  #follow symlinks
                     self._on_scan_folder_progress, 
                     self._on_scan_folder_completed, 
                     oid,

Modified: trunk/conduit/datatypes/Audio.py
==============================================================================
--- trunk/conduit/datatypes/Audio.py	(original)
+++ trunk/conduit/datatypes/Audio.py	Fri Aug 29 23:38:28 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	Fri Aug 29 23:38:28 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/defs.py.in
==============================================================================
--- trunk/conduit/defs.py.in	(original)
+++ trunk/conduit/defs.py.in	Fri Aug 29 23:38:28 2008
@@ -5,5 +5,14 @@
 LOCALE_DIR = "@LOCALEDIR@"
 SHARED_DATA_DIR = "@PKGDATADIR@"
 SHARED_MODULE_DIR = "@MODULEDIR@"
+
+#
+# Platform specific implementations
+#
+
+#{gtkmozembed, webkit, system}
+BROWSER_IMPL = "gtkmozembed"
+
+#{GConf,Python}
 SETTINGS_IMPL = "GConf"
 

Modified: trunk/conduit/gtkui/Canvas.py
==============================================================================
--- trunk/conduit/gtkui/Canvas.py	(original)
+++ trunk/conduit/gtkui/Canvas.py	Fri Aug 29 23:38:28 2008
@@ -294,6 +294,9 @@
         if idx != -1:
             self.root.remove_child(idx)
         self.welcome = None
+
+    def _resize_welcome(self, width):
+        self.welcome.set_width(width)
         
     def _create_welcome(self):
         c_x,c_y,c_w,c_h = self.get_bounds()
@@ -341,6 +344,10 @@
                     allocation.width,
                     self._get_minimum_canvas_size(allocation.height)
                     )
+
+        if self.welcome:
+            self._resize_welcome(allocation.width)
+
         for i in self._get_child_conduit_canvas_items():
             i.set_width(allocation.width)
 
@@ -427,7 +434,8 @@
             allocH = self.get_allocation().height
     
         bottom = self._get_bottom_of_conduits_coord()
-        return max(bottom + ConduitCanvasItem.WIDGET_HEIGHT + 20, allocH)
+        #return allocH-1 to stop vertical scroll bar
+        return max(bottom + ConduitCanvasItem.WIDGET_HEIGHT + 20, allocH-1)
         
     def _remove_overlap(self):
         """
@@ -837,6 +845,7 @@
     
 class ConduitCanvasItem(_CanvasItem):
 
+    BUTTONS = False
     DIVIDER = False
     FLAT_BOX = True
     WIDGET_HEIGHT = 63.0
@@ -865,6 +874,10 @@
         #goocanvas.Points need a list of tuples, not a list of lists. Yuck
         self.dividerPoints = [(),()]
 
+        #if self.BUTTONS, show sync and stop buttons
+        self.syncButton = None
+        self.stopButton = None
+
         #Build the widget
         self._build_widget(width)
 
@@ -938,6 +951,38 @@
                                     )
             self.add_child(self.divider)
 
+        if self.BUTTONS and self.model:
+            w = gtk.Button(label="")
+            w.set_image(
+                gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU)
+                )
+            w.set_relief(gtk.RELIEF_HALF)
+            self.syncButton = goocanvas.Widget(
+                                widget=w,
+                                x=true_width-19,
+                                y=22,
+                                width=28,
+                                height=28,
+                                anchor=gtk.ANCHOR_CENTER
+                                )
+            self.add_child(self.syncButton)
+
+            w = gtk.Button(label="")
+            w.set_image(
+                gtk.image_new_from_stock(gtk.STOCK_MEDIA_STOP, gtk.ICON_SIZE_MENU)
+                )
+            w.set_relief(gtk.RELIEF_HALF)
+            self.stopButton = goocanvas.Widget(
+                                widget=w,
+                                x=true_width-19,
+                                y=22+2+28,
+                                width=28,
+                                height=28,
+                                anchor=gtk.ANCHOR_CENTER
+                                )
+            self.add_child(self.stopButton)
+
+
     def _resize_height(self):
         sourceh =   0.0
         sinkh =     0.0
@@ -1155,7 +1200,7 @@
             self.dividerPoints[0] = (self.dividerPoints[0][0],h+10)
             self.dividerPoints[1] = (self.dividerPoints[0][0],h+10)
             self.divider.set_property("points", 
-                                goocanvas.Points(self.dividerPoints))        
+                                goocanvas.Points(self.dividerPoints))
 
     def set_width(self, w):
         true_width = w-self.LINE_WIDTH
@@ -1168,6 +1213,10 @@
             self.divider.set_property("points", 
                                 goocanvas.Points(self.dividerPoints))
 
+        #if self.BUTTONS:
+        #    self.syncButton.set_property("x", true_width-19)
+        #    self.stopButton.set_property("x", true_width-19)
+
         #resize the spacer
         p = goocanvas.Points([(0.0, 0.0), (true_width, 0.0)])
         self.l.set_property("points",p)

Modified: trunk/conduit/gtkui/Database.py
==============================================================================
--- trunk/conduit/gtkui/Database.py	(original)
+++ trunk/conduit/gtkui/Database.py	Fri Aug 29 23:38:28 2008
@@ -60,23 +60,35 @@
     def _on_inserted(self, db, oid):
         self.oidcache = []
         offset = self._get_offset(oid)
-        rowref = self.get_iter(offset)
-        path = self.get_path(rowref)
-        self.row_inserted(path, rowref)
+        try:
+            rowref = self.get_iter(offset)
+            path = self.get_path(rowref)
+            self.row_inserted(path, rowref)
+        except ValueError:
+            #not a valid rowref
+            pass
                 
     def _on_modified(self, db, oid):
         self.oidcache = []
         offset = self._get_offset(oid)
-        rowref = self.get_iter(offset)
-        path = self.get_path(rowref)
-        self.row_changed(path, rowref)
+        try:
+            rowref = self.get_iter(offset)
+            path = self.get_path(rowref)
+            self.row_changed(path, rowref)
+        except ValueError:
+            #not a valid rowref
+            pass
         
     def _on_deleted(self, db, oid):
         self.oidcache = []
         offset = self._get_offset(oid)
-        rowref = self.get_iter(offset)
-        path = self.get_path(rowref)
-        self.row_deleted(path)
+        try:
+            rowref = self.get_iter(offset)
+            path = self.get_path(rowref)
+            self.row_deleted(path)
+        except ValueError:
+            #not a valid rowref
+            pass
 
     def _get_n_rows(self):
         """

Modified: trunk/conduit/gtkui/SimpleConfigurator.py
==============================================================================
--- trunk/conduit/gtkui/SimpleConfigurator.py	(original)
+++ trunk/conduit/gtkui/SimpleConfigurator.py	Fri Aug 29 23:38:28 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	Fri Aug 29 23:38:28 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/hildonui/Canvas.py
==============================================================================
--- trunk/conduit/hildonui/Canvas.py	(original)
+++ trunk/conduit/hildonui/Canvas.py	Fri Aug 29 23:38:28 2008
@@ -47,6 +47,13 @@
         self.dataproviderMenu = DataProviderMenu(self)
         # conduit context menu
         self.conduitMenu = ConduitMenu(self)
+
+    def _resize_welcome(self, width):
+        self.welcome.set_properties(
+                            x=width/2, 
+                            y=width/3, 
+                            width=3*width/5
+                            )
         
     def _create_welcome(self):
         c_x,c_y,c_w,c_h = self.get_bounds()
@@ -330,6 +337,7 @@
 
 class ConduitCanvasItem(conduit.gtkui.Canvas.ConduitCanvasItem):
 
+    BUTTONS = False
     FLAT_BOX = False
     DIVIDER = False
     LINE_WIDTH = 3.0

Added: trunk/conduit/modules/AmazonS3Module/AmazonS3Module.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/AmazonS3Module/AmazonS3Module.py	Fri Aug 29 23:38:28 2008
@@ -0,0 +1,369 @@
+"""
+C{AmazonS3TwoWay} module for synchronizing with I{Amazon Simple Storage
+Service} (Amazon S3).
+
+Uses URI relative to base path as LUID. The LUID is also used as key on Amazon
+S3.
+"""
+import logging
+log = logging.getLogger("modules.AmazonS3")
+
+import conduit
+import conduit.Exceptions as Exceptions
+import conduit.utils as Utils
+import conduit.datatypes.File as File
+import conduit.dataproviders.DataProvider as DataProvider
+
+from boto.s3.connection import S3Connection
+from boto.s3.key import Key
+
+MODULES = {"AmazonS3TwoWay" : {"type": "dataprovider"}}
+
+class AmazonS3TwoWay(DataProvider.TwoWay):
+    """
+    TwoWay dataprovider for synchronizing files with Amazon S3 and vice-versa.
+    """
+
+    _name_ = "Amazon S3"
+    _description_ = "Sync with Amazon S3"
+    _category_ = conduit.dataproviders.CATEGORY_FILES
+    _module_type_ = "twoway"
+    _in_type_ = "file"
+    _out_type_ = "file"
+    _icon_ = "amazon"
+    _configurable_ = True
+
+    # default values for class variables (used by self.set_configuration())
+    DEFAULT_AWS_ACCESS_KEY = None
+    DEFAULT_AWS_SECRET_ACCESS_KEY = None
+    DEFAULT_BUCKET_NAME = ""
+    DEFAULT_USE_SSL = True
+
+    # set expire time for AWS https links
+    AWS_URL_EXPIRE_SECONDS = 60 * 15
+
+    def __init__(self):
+        """
+        Call base constructor and initialize all variables that are restored
+        from configuration.
+        """
+        DataProvider.TwoWay.__init__(self)
+
+        # configured AWS Access Key
+        self.aws_access_key = AmazonS3TwoWay.DEFAULT_AWS_ACCESS_KEY
+        # configured AWS Secret Access Key
+        self.aws_secret_access_key = \
+            AmazonS3TwoWay.DEFAULT_AWS_SECRET_ACCESS_KEY
+        # configured name of Amazon S3 bucket
+        self.bucket_name = AmazonS3TwoWay.DEFAULT_BUCKET_NAME
+        # configuration value determining use of SSL for Amazon S3 connection
+        self.use_ssl = AmazonS3TwoWay.DEFAULT_USE_SSL
+        # remote keys (equivalent to LUIDs)
+        self.keys = []
+        # for caching S3Connection object
+        self.connection = None
+        # for caching Bucket object
+        self.bucket = None
+
+    def _data_exists(self, LUID):
+        """
+        @returns: C{True} if data at the LUID exists, else C{False}.
+        """
+        return self.bucket.get_key(LUID) != None
+
+    def _get_proxyfile(self, key):
+        """
+        @param key: Key for which C{ProxyFile} should be returned.
+        @type key: C{boto.s3.key.Key}
+        @returns: C{ProxyFile} stored under supplied parameter C{key}.
+        """
+        httpurl = key.generate_url(AmazonS3TwoWay.AWS_URL_EXPIRE_SECONDS)
+        # BUG This will fail with "Access denied"
+        # (see http://bugzilla.gnome.org/show_bug.cgi?id=545000)
+        return File.ProxyFile(
+            httpurl,
+            key.name,
+            Utils.datetime_from_timestamp(long(key.get_metadata("mtime"))),
+            long(key.get_metadata("size")))
+
+    def _get_data(self, LUID):
+        """
+        @returns: ProxyFile object containing remote data with the specified
+        LUID.
+        """
+        key = self.bucket.get_key(LUID)
+        return self._get_proxyfile(key)
+
+    def _put_data(self, localfile):
+        """
+        Uploads the given File object to Amazon S3 and returns its record
+        identifier (Rid).
+
+        @returns: Rid of uploaded file.
+        """
+        filename = localfile.get_relative_uri()
+        key = Key(self.bucket)
+        # the key's name is the LUID
+        key.name = filename
+        # add a bit of metadata to key
+        # TODO store more metadata: file permissions and owner:group?
+        key.set_metadata("size", str(localfile.get_size()))
+        key.set_metadata(
+            "mtime", str(Utils.datetime_get_timestamp(localfile.get_mtime())))
+
+        # now upload the data
+        key.set_contents_from_filename(localfile.get_local_uri())
+
+        # return Rid of uploaded file
+        return self._get_proxyfile(key).get_rid()
+
+    def _replace_data(self, LUID, localfile):
+        """
+        Replaces the remote file identified by LUID with given file object.
+        """
+        # We don't assign a new LUID when replacing the file, so we can call
+        # self._put_data()
+        return self._put_data(localfile)
+
+    def _set_aws_access_key(self, key):
+        """
+        Sets variable C{self.aws_access_key} to given value.
+        """
+        # set to None if param is the empty string so that boto can figure
+        # out the access key by config file or environment variable
+        if key == "" or key == "None":
+            key = None
+        if self.aws_access_key != key:
+            self.aws_access_key = key
+            # reset connection when configuration changes
+            self.connection = None
+
+    def _set_aws_secret_access_key(self, key):
+        """
+        Sets variable C{self.aws_secret_access_key} to given value.
+        """
+        # set to None if param is the empty string so that boto can figure
+        # out the access key by config file or environment variable
+        if key == "" or key == "None":
+            key = None
+        if self.aws_secret_access_key != key:
+            self.aws_secret_access_key = key
+            # reset connection when configuration changes
+            self.connection = None
+
+    def _set_bucket_name(self, name):
+        """
+        Sets variable C{self.bucket_name} to given value.
+
+        @param name: Bucket name that C{self.bucket_name} shall be set to.
+        @type name: C{str}
+        """
+        name = str(name)
+        if self.bucket_name != name:
+            self.bucket_name = name
+            # reset bucket when configuration changes
+            self.bucket = None
+
+    def _set_use_ssl(self, use_ssl):
+        """
+        Sets variable C{self.use_ssl}.
+
+        @param use_ssl: C{True} if a secure connection should be used for
+                        communication with Amazon S3, C{False} otherwise.
+        @type use_ssl: C{bool}
+        """
+        self.use_ssl = bool(use_ssl)
+
+    def configure(self, window):
+        """
+        Show configuration dialog for this module.
+
+        @param window: The parent window (used for modal dialogs)
+        @type window: C{gtk.Window}
+        """
+        # lazily import gtk so if conduit is run from command line or a non
+        # gtk system, this module will still load. There should be no need
+        # to use gtk outside of this function
+        import gtk
+
+        def on_dialog_response(sender, response_id):
+            """
+            Response handler for configuration dialog.
+            """
+            if response_id == gtk.RESPONSE_OK:
+                self._set_aws_access_key(access_key_entry.get_text())
+                self._set_aws_secret_access_key(
+                    secret_access_key_entry.get_text())
+                self._set_bucket_name(bucket_name_entry.get_text())
+                self._set_use_ssl((True, False)[ssl_combo_box.get_active()])
+
+        tree = Utils.dataprovider_glade_get_widget(__file__,
+                                                   "config.glade",
+                                                   "AmazonS3ConfigDialog")
+
+        # get widgets
+        dialog = tree.get_widget("AmazonS3ConfigDialog")
+        access_key_entry = tree.get_widget("accessKey")
+        secret_access_key_entry = tree.get_widget("secretAccessKey")
+        bucket_name_entry = tree.get_widget("bucketName")
+        ssl_combo_box = tree.get_widget("useSsl")
+
+        # set values of widgets
+        access_key_entry.set_text(
+            (self.aws_access_key, "")[self.aws_access_key == None])
+        secret_access_key_entry.set_text((self.aws_secret_access_key, "")
+                                         [self.aws_secret_access_key == None])
+        bucket_name_entry.set_text(self.bucket_name)
+        ssl_combo_box.set_active((1, 0)[self.use_ssl])
+
+        # show dialog
+        Utils.run_dialog_non_blocking(dialog, on_dialog_response, window)
+
+    def _connect(self):
+        """
+        Connect to Amazon S3 if not already connected and makes sure that
+        variable C{self.connection} holds a valid C{S3Connection} object.
+        """
+        if self.connection != None:
+            log.debug("Already connected to Amazon S3.")
+        else:
+            log.debug("Connecting to Amazon S3.")
+            self.connection = S3Connection(self.aws_access_key,
+                                           self.aws_secret_access_key,
+                                           is_secure=self.use_ssl)
+
+    def _set_bucket(self):
+        """
+        Makes sure that variable C{self.bucket} holds a valid C{Bucket} object.
+        """
+        self._connect()
+        if self.bucket != None and self.bucket.name == self.bucket_name:
+            log.debug("Already have bucket (name = '%s')." % self.bucket.name)
+        else:
+            log.debug("Getting bucket named '%s'." % self.bucket_name)
+            # create or get configured bucket
+            # BUG this will fail with environment variable LC_TIME != "en"
+            # (see http://code.google.com/p/boto/issues/detail?id=140)
+            self.bucket = self.connection.create_bucket(self.bucket_name)
+
+    def refresh(self):
+        """
+        Connects to Amazon S3 if necessary and gets the name of all keys in the
+        configured bucket.
+        """
+        DataProvider.TwoWay.refresh(self)
+        self._set_bucket()
+        # Get LUIDs of all remote files (the keys of the remote files are the
+        # LUIDs)
+        self.keys = [key.name for key in self.bucket]
+
+    def get_all(self):
+        """
+        Returns the key names (LUIDs) of all remote files.
+        @return: A list of string LUIDs.
+        """
+        DataProvider.TwoWay.get_all(self)
+        # refresh() has been called previously, so we can just return self.keys
+        return self.keys
+
+    def get(self, LUID):
+        """
+        Stores remote file identified by supplied LUID locally and returns the
+        corresponding File object.
+        @param LUID: A LUID which uniquely represents data to return.
+        @type LUID: C{str}
+        """
+        DataProvider.TwoWay.get(self, LUID)
+        data = self._get_data(LUID)
+        data.force_new_filename(LUID)
+        # datatypes can be shared between modules. For this reason it is
+        # necessary to explicity set parameters like the LUID
+        data.set_UID(LUID)
+        return data
+
+    def put(self, localfile, overwrite, LUID):
+        """
+        Stores the given File object remotely on Amazon S3, if certain
+        conditions are met.
+        @returns: The Rid of the page at location LUID.
+        """
+        DataProvider.TwoWay.put(self, localfile, overwrite, LUID)
+        # If LUID is None, then we have once-upon-a-time uploaded this file
+        if LUID != None:
+            # Check if the remote file exists (i.e. has it been deleted)
+            if self._data_exists(LUID):
+                # The remote file exists
+                if not overwrite:
+                    # Only replace the data if it is newer than the remote one
+                    remotefile = self._get_data(LUID)
+                    comp = localfile.compare(remotefile)
+                    if comp == conduit.datatypes.COMPARISON_NEWER:
+                        return self._replace_data(LUID, localfile)
+                    elif comp == conduit.datatypes.COMPARISON_EQUAL:
+                        # We are the same, so return either rid
+                        return remotefile.get_rid()
+                    else:
+                        # If we are older than the remote page, or if the two
+                        # could not be compared, then we must ask the user what
+                        # to do via a conflict
+                        raise Exceptions.SynchronizeConflictError(comp,
+                                                                  localfile,
+                                                                  remotefile)
+
+        # If we get here then the file is new
+        return self._put_data(localfile)
+
+    def delete(self, LUID):
+        """
+        Delete remote file identified by given LUID.
+        """
+        DataProvider.TwoWay.delete(self, LUID)
+        # delete remote file
+        self.bucket.delete_key(LUID)
+
+    def get_configuration(self):
+        """
+        Returns a dict of key-value pairs. Key is the name of an internal
+        variable, and value is its current value to save.
+
+        It is important the the key is the actual name (minus the self.) of the
+        internal variable that should be restored when the user saves
+        their settings.
+        """
+        return {"aws_access_key" : self.aws_access_key,
+                "aws_secret_access_key" : self.aws_secret_access_key,
+                "bucket_name" : self.bucket_name,
+                "use_ssl" : self.use_ssl}
+
+    def set_configuration(self, config):
+        """
+        If you override this function then you are responsible for
+        checking the sanity of values in the config dict, including setting
+        any instance variables to sane defaults
+        """
+        self._set_aws_access_key(
+            config.get("aws_access_key", AmazonS3TwoWay.DEFAULT_AWS_ACCESS_KEY))
+        self._set_aws_secret_access_key(
+            config.get("aws_secret_access_key",
+                       AmazonS3TwoWay.DEFAULT_AWS_SECRET_ACCESS_KEY))
+        self._set_bucket_name(
+            config.get("bucket_name", AmazonS3TwoWay.DEFAULT_BUCKET_NAME))
+        self._set_use_ssl(config.get("use_ssl", AmazonS3TwoWay.DEFAULT_USE_SSL))
+
+    def is_configured(self, isSource, isTwoWay):
+        """
+        @returns: C{True} if this instance has been correctly configured and
+        data can be retrieved/stored into it, else C{False}.
+        """
+        # Below we also check if the AWS access and secret access key is set.
+        # boto is able to retrieve these values from its own config file or
+        # from environment variables, and we effectively disable this behavior
+        # by checking if these keys are set.
+        return self.bucket_name != None and self.aws_access_key != None and \
+            self.aws_secret_access_key != None
+
+    def get_UID(self):
+        """
+        @returns: A string uniquely representing this dataprovider.
+        """
+        return self.aws_access_key + self.bucket_name

Added: trunk/conduit/modules/AmazonS3Module/config.glade
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/AmazonS3Module/config.glade	Fri Aug 29 23:38:28 2008
@@ -0,0 +1,185 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
+<!--*- mode: xml -*-->
+<glade-interface>
+  <widget class="GtkDialog" id="AmazonS3ConfigDialog">
+    <property name="visible">True</property>
+    <property name="title" translatable="yes">Amazon S3</property>
+    <property name="resizable">False</property>
+    <property name="default_width">250</property>
+    <property name="default_height">300</property>
+    <property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
+    <child internal-child="vbox">
+      <widget class="GtkVBox" id="AmazonS3ConfigBox">
+        <property name="visible">True</property>
+        <property name="spacing">5</property>
+        <child>
+          <widget class="GtkLabel" id="accountDetailsLabel">
+            <property name="visible">True</property>
+            <property name="label" translatable="yes">&lt;b&gt;Account Details&lt;/b&gt;</property>
+            <property name="use_markup">True</property>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">2</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkLabel" id="accessKeyLabel">
+            <property name="visible">True</property>
+            <property name="xalign">0</property>
+            <property name="label" translatable="yes">AWS Access Key:</property>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">3</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkEntry" id="accessKey">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="invisible_char">â</property>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">4</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkLabel" id="secretAccessKeyLabel">
+            <property name="visible">True</property>
+            <property name="xalign">0</property>
+            <property name="label" translatable="yes">AWS Secret Access Key:</property>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">5</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkEntry" id="secretAccessKey">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="visibility">False</property>
+            <property name="invisible_char">â</property>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">6</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkLabel" id="storageLocationLabel">
+            <property name="visible">True</property>
+            <property name="label" translatable="yes">&lt;b&gt;Storage Location&lt;/b&gt;</property>
+            <property name="use_markup">True</property>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">7</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkLabel" id="bucketNameLabel">
+            <property name="visible">True</property>
+            <property name="xalign">0</property>
+            <property name="label" translatable="yes">Bucket name:</property>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">8</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkEntry" id="bucketName">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">9</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkLabel" id="connectionSettingsLabel">
+            <property name="visible">True</property>
+            <property name="label" translatable="yes">&lt;b&gt;Connection Settings&lt;/b&gt;</property>
+            <property name="use_markup">True</property>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">10</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkLabel" id="useSslLabel">
+            <property name="visible">True</property>
+            <property name="xalign">0</property>
+            <property name="label" translatable="yes">Use SSL:</property>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">11</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkComboBox" id="useSsl">
+            <property name="visible">True</property>
+            <property name="active">0</property>
+            <property name="items" translatable="yes">Yes
+No</property>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">12</property>
+          </packing>
+        </child>
+        <child internal-child="action_area">
+          <widget class="GtkHButtonBox" id="okCancelButtonBox">
+            <property name="visible">True</property>
+            <property name="layout_style">GTK_BUTTONBOX_END</property>
+            <child>
+              <widget class="GtkButton" id="cancelButton">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="can_default">True</property>
+                <property name="label">gtk-cancel</property>
+                <property name="use_stock">True</property>
+                <property name="response_id">-6</property>
+              </widget>
+            </child>
+            <child>
+              <widget class="GtkButton" id="okButton">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="can_default">True</property>
+                <property name="label">gtk-ok</property>
+                <property name="use_stock">True</property>
+                <property name="response_id">-5</property>
+              </widget>
+              <packing>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="pack_type">GTK_PACK_END</property>
+          </packing>
+        </child>
+      </widget>
+    </child>
+  </widget>
+</glade-interface>

Modified: trunk/conduit/modules/AudioVideoConverterModule.py
==============================================================================
--- trunk/conduit/modules/AudioVideoConverterModule.py	(original)
+++ trunk/conduit/modules/AudioVideoConverterModule.py	Fri Aug 29 23:38:28 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(**kwargs)
+            kwargs['pass'] = 2
+            return self._run_pipeline(**kwargs)
+        else:
+            return self._run_pipeline(**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/GoogleModule/GoogleModule.py
==============================================================================
--- trunk/conduit/modules/GoogleModule/GoogleModule.py	(original)
+++ trunk/conduit/modules/GoogleModule/GoogleModule.py	Fri Aug 29 23:38:28 2008
@@ -609,15 +609,20 @@
         except Exception, e:
             raise Exceptions.SyncronizeError("Picasa Update Error.")
 
-    def _get_album(self):
+    def _find_album(self):
         for name,album in self._get_albums():
             if name == self.albumName:
                 log.debug("Found album %s" % self.albumName)
-                self.galbum = album
-                return
+                return album
+
+        return None
 
-        log.debug("Creating new album %s." % self.albumName)
-        self.galbum = self._create_album(self.albumName)
+    def _get_album(self):
+        self.galbum = self._find_album()
+        if not self.galbum:
+            log.debug("Creating new album %s." % self.albumName)
+            self._create_album(self.albumName)
+            self.galbum = self._find_album()
 
     def _get_albums(self):
         albums = []

Modified: trunk/conduit/modules/TestModule.py
==============================================================================
--- trunk/conduit/modules/TestModule.py	(original)
+++ trunk/conduit/modules/TestModule.py	Fri Aug 29 23:38:28 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	Fri Aug 29 23:38:28 2008
@@ -0,0 +1 @@
+

Modified: trunk/conduit/modules/iPodModule/config.glade
==============================================================================
--- trunk/conduit/modules/iPodModule/config.glade	(original)
+++ trunk/conduit/modules/iPodModule/config.glade	Fri Aug 29 23:38:28 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	Fri Aug 29 23:38:28 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	Fri Aug 29 23:38:28 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	Fri Aug 29 23:38:28 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

Added: trunk/data/amazon.png
==============================================================================
Binary files (empty file) and trunk/data/amazon.png	Fri Aug 29 23:38:28 2008 differ

Modified: trunk/data/conduit.glade
==============================================================================
--- trunk/data/conduit.glade	(original)
+++ trunk/data/conduit.glade	Fri Aug 29 23:38:28 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/help/Makefile.am
==============================================================================
--- trunk/help/Makefile.am	(original)
+++ trunk/help/Makefile.am	Fri Aug 29 23:38:28 2008
@@ -4,7 +4,7 @@
 DOC_MODULE = conduit
 DOC_ENTITIES = 
 DOC_INCLUDES = 
-DOC_LINGUAS = es
+DOC_LINGUAS = de es fr
 
 DOC_FIGURES = \
     figures/conduit-login.png \

Modified: trunk/po/LINGUAS
==============================================================================
--- trunk/po/LINGUAS	(original)
+++ trunk/po/LINGUAS	Fri Aug 29 23:38:28 2008
@@ -13,6 +13,7 @@
 nb
 oc
 pa
+pl
 pt
 pt_BR
 ru

Modified: trunk/scripts/release.sh
==============================================================================
--- trunk/scripts/release.sh	(original)
+++ trunk/scripts/release.sh	Fri Aug 29 23:38:28 2008
@@ -6,7 +6,7 @@
 fi
 
 ./scripts/maintainer.py \
-    --revision=0.3.13 \
+    --revision=0.3.13.1 \
     --package-name=Conduit \
     --package-version=0.3.14 \
     --package-module=conduit \

Modified: trunk/test/python-tests/TestCoreConvertAudioVideo.py
==============================================================================
--- trunk/test/python-tests/TestCoreConvertAudioVideo.py	(original)
+++ trunk/test/python-tests/TestCoreConvertAudioVideo.py	Fri Aug 29 23:38:28 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/TestSyncFileFolder.py
==============================================================================
--- trunk/test/python-tests/TestSyncFileFolder.py	(original)
+++ trunk/test/python-tests/TestSyncFileFolder.py	Fri Aug 29 23:38:28 2008
@@ -12,12 +12,12 @@
 
 #Repeat syncs a few times to check for duplicate mapping bugs, etc
 SYNC_N_TIMES = 3
-#Num files to start with, should end with 150% of this number
-NUM_FILES = 10
+#Num files to start with
+NUM_FILES = 20
 #Sleep time for file I/O
 SLEEP_TIME = 1
 #Print the mapping DB on the last sync?
-PRINT_MAPPING_DB = True
+PRINT_MAPPING_DB = False
 
 #setup test
 test = SimpleSyncTest()
@@ -28,24 +28,42 @@
 
 #prepare the test data
 sourceDir = os.path.join(os.environ['TEST_DIRECTORY'],"filesource")
+sourceFolder = os.path.join(os.environ['TEST_DIRECTORY'],"filesourcefolder")
 sinkDir = os.path.join(os.environ['TEST_DIRECTORY'],"foldersink")
 if not os.path.exists(sourceDir):
     os.mkdir(sourceDir)
 if not os.path.exists(sinkDir):
     os.mkdir(sinkDir)
+if not os.path.exists(sourceFolder):
+    os.mkdir(sourceFolder)
+
 
 FILES = []
-for i in range(0, NUM_FILES):
+
+#add some plain files to the dp
+for i in range(0, NUM_FILES/2):
     name = Utils.random_string()
     contents = Utils.random_string()
     f = File.TempFile(contents)
     f.force_new_filename(name)
     f.transfer(sourceDir, True)
-    FILES.append((name,contents))
+    FILES.append((name,contents,sourceDir,"",""))
+plainFiles = [os.path.join(sourceDir, name) for name,contents,i,j,k in FILES]
+
+#also add a plain folder, containg some more files
+FOLDER_GRP_NAME = "i-am-a-folder"
+for i in range(0, NUM_FILES/2):
+    name = Utils.random_string()
+    contents = Utils.random_string()
+    f = File.TempFile(contents)
+    f.force_new_filename(name)
+    f.transfer(sourceFolder, True)
+    FILES.append((name,contents,sourceFolder,"",FOLDER_GRP_NAME))
 
 #configure the source
 config = {}
-config["files"] = [os.path.join(sourceDir, name) for name,contents in FILES]
+config["files"] = plainFiles
+config["folders"] = ["file://%s---FIXME---%s" % (sourceFolder,FOLDER_GRP_NAME)]
 test.configure(source=config)
 
 #configure the sink
@@ -70,7 +88,7 @@
 
     mapSource2Sink = conduit.GLOBALS.mappingDB.get_mappings_for_dataproviders(sourceW.get_UID(), sinkW.get_UID())
     mapSink2Source = conduit.GLOBALS.mappingDB.get_mappings_for_dataproviders(sinkW.get_UID(), sourceW.get_UID())
-    ok("Oneway Sync: 10 Mappings source -> sink", len(mapSource2Sink) == 10 and len(mapSink2Source) == 0)
+    ok("Oneway Sync: %s Mappings source -> sink" % NUM_FILES, len(mapSource2Sink) == NUM_FILES and len(mapSink2Source) == 0)
 
 #two way sync
 test.set_two_way_sync(True)
@@ -84,20 +102,24 @@
 
     mapSource2Sink = conduit.GLOBALS.mappingDB.get_mappings_for_dataproviders(sourceW.get_UID(), sinkW.get_UID())
     mapSink2Source = conduit.GLOBALS.mappingDB.get_mappings_for_dataproviders(sinkW.get_UID(), sourceW.get_UID())
-    ok("Sync: 10 Mappings in total", len(mapSource2Sink + mapSink2Source) == 10)
+    ok("Sync: %s Mappings in total", len(mapSource2Sink + mapSink2Source) == NUM_FILES)
 
-for name,contents in FILES:
-    f1 = File.File(os.path.join(sourceDir, name))
-    f2 = File.File(os.path.join(sinkDir, name))
+#check the plain files
+for name,contents,sourceDir,sourceRelPath, sinkRelPath in FILES:
+    f1 = File.File(os.path.join(sourceDir, sourceRelPath, name))
+    f2 = File.File(os.path.join(sinkDir, sinkRelPath, name))
     comp = f1.compare(f2)
-    ok("Sync: checking source/%s == sink/%s" % (name, name),comp == COMPARISON_EQUAL)
+    ok("Sync: checking %s == %s" % (f1._get_text_uri(), f2._get_text_uri()),comp == COMPARISON_EQUAL)
 
 #Now delete half the files
-for i in range(0, NUM_FILES/2):
-    name, contents = FILES[i]
-    path = os.path.join(sourceDir, name)
-    del(FILES[i])
+d = []
+for i in range(0, NUM_FILES, 2):
+    name, contents, sourceDir, sourceRelPath, sinkRelPath = FILES[i]
+    path = os.path.join(sourceDir, sourceRelPath, name)
     os.remove(path)
+    d.append(FILES[i])
+for i in d:
+    FILES.remove(i)
 
 #some IO time
 time.sleep(SLEEP_TIME)
@@ -114,13 +136,12 @@
     ok("Delete: Files were deleted (%s,%s,%s)" % (a,b,len(FILES)), a==len(FILES) and b==len(FILES))
     mapSource2Sink = conduit.GLOBALS.mappingDB.get_mappings_for_dataproviders(sourceW.get_UID(), sinkW.get_UID())
     mapSink2Source = conduit.GLOBALS.mappingDB.get_mappings_for_dataproviders(sinkW.get_UID(), sourceW.get_UID())
-    ok("Delete: 5 Mappings in total", len(mapSource2Sink) == 5 and len(mapSink2Source) == 0)
-
+    ok("Delete: %s Mappings in total" % (NUM_FILES/2), len(mapSource2Sink) == (NUM_FILES/2) and len(mapSink2Source) == 0)
 
-for name,contents in FILES:
-    f1 = File.File(os.path.join(sourceDir, name))
-    f2 = File.File(os.path.join(sinkDir, name))
+for name,contents,sourceDir,sourceRelPath, sinkRelPath in FILES:
+    f1 = File.File(os.path.join(sourceDir, sourceRelPath, name))
+    f2 = File.File(os.path.join(sinkDir, sinkRelPath, name))
     comp = f1.compare(f2)
-    ok("Delete: checking source/%s == sink/%s" % (name, name),comp == COMPARISON_EQUAL)
+    ok("Delete: checking %s == %s" % (f1._get_text_uri(), f2._get_text_uri()),comp == COMPARISON_EQUAL)
 
 finished()

Modified: trunk/test/python-tests/data/audio.list
==============================================================================
--- trunk/test/python-tests/data/audio.list	(original)
+++ trunk/test/python-tests/data/audio.list	Fri Aug 29 23:38:28 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	Fri Aug 29 23:38:28 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]