conduit r1852 - in trunk: . conduit/modules/iPodModule conduit/utils test/python-tests test/python-tests/data/resources-ipod



Author: arosenfeld
Date: Mon Feb  9 21:16:55 2009
New Revision: 1852
URL: http://svn.gnome.org/viewvc/conduit?rev=1852&view=rev

Log:
2009-02-09  Alexandre Rosenfeld  <airmind gmail com>

        * conduit/modules/iPodModule/iPodModule.py:
        Rewrote iPod track data type, now the code is more clear, and
        the iPod track information can be used in any direction.
        Changed iPod Media dataproviders to return that data type
        when getting items (metadata can be retrieved much faster).
        Made the DB creation only when needed, and the DB locking more
        granular (still needs atention tough).
        * conduit/utils/MediaFile.py:
        * test/python-tests/TestCoreConvertAudioVideo.py:
        Just added a few warnings regarding GStreamer.
        * test/python-tests/TestDataProvideriPod.py:
        Added iPod Music test (it might fail, but it's a start).
        * test/python-tests/common.py:
        Re-enabled the Audio and Video data types.



Modified:
   trunk/ChangeLog
   trunk/conduit/modules/iPodModule/iPodModule.py
   trunk/conduit/utils/MediaFile.py
   trunk/test/python-tests/TestCoreConvertAudioVideo.py
   trunk/test/python-tests/TestDataProvideriPod.py
   trunk/test/python-tests/common.py
   trunk/test/python-tests/data/resources-ipod/tiny.mp3

Modified: trunk/conduit/modules/iPodModule/iPodModule.py
==============================================================================
--- trunk/conduit/modules/iPodModule/iPodModule.py	(original)
+++ trunk/conduit/modules/iPodModule/iPodModule.py	Mon Feb  9 21:16:55 2009
@@ -516,92 +516,161 @@
     def uninitialize(self):
         self.db.close()
 
-STR_CONV = lambda v: unicode(v).encode('UTF-8','replace')
-INT_CONV = lambda v: int(v)
+unicode_conv = lambda v: unicode(v).encode('UTF-8','replace')
 
 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
+    '''
+    A wrapper around an iPod track. iPod track properties are converted into
+    Media properties, and vice-versa.
+    '''
+    
+    #Mappings from the Media metadata to the iPod metadata and vice-versa, 
+    #including type-checking
+    media_to_ipod = {
+        'title' : ('title', unicode_conv),
+        'artist' : ('artist', unicode_conv),
+        'album' : ('album', unicode_conv),
+        'composer' : ('composer', unicode_conv),
+        'rating' : ('rating', lambda v: float(v) / 0.05),
+        'genre' : ('genre', unicode_conv),
+        'track_nr' : ('track-number', int),
+        'tracks' : ('track-count', int),
+        'bitrate' : ('bitrate', int),
+        'tracklen' : ('duration', int),
+        'samplerate' : ('samplerate', int),
+        'width' : ('width', int),
+        'height' : ('height', int),
+    }
+    
+    ipod_to_media = {
+        'title' : ('title', unicode_conv),
+        'artist' : ('artist', unicode_conv),
+        'album' : ('album', unicode_conv),
+        'composer' : ('composer', unicode_conv),
+        'rating' : ('rating', lambda v: float(v) * 0.05),
+        'genre' : ('genre', unicode_conv),
+        'track-number' : ('track_nr', int),
+        'track-count' : ('tracks', int),
+        'bitrate' : ('bitrate', int),
+        'duration' : ('tracklen', int),
+        'samplerate' : ('samplerate', int),
+        'width' : ('width', int),
+        'height' : ('height', int),        
     }
 
-    def __init__(self, db):
+    def __init__(self, db, track = None, f = None):
+        '''
+        Wraps an iPod track in a Datatype. 
+        Passing a file creates a new track in the iPod db, with media information 
+        from that file. Use copy_ipod to transfer it into the iPod.
+        Passing an existing iPod track exports the track's information as a 
+        Media datatype.
+        
+        @param ipod_track: An iPod track to wrap
+        @param f: A File to extract the information from
+        '''
         self.db = db
-        self.track = self.db.new_Track()
+        if track:
+            self.track = track
+        else:
+            self.track = self.db.new_Track()
+        if f:
+            self.set_info_from_file(f)
+        
+    def get_UID(self):
+        '''
+        Returns the database ID (usually a random number, which is always valid
+        for this track in this db, even across application restarts)
+        '''
+        return str(self.track['dbid'])
+
+    def _convert_tags(self, from_tags, mapping):
+        '''
+        Convert from one mapping to another.
+        Returns an iterator with (name, value) for each tag in from_tags
+        '''
+        for from_name, from_value in from_tags.iteritems():
+            if from_name in mapping:
+                to_name, to_converter = mapping[from_name]
+                try:
+                    to_value = to_converter(from_value)
+                    yield to_name, to_value
+                except Exception, e:
+                    log.warn("Could not convert property %s: %s as %s. (Error: %s)" % (from_name, from_value, to_converter, e))
 
     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"),
+        '''
+        Get the track information from a file, including the metadata.
+        Works best with GStreamer metadata in MediaFile.
+        '''
         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']
+        for name, value in self._convert_tags(tags, self.media_to_ipod):
+            log.debug("Got %s = %s" % (name, value))
+            self.track[name] = value
+        #Make sure we have a title to this song, even if it's just the filename
         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': locale.getpreferredencoding()}
         self.track._set_userdata_utf8('filename', f.get_local_uri())
+        
+    def get_track_filename(self):
+        filename = self.track.ipod_filename()
+        if not filename or not os.path.exists(filename):
+            filename = self.track._userdata_into_default_locale('filename')
+        return filename
+        
+    def get_media_tags(self):
+        '''
+        Extends the MediaFile class to include the iPod metadata, instead of
+        calling the GStreamer loader. It's much faster this way, and provides
+        some nice information to other dataproviders, like ratings.
+        '''
+        #FIXME: Cache this information
+        
+        #Get the information from the iPod track.
+        #The track might look like a dict, but it isnt, so we make it into one.
+        track_tags = dict([(name, track[name]) for name in self.media_to_ipod.keys()])
+        return dict(self._convert_tags(track_tags, self.ipod_to_media))
 
     #FIXME: Remove this. Use native operations from Conduit instead.
+    #       We would have to define the transfered userdata as 1 and then call
+    #       Conduit to copy the file. 
+    #       But that is Conduit's copy file way?
     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'] = MEDIATYPE_AUDIO
+class IPodAudio(IPodFileBase, Audio.Audio):
+    def __init__(self, *args, **kwargs):
+        '''
+        Initialize a new Audio track for this db and file.
+        '''
+        IPodFileBase.__init__(self, *args, **kwargs)
+        Audio.Audio.__init__(self, URI = self.get_track_filename())
+
+    def set_info_from_file(self, audio):
+        IPodFileBase.set_info_from_file(self, 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)
+class IPodVideo(IPodFileBase, Video.Video):
+    def __init__(self, *args, **kwargs):
+        '''
+        Initialize a new Video track for this db and file.
+        '''
+        IPodFileBase.__init__(self, *args, **kwargs)
+        Video.Video.__init__(self, URI = self.get_track_filename())
+        
         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)
+        
+    def set_info_from_file(self, video):
+        IPodFileBase.set_info_from_file(video)
         self.track['mediatype'] = {'movie': MEDIATYPE_MOVIE,
                                    'musicvideo': MEDIATYPE_MUSICVIDEO,
                                    'tvshow': MEDIATYPE_TVSHOW,
@@ -663,62 +732,88 @@
     FORMAT_CONVERSION_STRING = _("Encoding")
 
     def __init__(self, *args):
-        if len(args) != 0:
+        self.local_db = (len(args) == 0)
+        if not self.local_db:
             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)
+            DataProvider.TwoWay.__init__(self)        
             self.uid = "Local"
+        self.db = None
         #self.tracks = {}
         self.tracks_id = {}
         self.track_args = {}
         self.keep_converted = True
+        
+    def get_db(self):
+        if self.db:
+            DBCache.lock_db(self.db)
+            return self.db
+        if not self.local_db:
+            self.db = DBCache.get_db(self.mountPoint)
+        else:
+            self.db = DBCache.get_db(None)        
+        DBCache.lock_db(self.db)
+        return self.db
+    
+    def unlock_db(self):
+        DBCache.unlock_db(self.db)
+        
+    def release_db(self):
+        if not self.db:
+            return
+        self.db.close()
+        DBCache.release_db(self.db)
+        self.db = None        
 
     def refresh(self):
         DataProvider.TwoWay.refresh(self)
         self.tracks = {}
         self.tracks_id = {}
-        DBCache.lock_db(self.db)
+        self.get_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
+                self.tracks_id[str(track['dbid'])] = track
             [add_track(track) for track in self.db \
                 if track['mediatype'] in self._mediatype_]
         finally:
-            DBCache.unlock_db(self.db)
+            self.unlock_db()
 
     def get_all(self):
         return self.tracks_id.keys()
 
     def get(self, LUID = None):
-        DBCache.lock_db(self.db)
+        self.get_db()
         try:
+            if LUID not in self.tracks_id:
+                raise Exceptions.SyncronizeError('Track ID %s not found in iPod DB %s' % (LUID, self.db))
             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
+            ipod_file = self._ipodmedia_(self.db, track = track)
+            filename = ipod_file.get_track_filename()
+            if not os.path.exists(filename):
+                raise Exceptions.SyncronizeError("Could not find iPod track file %s" % (filename))
+            #Set a nice "Artist - Title" name with the original filename
+            #extension
+            #FIXME: Doesnt work as expected anymore, the original filename is
+            #renamed instead
+            #if track.ipod_filename() and track['artist'] and track['title']:
+            #    ipod_file.force_new_filename("%(artist)s - %(title)s" % track + \
+            #        os.path.splitext(filename)[1])
+            return ipod_file
         finally:
-            DBCache.unlock_db(self.db)
+            self.unlock_db()
         return None
 
     def put(self, f, overwrite, LUID=None):
-        DBCache.lock_db(self.db)
+        self.get_db()
         try:
-            media_file = self._ipodmedia_(f, self.db, **self.track_args)
+            media_file = self._ipodmedia_(db = self.db, f = f, **self.track_args)
             #FIXME: We keep the db locked while we copy the file. Not good
+            #media_file.
             media_file.copy_ipod()
+            self.tracks_id[str(media_file.track['dbid'])] = media_file.track
             #FIXME: Writing the db here is for debug only. Closing does not actually
-            # close the db, it only writes it's contents to disk.
-            
+            # 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
@@ -728,17 +823,17 @@
             self.db.close()
             return media_file
         finally:
-            DBCache.unlock_db(self.db)
+            self.unlock_db()
 
     def delete(self, LUID):
         track = self.tracks_id[LUID]
         if track:
-            DBCache.lock_db(db)
+            self.get_db()
             try:
                 self.db.remove(track)
                 self.db.close()
             finally:
-                DBCache.unlock_db(db)
+                self.unlock_db()
 
     def get_config_items(self):
         import gtk
@@ -805,9 +900,7 @@
             return {}
 
     def uninitialize(self):
-        self.db.close()
-        DBCache.release_db(self.db)
-        self.db = None
+        self.release_db()
 
 IPOD_AUDIO_ENCODINGS = {
     "mp3": {"description": "Mp3", "acodec": "lame", "file_extension": "mp3"},
@@ -826,7 +919,6 @@
     _configurable_ = True
 
     _mediatype_ = (MEDIATYPE_AUDIO,)
-    _mediafile_ = Audio.Audio
     _ipodmedia_ = IPodAudio
 
     def __init__(self, *args):
@@ -861,7 +953,6 @@
     _configurable_ = True
 
     _mediatype_ = (MEDIATYPE_MUSICVIDEO, MEDIATYPE_MOVIE, MEDIATYPE_TVSHOW)
-    _mediafile_ = Video.Video
     _ipodmedia_ = IPodVideo
 
     def __init__(self, *args):

Modified: trunk/conduit/utils/MediaFile.py
==============================================================================
--- trunk/conduit/utils/MediaFile.py	(original)
+++ trunk/conduit/utils/MediaFile.py	Mon Feb  9 21:16:55 2009
@@ -44,7 +44,13 @@
 
     def _create_gst_metadata(self):
         '''
-        Create metadata from GStreamer
+        Create metadata from GStreamer.
+        
+        Requires a mainloop and the calling thread MUST BE outside the main 
+        loop (usually not a problem inside the synchronization process, which
+        has it's own thread).
+        This is also a very expensive operation, should be called only when 
+        necessary.   
         '''
         event = threading.Event()
         def discovered(discoverer, valid):
@@ -56,6 +62,8 @@
         info.connect('discovered', discovered)
         info.discover()
         # Wait for discover to finish (which is async and emits discovered)
+        # This thread MUST NOT be the mainloop thread, because then the signal
+        # will NEVER be received, and this will deadlock.
         event.wait()
         if self._valid:
             tags = info.tags
@@ -96,6 +104,8 @@
         '''
         Get a dict containing all availiable metadata.
 
+        It defaults to get the metadata from GStreamer and make a cache that is
+        accessed later.
         Descendants should override this function to provide their own tags,
         or merge with these tags, by calling MediaFile.get_media_tags().
         '''

Modified: trunk/test/python-tests/TestCoreConvertAudioVideo.py
==============================================================================
--- trunk/test/python-tests/TestCoreConvertAudioVideo.py	(original)
+++ trunk/test/python-tests/TestCoreConvertAudioVideo.py	Mon Feb  9 21:16:55 2009
@@ -51,6 +51,8 @@
     gobject.idle_add(mainloop.quit)
 
 def idle_cb():
+    # We must run the conversion tests in a thread because the GStreamer 
+    # conversion uses Locks internally which blocks the main loop
     threading.Thread(target=convert).start()
     return False
 

Modified: trunk/test/python-tests/TestDataProvideriPod.py
==============================================================================
--- trunk/test/python-tests/TestDataProvideriPod.py	(original)
+++ trunk/test/python-tests/TestDataProvideriPod.py	Mon Feb  9 21:16:55 2009
@@ -1,6 +1,8 @@
 #common sets up the conduit environment
+    
 from common import *
 
+import threading
 import os.path
 import shutil
 import traceback
@@ -17,6 +19,7 @@
 ipodContactsDp = iPodModule.IPodContactsTwoWay(fakeIpodDir,"")
 ipodCalendarDp = iPodModule.IPodCalendarTwoWay(fakeIpodDir,"")
 ipodPhotoDp = iPodModule.IPodPhotoSink(fakeIpodDir,"")
+ipodMusicDp = iPodModule.IPodMusicTwoWay(None,"")
 
 #The ipod photo (and music AFAICT) require some initialization of 
 #a skeleton database file. 
@@ -35,22 +38,42 @@
 (ipodNoteDp,        new_note,               "IPodNoteTwoWay",       True),
 (ipodContactsDp,    new_contact,            "IPodContactsTwoWay",   True),
 (ipodCalendarDp,    new_event,              "IPodCalendarTwoWay",   True),
-(ipodPhotoDp,       new_photo,              "IPodPhotoSink",        False),
+(ipodMusicDp,       new_audio,              "IPodMusicTwoWay",      True),
+#(ipodPhotoDp,       new_photo,              "IPodPhotoSink",        False),
 )
 
-for dp, newdata_func, name, istwoway in TESTS:
-    test = SimpleTest()
-    test.set_sink(
-            test.wrap_dataprovider(dp)
-            )
-    
-    newdata = newdata_func(None)
-    test.do_dataprovider_tests(
-        supportsGet=istwoway,
-        supportsDelete=True,
-        safeLUID=None,
-        data=newdata,
-        name="%s:%s" % (name,dp._in_type_)
-        )
+import gobject
+import gtk
+gtk.gdk.threads_init()
+
+
+mainloop = gobject.MainLoop()
+
+def run_tests():
+    try:
+        for dp, newdata_func, name, istwoway in TESTS:
+            test = SimpleTest()
+            test.set_sink(
+                    test.wrap_dataprovider(dp)
+                    )
+            
+            newdata = newdata_func(None)
+            test.do_dataprovider_tests(
+                supportsGet=istwoway,
+                supportsDelete=True,
+                safeLUID=None,
+                data=newdata,
+                name="%s:%s" % (name,dp._in_type_)
+                )
+                
+    finally:
+        mainloop.quit()
+
+def idle_cb():
+    threading.Thread(target=run_tests).start()
+    return False
 
+gobject.idle_add(idle_cb)
+mainloop.run()
 finished()
+

Modified: trunk/test/python-tests/common.py
==============================================================================
--- trunk/test/python-tests/common.py	(original)
+++ trunk/test/python-tests/common.py	Mon Feb  9 21:16:55 2009
@@ -223,21 +223,23 @@
     t.set_open_URI(Utils.random_string())
     return t
 
-#def new_audio(filename):
-#    a = Audio.Audio(
-#                URI=filename
-#                )                
-#    a.set_UID(Utils.random_string())
-#    a.set_open_URI(filename)
-#    return a
-
-#def new_video(filename):
-#    v = Video.Video(
-#                URI=filename
-#                )                
-#    v.set_UID(Utils.random_string())
-#    v.set_open_URI(filename)
-#    return v
+def new_audio(filename = None):
+    if not filename:
+        filename = get_files_from_data_dir("*.mp3")[0]
+    a = Audio.Audio(
+                URI=filename
+                )                
+    a.set_UID(Utils.random_string())
+    a.set_open_URI(filename)
+    return a
+
+def new_video(filename):
+    v = Video.Video(
+                URI=filename
+                )                
+    v.set_UID(Utils.random_string())
+    v.set_open_URI(filename)
+    return v
 
 def new_photo(filename):
     if filename == None:

Modified: trunk/test/python-tests/data/resources-ipod/tiny.mp3
==============================================================================
Binary files. No diff available.



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