conduit r1767 - in trunk: . conduit/datatypes conduit/modules conduit/modules/RhythmboxModule conduit/modules/iPodModule conduit/utils



Author: arosenfeld
Date: Mon Oct 20 17:07:07 2008
New Revision: 1767
URL: http://svn.gnome.org/viewvc/conduit?rev=1767&view=rev

Log:
2008-10-20  Alexandre Rosenfeld  <airmind gemini>

	* conduit/datatypes/Audio.py:
	* conduit/datatypes/Video.py: Improved documentation and added genre
	and playcount to Audio	
	* conduit/modules/AudioVideoConverterModule.py: Fixed no conversion
	needed for Audio
	* conduit/modules/RhythmboxModule/RhythmboxModule.py: Applied patch
	from bug 556763 - Adds rating, play-count and partial cover-location
	support. Extended to include all required fields from the Rhythmbox
	DB.	
	* conduit/modules/iPodModule/iPodModule.py: Improved Configuration 
	code and fixed small bugs
	* conduit/utils/MediaFile.py: Improved documentation



Modified:
   trunk/ChangeLog
   trunk/conduit/datatypes/Audio.py
   trunk/conduit/datatypes/Video.py
   trunk/conduit/modules/AudioVideoConverterModule.py
   trunk/conduit/modules/RhythmboxModule/RhythmboxModule.py
   trunk/conduit/modules/iPodModule/iPodModule.py
   trunk/conduit/utils/MediaFile.py

Modified: trunk/conduit/datatypes/Audio.py
==============================================================================
--- trunk/conduit/datatypes/Audio.py	(original)
+++ trunk/conduit/datatypes/Audio.py	Mon Oct 20 17:07:07 2008
@@ -4,12 +4,12 @@
 import logging
 log = logging.getLogger("datatypes.Audio")
 
-from threading import Lock
-
 PRESET_ENCODINGS = {
-    "ogg":{"acodec":"vorbisenc","format":"oggmux","file_extension":"ogg"},
-    "wav":{"acodec":"wavenc","file_extension":"wav"},
-    "mp3":{"acodec":"lame", "file_extension": "mp3"},
+    "ogg":{"description": "Ogg", "acodec": "vorbisenc", "format":"oggmux","file_extension":"ogg", 'mimetype': 'application/ogg'},
+    "wav":{"description": "Wav", "acodec": "wavenc", "file_extension":"wav", 'mimetype': 'audio/x-wav'},
+    "mp3":{"description": "Mp3", "acodec": "lame", "file_extension": "mp3", 'mimetype':'audio/mpeg'},
+    #AAC conversion doesn't work
+    #"aac":{"description": "AAC", "acodec": "faac", "file_extension": "m4a"},    
     }
 
 def mimetype_is_audio(mimetype):
@@ -32,38 +32,49 @@
 
     def get_audio_title(self):
         '''
-        Song title (string)
+        Song title (str)
         '''
         return self._get_metadata('title')
 
     def get_audio_artist(self):
         '''
-        Song artist (string)
+        Song artist (str)
         '''
         return self._get_metadata('artist')
 
     def get_audio_album(self):
         '''
-        Song album (string)
+        Song album (str)
         '''
         return self._get_metadata('album')
 
+    def get_audio_genre(self):
+        '''
+        Song genre (str)
+        '''
+        return self._get_metadata('genre')
+
     def get_audio_track(self):
+        '''
+        Get number of the track inside the album (int)
+        '''
         return self._get_metadata('track-number')
 
     def get_audio_tracks(self):
-
+        '''
+        Get number of tracks in album (int)
+        '''
         return self._get_metadata('track-count')
 
     def get_audio_bitrate(self):
         '''
-        Bitrate of the audio stream (int)
+        Bitrate of the audio stream, in bits/sec (int)
         '''
         return self._get_metadata('bitrate')
 
     def get_audio_composer(self):
         '''
-        Song composer
+        Song composer (str)
         '''
         return self._get_metadata('composer')
 
@@ -85,11 +96,20 @@
         '''
         return self._get_metadata('channels')
 
+    def get_audio_playcount(self):
+        '''
+        Audio play count (int)
+        '''
+        return self._get_metadata('play_count')
+
     def get_audio_rating(self):
         '''
-        Audio rating from 0.0 to 5.0
+        Audio rating from 0.0 to 5.0 (float)
         '''
         return self._get_metadata('rating')
 
     def get_audio_cover_location(self):
+        '''
+        Get path to the track album cover (str)
+        '''
         return self._get_metadata('cover_location')

Modified: trunk/conduit/datatypes/Video.py
==============================================================================
--- trunk/conduit/datatypes/Video.py	(original)
+++ trunk/conduit/datatypes/Video.py	Mon Oct 20 17:07:07 2008
@@ -2,19 +2,13 @@
 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":"xvidenc", "acodec":"lame", "format":"avimux", "vtag":"DIVX", "file_extension":"avi", "mimetype": "video/x-msvideo"},
-    #breaks on single channel audio files because ffmpeg vorbis encoder only suuport stereo
+    #FIXME: The following comment has not been tested with GStreamer, it may or may not still be true:
+    # breaks on single channel audio files because ffmpeg vorbis encoder only suuport stereo
     "ogg":{"vcodec":"theoraenc", "acodec":"vorbisenc", "format":"oggmux", "file_extension":"ogg"},
-    #needs mencoder or ffmpeg compiled with mp3 support
     #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):
@@ -36,7 +30,13 @@
         MediaFile.MediaFile.__init__(self, URI, **kwargs)
 
     def get_video_duration(self):
+        '''
+        Video duration, in milisecs (int)
+        '''
         return self._get_metadata('duration')
 
     def get_video_size(self):
+        '''
+        Video size, as a tuple (width, height), both in pixels (int, int)
+        '''
         return self._get_metadata('width'), self._get_metadata('height')

Modified: trunk/conduit/modules/AudioVideoConverterModule.py
==============================================================================
--- trunk/conduit/modules/AudioVideoConverterModule.py	(original)
+++ trunk/conduit/modules/AudioVideoConverterModule.py	Mon Oct 20 17:07:07 2008
@@ -239,10 +239,12 @@
 			# FIXME: A little hackish, but works.
             if hasattr(current_thread, 'cancelled'):
                 if current_thread.cancelled:
+                    log.debug("Stopping conversion")
                     pipeline.set_state(gst.STATE_NULL)      
                     pipeline = None
                     return False
             check_progress = True
+        pipeline.set_state(gst.STATE_NULL)
         pipeline = None
         return self.success
 
@@ -358,7 +360,11 @@
         
         kwargs['in_file'] = audio.get_local_uri()
         kwargs['out_file'] = self._get_output_file(kwargs['in_file'], **kwargs)
-
+        
+        if kwargs.get('mimetype', None) == mimetype:    
+            log.debug('No need to convert file')
+            return audio
+        
         #convert audio
         gst_converter = GStreamerConverter()
         sucess = gst_converter.convert(**kwargs)

Modified: trunk/conduit/modules/RhythmboxModule/RhythmboxModule.py
==============================================================================
--- trunk/conduit/modules/RhythmboxModule/RhythmboxModule.py	(original)
+++ trunk/conduit/modules/RhythmboxModule/RhythmboxModule.py	Mon Oct 20 17:07:07 2008
@@ -9,6 +9,8 @@
 import urllib
 import os
 import logging
+from xml.sax import make_parser, handler, SAXException
+
 log = logging.getLogger("modules.Rhythmbox")
 
 
@@ -35,6 +37,8 @@
 NAME_IDX=0
 CHECK_IDX=1
 
+class SearchComplete(SAXException): pass
+
 class RhythmboxSource(DataProvider.DataSource):
 
     _name_ = _("Rhythmbox Music")
@@ -44,8 +48,10 @@
     _in_type_ = "file/audio"
     _out_type_ = "file/audio"
     _icon_ = "rhythmbox"
+    _configurable_ = True
 
     PLAYLIST_PATH="~/.gnome2/rhythmbox/playlists.xml"
+    RHYTHMDB_PATH="~/.gnome2/rhythmbox/rhythmdb.xml"
 
     def __init__(self, *args):
         DataProvider.DataSource.__init__(self)
@@ -53,6 +59,7 @@
         self.allPlaylists = []
         #Names we wish to sync
         self.playlists = []
+        self.songdata = {}
 
     def _parse_playlists(self, path, allowed=[]):
         playlists = []
@@ -85,16 +92,22 @@
             if element.text:
                 text = element.text
             if element.tag == "location":
-                song_location = ''.join(urllib.url2pathname(text).split("://")[1:])
-
-                if not os.path.exists(song_location):
-                    print "WARNING: A song referred to from the playlist '%s' cannot be found on the harddrive." % playlist_name
-                    continue
-
-                songs.append( song_location )
+                songs.append( text )
 
         return playlists
 
+    def _init_songdata(self, songs):
+        rb_handler = RhythmDBHandler(songs)
+        parser = make_parser()
+        parser.setContentHandler(rb_handler)
+        path = os.path.expanduser(self.RHYTHMDB_PATH) 
+        try:
+            parser.parse(path)
+        except SearchComplete:
+            pass
+        self.songdata = rb_handler.songdata
+        return rb_handler.cleansongs
+
     def configure(self, window):
         import gtk
         import gobject
@@ -160,18 +173,102 @@
             for song in playlist[1]:
                 songs.append(song)
 
-        return songs
+        # get only the song data that we care about and clean up the file paths
+        return self._init_songdata(songs)
 
     def get(self, songuri):
         DataProvider.DataSource.get(self, songuri)
-        f = Audio.Audio(URI=songuri)
+        f = RhythmboxAudio(URI=songuri, songdata=self.songdata.get(songuri))
         f.set_UID(songuri)
         f.set_open_URI(songuri)
 
         return f
+
     def get_configuration(self):
         return { "playlists" : self.playlists }
  
     def get_UID(self):
         return ""
 
+class RhythmboxAudio(Audio.Audio):
+    '''Wrapper around the standard Audio datatype that implements
+    the rating, playcount, and cover location tags.
+    '''
+    COVER_ART_PATH="~/.gnome2/rhythmbox/covers/"
+    def __init__(self, URI, **kwargs):
+        Audio.Audio.__init__(self, URI, **kwargs)
+        self._songdata = kwargs['songdata'] or {}
+        tags = {}
+        # Make sure the songs has a rating (which is different from having a 0 rating)
+        if 'rating' in self._songdata:
+            tags['rating'] = float(self._songdata.get('rating', 0))
+        tags['play_count'] = int(self._songdata.get('play-count', 0))
+        tags['cover_location'] = self.find_cover_location()
+        tags['title'] = self._songdata.get('title')
+        tags['artist'] = self._songdata.get('artist')
+        tags['album'] = self._songdata.get('album')
+        tags['genre'] = self._songdata.get('genre')
+        tags['track-number'] = int(self._songdata.get('track-number', 0))
+        tags['duration'] = int(self._songdata.get('duration', 0)) * 1000
+        tags['bitrate'] = int(self._songdata.get('bitrate', 0)) * 1000
+        self.rhythmdb_tags = tags
+
+    def find_cover_location(self):
+        #TODO: Finish this
+        return ''
+
+    def get_media_tags(self):
+        return self.rhythmdb_tags
+
+
+class RhythmDBHandler(handler.ContentHandler):
+    '''A SAX XML handler that loops through a list of songs and retrieves the interesting data.  
+    While we're at it, clean the filepath and check for the existance of the file 
+    before adding it to the final list of songs.
+
+    We use a SAX parser because it's gentler on resources (it doesn't need to store the 
+    entire parsed file in memory), it's *tons* faster (there is no overhead of creating
+    an object tree/map), and we can stop parsing once all of the songs in the 
+    list have been found.
+    '''
+    #we could just as easily get the rest of the file information
+    _interesting_ = ('location', 'title', 'genre', 'artist', 'album', 'track-number',
+        'play-count', 'rating', 'duration', 'bitrate')
+
+    def __init__(self, searchlist):
+        self.searchlist = searchlist
+        self.cleansongs = []
+        self.songdata = {}
+        self._content_needed = ''
+
+    def _clean_location(self, location):
+        song_location = ''.join(urllib.url2pathname(location).split("://")[1:])
+        if not os.path.exists(song_location):
+            print "WARNING: A song referred to from the playlist '%s' cannot be found on the harddrive." % playlist_name
+            return None
+        return song_location
+
+    def startElement(self, name, attrs):
+        if name=='entry':
+            self.song = {}
+        if name in self._interesting_:
+            self._content_needed = name
+            
+    def endElement(self, name):
+        if name=='entry':
+            location = self.song.get('location')
+            if location in self.searchlist:
+                songpath = self._clean_location(location)
+                if songpath:
+                    # We've found a song and it exists on the file system
+                    self.cleansongs.append(songpath)
+                    self.songdata[songpath] = self.song
+                    self.searchlist.remove(location)
+        self._content_needed = ''
+        if not self.searchlist:
+            raise SearchComplete('Exhausted search items.')
+
+    def characters(self, content):
+        if self._content_needed:
+            self.song[self._content_needed] = content
+

Modified: trunk/conduit/modules/iPodModule/iPodModule.py
==============================================================================
--- trunk/conduit/modules/iPodModule/iPodModule.py	(original)
+++ trunk/conduit/modules/iPodModule/iPodModule.py	Mon Oct 20 17:07:07 2008
@@ -714,30 +714,27 @@
 
     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()]
+        self.config_encodings = tuple(self.encodings.iteritems())
         initial_enc = None
-        for encoding in self.config_encodings:
-            if encoding['name'] == self.encoding:
-                initial_enc = encoding.get('description', None) or encoding['name']
+        for (encoding_name, encoding_opts) in self.config_encodings:
+            if encoding_name == self.encoding:
+                initial_enc = encoding_opts.get('description', None) or encoding_name
 
         def selectEnc(index, text):
-            self.encoding = self.config_encodings[index]['name']
+            self.encoding = self.config_encodings[index][0]
             log.debug('Encoding %s selected' % self.encoding)
-            
+        
         def selectKeep(value):
             self.keep_converted = value
             log.debug("Keep converted selected: %s" % (value))
-            
+        
         return [
                     {
                     "Name" : self.FORMAT_CONVERSION_STRING,
                     "Kind" : "list",
                     "Callback" : selectEnc,
-                    "Values" : [encoding.get('description', None) or encoding['name'] for encoding in self.config_encodings],
+                    "Values" : [enc_opts.get('description', None) or enc_name for enc_name, enc_opts in self.config_encodings],
                     "InitialValue" : initial_enc
                     },
                     
@@ -786,8 +783,8 @@
 
 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"},
+    #FIXME: AAC needs a MP4 mux
+    #"aac": {"description": "AAC", "acodec": "faac", "file_extension": "m4a"},
     }
 
 class IPodMusicTwoWay(IPodMediaTwoWay):
@@ -880,10 +877,10 @@
     def set_configuration(self, config):
         IPodMediaTwoWay.set_configuration(self, config)
         if 'video_kind' in config:
-            self.encoding = config['video_kind']
+            self.video_kind = config['video_kind']
         self._update_track_args()
 
     def get_configuration(self):
         config = IPodMediaTwoWay.get_configuration(self)
-        config.update({'encoding':self.encoding})
+        config.update({'video_kind':self.video_kind})
         return config

Modified: trunk/conduit/utils/MediaFile.py
==============================================================================
--- trunk/conduit/utils/MediaFile.py	(original)
+++ trunk/conduit/utils/MediaFile.py	Mon Oct 20 17:07:07 2008
@@ -12,20 +12,45 @@
     GST_AVAILABLE = False
 
 class MediaFile(File.File):
+    '''
+    A MediaFile is a file with multimedia attributes, such as an audio or video
+    file.
+    
+    This class includes methods to access metadata included in the file. 
+    Using the GStreaner framework, it is able to retrieve most commonly used
+    properties of this kind of file.
+    
+    Media providers can include their own data by overriding get_media_tags,
+    and either providing a new set of properties, or call this class's
+    get_media_tags to merge their data with the GStreamer properties.
+    
+    The Audio and Video classes expose these properties as convenient
+    methods. Note that a descendant of this class only needs to put their
+    data in get_media_tags for them to be exposed by the Audio and Video 
+    classes. However, they need to follow the types and units of the GStreamer
+    properties, which are described in each of their methods.
+    
+    Retrieving metadata through GStreamer is a costly process, because the file
+    must be accessed and processed. Thus, it is only retrieved when needed, when
+    the gst_tags attribute is accessed. So, accessing any metadata starts a 
+    chain reaction, which starts with descendants overriding get_media_tags,
+    eventually calling get_media_tags in this class, then accesses gst_tags,
+    thus creating the gst metadata if needed.
+    '''
 
     def __init__(self, URI, **kwargs):
         File.File.__init__(self, URI, **kwargs)
 
     def _create_gst_metadata(self):
         '''
-        Get metadata from GStreamer
+        Create metadata from GStreamer
         '''
         event = threading.Event()
         def discovered(discoverer, valid):
             self._valid = valid
             event.set()
         # FIXME: Using Discoverer for now, but we should switch to utils.GstMetadata
-        #        when we get thumbnails working on it.
+        #        when we get it to work (and eventually support thumbnails).
         info = discoverer.Discoverer(self.get_local_uri())
         info.connect('discovered', discovered)
         info.discover()
@@ -36,6 +61,8 @@
         else:
             log.debug("Media file not valid")
             return {}
+
+        tags['mimetype'] = info.mimetype
         if info.is_video:
             tags['width'] = info.videowidth
             tags['height'] = info.videoheight
@@ -45,6 +72,8 @@
             tags['duration'] = info.audiolength / gst.MSECOND
             tags['samplerate'] = info.audiorate
             tags['channels'] = info.audiochannels
+            tags['audiowidth'] = info.audiowidth
+            tags['audiodepth'] = info.audiodepth
         return tags
 
     def _get_metadata(self, name):        
@@ -57,6 +86,7 @@
         # Get metadata only when needed
         if name == 'gst_tags':
             tags = self.gst_tags = self._create_gst_metadata()
+            # Don't call self.gst_tags here
             return tags
         else:
             raise AttributeError
@@ -66,8 +96,15 @@
         Get a dict containing all availiable metadata.
 
         Descendants should override this function to provide their own tags,
-        or merge with these tags.
+        or merge with these tags, by calling MediaFile.get_media_tags().
         '''
         if GST_AVAILABLE:
             return self.gst_tags
         return {}
+    
+    def get_media_mimetype(self):
+        '''
+        Return the file miemtype, as returned by GStreamer, which might differ
+        from the file mimetype
+        '''
+        return self._get_metadata('mimetype')



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