conduit r1767 - in trunk: . conduit/datatypes conduit/modules conduit/modules/RhythmboxModule conduit/modules/iPodModule conduit/utils
- From: arosenfeld svn gnome org
- To: svn-commits-list gnome org
- Subject: conduit r1767 - in trunk: . conduit/datatypes conduit/modules conduit/modules/RhythmboxModule conduit/modules/iPodModule conduit/utils
- Date: Mon, 20 Oct 2008 17:07:07 +0000 (UTC)
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]