updated on Sat Jan 14 00:11:12 UTC 2012
[aur-mirror.git] / soundconverter-xfce / soundconverter.patched
blob1fc23b3efa0d13fbc523ae4153687ae45995eef8
1 #!/usr/bin/python2 -tt
2 # -*- coding: utf-8 -*-
4 # SoundConverter - GNOME application for converting between audio formats.
5 # Copyright 2004 Lars Wirzenius
6 # Copyright 2005-2011 Gautier Portet
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; version 3 of the License.
12 # This program is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 # General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
20 # USA
22 NAME = 'SoundConverter'
23 VERSION = '1.5.4'
24 GLADE = '/usr/share/soundconverter/soundconverter.glade'
26 print '%s %s' % (NAME, VERSION)
28 # Python standard stuff.
29 import sys
30 import os
31 import inspect
32 import textwrap
33 import urlparse
34 import string
35 import thread
36 import urllib
37 import time
38 import unicodedata
39 from optparse import OptionParser
41 #localization
42 import locale
43 import gettext
44 PACKAGE = NAME.lower()
45 gettext.bindtextdomain(PACKAGE,'/usr/share/locale')
46 locale.setlocale(locale.LC_ALL,'')
47 gettext.textdomain(PACKAGE)
48 gettext.install(PACKAGE,localedir='/usr/share/locale',unicode=1)
51 def cpuCount():
52         '''
53         Returns the number of CPUs in the system.
54         (from pyprocessing)
55         '''
56         if sys.platform == 'win32':
57                 try:
58                         num = int(os.environ['NUMBER_OF_PROCESSORS'])
59                 except (ValueError, KeyError):
60                         num = 0
61         elif sys.platform == 'darwin':
62                 try:
63                         num = int(os.popen('sysctl -n hw.ncpu').read())
64                 except ValueError:
65                         num = 0
66         else:
67                 try:
68                         num = os.sysconf('SC_NPROCESSORS_ONLN')
69                 except (ValueError, OSError, AttributeError):
70                         num = 0
71         if num >= 1:
72                 return num
73         else:
74                 return 1
76 settings = {
77         'mode': 'gui',
78         'quiet': False,
79         'debug': False,
80         'cli-output-type': 'audio/x-vorbis',
81         'cli-output-suffix': '.ogg',
82         'jobs': cpuCount(),
86 def get_option(key):
87         assert key in settings
88         return settings[key]
90 def mode_callback(option, opt, value, parser, **kwargs):
91         setattr(parser.values, option.dest, kwargs[option.dest])
94 def parse_command_line():
95         parser = OptionParser()
96         parser.add_option('-b', '--batch', dest='mode', action='callback',
97                 callback=mode_callback, callback_kwargs={'mode':'batch'},
98                 help=_('Convert in batch mode, from command line, '
99                         'without a graphical user\n interface. You '
100                         'can use this from, say, shell scripts.'))
101         parser.add_option('-t', '--tags', dest="mode", action='callback',
102                 callback=mode_callback,  callback_kwargs={'mode':'tags'},
103                 help=_('Show tags for input files instead of converting'
104                         'them. This indicates \n command line batch mode'
105                         'and disables the graphical user interface.'))
106         parser.add_option('-m', '--mime-type', action="store", dest="batch_mime",
107                 help=_('Set the output MIME type for batch mode. The default'
108                         'is %s. Note that you probably want to set the output'
109                         'suffix as well.') % settings['cli-output-type'])
110         parser.add_option('-q', '--quiet', action="store_true", dest="quiet",
111                 help=_("Be quiet. Don't write normal output, only errors."))
112         parser.add_option('-d', '--debug', action="store_true", dest="debug",
113                 help=_('Print additional debug information'))
114         parser.add_option('-s', '--suffix', dest="new_suffix",
115                 help=_('Set the output filename suffix for batch mode.'
116                         'The default is %s . Note that the suffix does not'
117                         'affect\n the output MIME type.') % settings['cli-output-suffix'])
118         parser.add_option('-j', '--jobs', action='store', type='int', dest='jobs',
119                 metavar='NUM', help=_('Force number of concurrent conversions.'))
120         parser.add_option('--help-gst', action="store_true", dest="_unused",
121                 help=_('Show GStreamer Options'))
122         return parser
124 parser = parse_command_line()
125 # remove gstreamer arguments so only gstreamer see them.
126 args = [a for a in sys.argv[1:] if '-gst' not in a]
128 options, args = parser.parse_args(args)
130 if options.mode:
131         settings['mode'] = options.mode
132 if options.jobs:
133         settings['jobs'] = options.jobs
134 if options.batch_mime:
135         settings['cli-output-type'] = options.batch_mime
136 if options.new_suffix:
137         settings['cli-output-suffix'] = options.new_suffix
140 # we prefer to launch locally present glade file
141 # so we can launch from source folder, without installing
142 LOCAL_GLADE = '../data/soundconverter.glade'
143 if os.path.exists(LOCAL_GLADE):
144         GLADE = LOCAL_GLADE
146 # GNOME and related stuff.
147 try:
148         import pygtk
149         pygtk.require('2.0')
150         import gtk
151         import gtk.glade
152 # XFCEpatch
153 #       import gnome
154 #       import gnome.ui
155 #       gnome.ui.authentication_manager_init()
156         import gconf
157         import gobject
158         gobject.threads_init()
159         import gnomevfs
160 except ImportError, e:
161         print 'error: %s' % e
162         print '%s needs gnome-python 2.10!' % NAME
163         sys.exit(1)
165 # GStreamer
166 try:
167         import pygst
168         pygst.require('0.10')
169         import gst
170 except ImportError:
171         print '%s needs python-gstreamer 0.10!' % NAME
172         sys.exit(1)
174 print '  using Gstreamer version: %s' % (
175                 '.'.join([str(s) for s in gst.gst_version]))
177 # This is missing from gst, for some reason.
178 FORMAT_PERCENT_SCALE = 10000
180 # notifications
181 def notification(message):
182         pass
184 try:
185         import pynotify
187         if pynotify.init('Basics'):
188                 def notification(message):
189                         try:
190                                 n = pynotify.Notification(NAME, message)
191                                 n.show()
192                         except:
193                                 pass
194 except:
195         pass
198 gtk.glade.bindtextdomain(PACKAGE,'/usr/share/locale')
199 gtk.glade.textdomain(PACKAGE)
201 TRANSLATORS = ("""
202 Guillaume Bedot <littletux zarb.org> (French)
203 Dominik Zabłotny <dominz wp.pl> (Polish)
204 Jonh Wendell <wendell bani.com.br> (Portuguese Brazilian)
205 Marc E. <m4rccd yahoo.com> (Spanish)
206 Daniel Nylander <po danielnylander se> (Swedish)
207 Alexandre Prokoudine <alexandre.prokoudine gmail.com> (Russian)
208 Kamil Páral <ripper42 gmail.com > (Czech)
209 Stefano Luciani <luciani.fa tiscali.it > (Italian)
210 Uwe Bugla <uwe.bugla@gmx.de> (German)
211 Nizar Kerkeni <nizar.kerkeni gmail.com>(Arabic)
212 amenudo (Basque)
213 rainofchaos (Simplified Chinese)
214 Pavol Klačanský (Slovak)
215 Moshe Basanchig <moshe.basanchig gmail.com> (Hebrew)
216 """)
218 # Names of columns in the file list
219 VISIBLE_COLUMNS = ['filename']
220 ALL_COLUMNS = VISIBLE_COLUMNS + ['META']
222 MP3_CBR, MP3_ABR, MP3_VBR = range(3)
224 # add here any format you want to be read
225 mime_whitelist = (
226         'audio/',
227         'video/',
228         'application/ogg',
229         'application/x-id3',
230         'application/x-ape',
231         'application/vnd.rn-realmedia',
232         'application/x-pn-realaudio',
233         'application/x-shockwave-flash',
234         'application/x-3gp',
237 # custom filename patterns
238 english_patterns = 'Artist Album Title Track Total Genre Date Year Timestamp'
240 # traductors: These are the custom filename patterns. Only if it does make sense.
241 locale_patterns = _('Artist Album Title Track Total Genre Date Year Timestamp')
243 patterns_formats = (
244         '%(artist)s',
245         '%(album)s',
246         '%(title)s',
247         '%(track-number)02d',
248         '%(track-count)02d',
249         '%(genre)s',
250         '%(date)s',
251         '%(year)s',
252         '%(timestamp)s',
255 # add english and locale
256 custom_patterns = english_patterns + ' ' + locale_patterns
257 # convert to list
258 custom_patterns = [ '{%s}' % p for p in custom_patterns.split()]
259 # and finally to dict, thus removing doubles
260 custom_patterns = dict(zip(custom_patterns, patterns_formats*2))
262 locale_patterns_dict = dict(zip(
263         [ p.lower() for p in english_patterns.split()],
264         [ '{%s}' % p for p in locale_patterns.split()] ))
266 # add here the formats not containing tags
267 # not to bother searching in them
268 tag_blacklist = (
269         'audio/x-wav',
272 # Name and pattern for CustomFileChooser
273 filepattern = (
274         (_('All files'),'*.*'),
275         ('MP3','*.mp3'),
276         ('Ogg Vorbis','*.ogg;*.oga'),
277         ('AAC','*.m4a;*.aac'),
278         ('WAV','*.wav'),
279         ('FLAC','*.flac'),
280         ('AC3','*.ac3')
283 def beautify_uri(uri):
284         return unquote_filename(uri).split('file://')[-1]
287 def vfs_walk(uri):
288         """similar to os.path.walk, but with gnomevfs.
290         uri -- the base folder uri.
291         return a list of uri.
293         """
294         if str(uri)[-1] != '/':
295                 uri = uri.append_string('/')
297         filelist = []
299         try:
300                 dirlist = gnomevfs.open_directory(uri, gnomevfs.FILE_INFO_FOLLOW_LINKS)
301         except:
302                 log("skipping: '%s\'" % uri)
303                 return filelist
305         for file_info in dirlist:
306                 try:
307                         if file_info.name[0] == '.':
308                                 continue
310                         if file_info.type == gnomevfs.FILE_TYPE_DIRECTORY:
311                                 filelist.extend(
312                                         vfs_walk(uri.append_path(file_info.name)) )
314                         if file_info.type == gnomevfs.FILE_TYPE_REGULAR:
315                                 filelist.append( str(uri.append_file_name(file_info.name)) )
316                 except ValueError:
317                         # this can happen when you do not have sufficent
318                         # permissions to read file info.
319                         log("skipping: \'%s\'" % uri)
320         return filelist
322 def vfs_makedirs(path_to_create):
323         """Similar to os.makedirs, but with gnomevfs"""
325         uri = gnomevfs.URI(path_to_create)
326         path = uri.path
328         # start at root
329         uri =  uri.resolve_relative('/')
331         for folder in path.split('/'):
332                 if not folder:
333                         continue
334                 uri = uri.append_string(folder.replace('%2f', '/'))
335                 try:
336                         gnomevfs.make_directory(uri, 0777)
337                 except gnomevfs.FileExistsError:
338                         pass
339                 except :
340                         return False
341         return True
343 def vfs_unlink(filename):
344         gnomevfs.unlink(gnomevfs.URI(filename))
346 def vfs_exists(filename):
347         try:
348                 return gnomevfs.exists(filename)
349         except:
350                 return False
352 def filename_to_uri(filename):
353         """Convert a filename to a valid uri.
354         Filename can be a relative or absolute path, or an uri.
355         """
356         url = urlparse.urlparse(filename)
357         if not url[0]:
358                 filename = urllib.pathname2url(os.path.abspath(filename))
359                 filename = str(gnomevfs.URI(filename))
360         return filename
362 # GStreamer gnomevfssrc helpers
364 def vfs_encode_filename(filename):
365         return filename_to_uri(filename)
366         #return filename.replace('%252f', '/')
369 def file_encode_filename(filename):
370         return gnomevfs.get_local_path_from_uri(filename).replace(' ', '\ ')
371         #filename = filename.replace('%2f', '/');
374 def unquote_filename(filename):
375         return urllib.unquote(filename)
378 def format_tag(tag):
379         if isinstance(tag, list):
380                 if len(tag) > 1:
381                         tag = ', '.join(tag[:-1]) + ' & ' + tag[-1]
382                 else:
383                         tag = tag[0]
385         return tag
387 def markup_escape(message):
388         return gobject.markup_escape_text(message)
390 def __filename_escape(str):
391         return str.replace("'","\'").replace("\"","\\\"").replace('!','\\!')
394 required_elements = ('decodebin', 'fakesink', 'audioconvert', 'typefind')
395 for element in required_elements:
396         if not gst.element_factory_find(element):
397                 print "required gstreamer element \'%s\' not found." % element
398                 sys.exit(1)
400 use_gnomevfs = False
402 if gst.element_factory_find('giosrc'):
403         gstreamer_source = 'giosrc'
404         gstreamer_sink = 'giosink'
405         encode_filename = vfs_encode_filename
406         use_gnomevfs = True
407         print '  using gio'
408 elif gst.element_factory_find('gnomevfssrc'):
409         gstreamer_source = 'gnomevfssrc'
410         gstreamer_sink = 'gnomevfssink'
411         encode_filename = vfs_encode_filename
412         use_gnomevfs = True
413         print '  using deprecated gnomevfssrc'
414 else:
415         gstreamer_source = 'filesrc'
416         gstreamer_sink = 'filesink'
417         encode_filename = file_encode_filename
418         print '  not using gnomevfssrc, look for a gnomevfs gstreamer package.'
421 encoders = (
422         ('flacenc',             'FLAC'),
423         ('wavenc',              'WAV'),
424         ('vorbisenc',   'Ogg Vorbis'),
425         ('oggmux',              'Ogg Vorbis'),
426         ('id3v2mux',    'MP3 Tags'),
427         ('xingmux',             'Xing Header'),
428         ('lame',                'MP3'),
429         ('faac',        'AAC'),
430         ('mp4mux',      'AAC'),
431         )
433 for encoder, name in encoders:
434         have_it = bool(gst.element_factory_find(encoder))
435         if not have_it:
436                 print ("\t'%s' gstreamer element not found"
437                         ", disabling %s." % (encoder, name))
438         exec('have_%s = %s' % (encoder, have_it))
440 if not have_oggmux:
441         have_vorbis = False
443 _GCONF_PROFILE_PATH = "/system/gstreamer/0.10/audio/profiles/"
444 _GCONF_PROFILE_LIST_PATH = "/system/gstreamer/0.10/audio/global/profile_list"
445 audio_profiles_list = []
446 audio_profiles_dict = {}
449 _GCONF = gconf.client_get_default()
450 profiles = _GCONF.get_list(_GCONF_PROFILE_LIST_PATH, 1)
451 for name in profiles:
452         if (_GCONF.get_bool(_GCONF_PROFILE_PATH + name + "/active")):
453                 description = _GCONF.get_string(_GCONF_PROFILE_PATH + name + "/name")
454                 extension = _GCONF.get_string(_GCONF_PROFILE_PATH + name + "/extension")
455                 pipeline = _GCONF.get_string(_GCONF_PROFILE_PATH + name + "/pipeline")
456                 profile = description, extension, pipeline
457                 audio_profiles_list.append(profile)
458                 audio_profiles_dict[description] = profile
460 # logging & debugging
462 def log(*args):
463         if get_option('quiet') == False:
464                 print ' '.join([str(msg) for msg in args])
466 def debug(*args):
467         if get_option('debug') == True:
468                 print ' '.join([str(msg) for msg in args])
470 def gtk_iteration():
471         while gtk.events_pending():
472                 gtk.main_iteration(False)
474 def gtk_sleep(duration):
475         start = time.time()
476         while time.time() < start + duration:
477                 time.sleep(0.010)
478                 gtk_iteration()
480 def UNUSED_display_from_mime(mime):
481         # TODO
482         mime_dict = {
483                 'application/ogg': 'Ogg Vorbis',
484                 'audio/x-wav': 'MS WAV',
485                 'audio/mpeg': 'MPEG 1 Layer 3 (MP3)',
486                 'audio/x-flac': 'FLAC',
487                 'audio/x-musepack': 'MusePack',
488                 'audio/x-au': 'AU',
489         }
490         return mime_dict[mime]
493 class SoundConverterException(Exception):
495         def __init__(self, primary, secondary):
496                 Exception.__init__(self)
497                 self.primary = primary
498                 self.secondary = secondary
502 class SoundFile:
504         """Meta data information about a sound file (uri, tags)."""
506         def __init__(self, uri, base_path=None):
508                 self.uri = uri
510                 if base_path:
511                         self.base_path = base_path
512                         self.filename = self.uri[len(self.base_path):]
513                 else:
514                         self.base_path, self.filename = os.path.split(self.uri)
515                         self.base_path += '/'
517                 self.tags = {
518                         'track-number': 0,
519                         'title':  'Unknown Title',
520                         'artist': 'Unknown Artist',
521                         'album':  'Unknown Album',
522                 }
523                 self.have_tags = False
524                 self.tags_read = False
525                 self.duration = 0
526                 self.mime_type = None
528         def get_uri(self):
529                 return self.uri
531         def get_base_path(self):
532                 return self.base_path
534         def get_filename(self):
535                 return self.filename
537         def get_filename_for_display(self):
538                 return gobject.filename_display_name(
539                                 unquote_filename(self.filename))
541         def add_tags(self, taglist):
542                 for key in taglist.keys():
543                         self.tags[key] = taglist[key]
545         def get_tag_names(self):
546                 return self.tags.key()
548         def get_tag(self, key, default=''):
549                 return self.tags.get(key, default)
551         get = get_tag
552         __getitem__ = get_tag
554         def keys(self):
555                 return self.tags.keys()
558 class TargetNameGenerator:
560         """Generator for creating the target name from an input name."""
562         nice_chars = string.ascii_letters + string.digits + '.-_/'
564         def __init__(self):
565                 self.folder = None
566                 self.subfolders = ''
567                 self.basename= '%(.inputname)s'
568                 self.ext = '%(.ext)s'
569                 self.suffix = None
570                 self.replace_messy_chars = False
571                 self.max_tries = 2
572                 if use_gnomevfs:
573                         self.exists = gnomevfs.exists
574                 else:
575                         self.exists = os.path.exists
577         # This is useful for unit testing.
578         def set_exists(self, exists):
579                 self.exists = exists
581         def set_target_suffix(self, suffix):
582                 self.suffix = suffix
584         def set_folder(self, folder):
585                 self.folder = folder
587         def set_subfolder_pattern(self, pattern):
588                 self.subfolders = pattern
590         def set_basename_pattern(self, pattern):
591                 self.basename = pattern
593         def set_replace_messy_chars(self, yes_or_no):
594                 self.replace_messy_chars = yes_or_no
596         def _unicode_to_ascii(self, unicode_string):
597                 # thanks to http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/251871
598                 try:
599                         unicode_string = unicode(unicode_string, 'utf-8')
600                         return unicodedata.normalize('NFKD', unicode_string).encode('ASCII', 'ignore')
601                 except UnicodeDecodeError:
602                         unicode_string = unicode(unicode_string, 'iso-8859-1')
603                         return unicodedata.normalize('NFKD', unicode_string).encode('ASCII', 'replace')
606         def get_target_name(self, sound_file):
608                 assert self.suffix, 'you just forgot to call set_target_suffix()'
610                 u = gnomevfs.URI(sound_file.get_uri())
611                 root, ext = os.path.splitext(u.path)
612                 if u.host_port:
613                         host = '%s:%s' % (u.host_name, u.host_port)
614                 else:
615                         host = u.host_name
617                 root = sound_file.get_base_path()
618                 basename, ext = os.path.splitext(urllib.unquote(sound_file.get_filename()))
620                 # make sure basename contains only the filename
621                 basefolder, basename = os.path.split(basename)
623                 dict = {
624                         '.inputname': basename,
625                         '.ext': ext,
626                         'album': '',
627                         'artist': '',
628                         'title': '',
629                         'track-number': 0,
630                         'track-count': 0,
631                         'genre': '',
632                         'year': '',
633                         'date': '',
634                 }
635                 for key in sound_file.keys():
636                         dict[key] = sound_file[key]
637                         if isinstance(dict[key], basestring):
638                                 # take care of tags containing slashes
639                                 dict[key] = dict[key].replace('/', '-')
641                 # add timestamp to substitution dict -- this could be split into more
642                 # entries for more fine-grained control over the string by the user...
643                 timestamp_string = time.strftime('%Y%m%d_%H_%M_%S')
644                 dict['timestamp'] = timestamp_string
646                 pattern = os.path.join(self.subfolders, self.basename + self.suffix)
647                 result = pattern % dict
648                 if isinstance(result, unicode):
649                         result = result.encode('utf-8')
651                 if self.replace_messy_chars:
652                         result = self._unicode_to_ascii(result)
653                         s = ''
654                         for c in result:
655                                 if c not in self.nice_chars:
656                                         s += '_'
657                                 else:
658                                         s += c
659                         result = s
662                 if self.folder is None:
663                         # same folder
664                         folder = os.path.join(root, basefolder)
665                 else:
666                         if self.subfolders:
667                                 # destination folder + folders from tags
668                                 folder = self.folder
669                         else:
670                                 # destination folder
671                                 folder = os.path.join(self.folder, basefolder)
673                 result = os.path.join(folder, urllib.quote(result))
675                 return result
678 class ErrorDialog:
680         def __init__(self, glade):
681                 self.dialog = glade.get_widget('error_dialog')
682                 self.primary = glade.get_widget('primary_error_label')
683                 self.secondary = glade.get_widget('secondary_error_label')
685         def show(self, primary, secondary):
686                 self.primary.set_markup(primary)
687                 self.secondary.set_markup(secondary)
688                 self.dialog.run()
689                 self.dialog.hide()
691         def show_exception(self, exception):
692                 self.show('<b>%s</b>' % markup_escape(exception.primary),
693                                         exception.secondary)
696 class ErrorPrinter:
698         def show(self, primary, secondary):
699                 sys.stderr.write(_('\n\nError: %s\n%s\n') % (primary, secondary))
700                 sys.exit(1)
702         def show_exception(self, e):
703                 self.show(e.primary, e.secondary)
706 error = None
708 class BackgroundTask:
710         """A background task.
712         To use: derive a subclass and define the methods started, and
713         finished. Then call the start() method when you want to start the task.
714         You must call done() when the processing is finished.
715         Call the abort() method if you want to stop the task before it finishes
716         normally."""
718         def __init__(self):
719                 self.paused = False
720                 self.running = False
721                 self.current_paused_time = 0
722                 self.listeners = {}
723                 self.progress = None
725         def start(self):
726                 """Start running the task. Call started()."""
727                 try:
728                         self.emit('started')
729                 except SoundConverterException, e:
730                         error.show_exception(e)
731                         return
732                 self.running = True
733                 self.paused = False
734                 self.run_start_time = time.time()
735                 self.current_paused_time = 0
736                 self.paused_time = 0
738         def add_listener(self, signal, listener):
739                 """Add a custom listener to the given signal. Signals are 'started' and 'finished'"""
740                 if signal not in self.listeners:
741                         self.listeners[signal] = []
742                 self.listeners[signal].append(listener)
744         def emit(self, signal):
745                 """Call the signal handlers.
746                 Callbacks are called as gtk idle funcs to be sure they are in the main thread."""
747                 gobject.idle_add(getattr(self, signal))
748                 if signal in self.listeners:
749                         for listener in self.listeners[signal]:
750                                 gobject.idle_add(listener, self)
752         def done(self):
753                 """Call to end normally the task."""
754                 self.run_finish_time = time.time()
755                 if self.running:
756                         self.running = False
757                         self.emit('finished')
759         def abort(self):
760                 """Stop task processing. finished() is not called."""
761                 pass
763         def started(self):
764                 """called when the task starts."""
765                 pass
767         def finished(self):
768                 """Clean up the task after all work has been done."""
769                 pass
772 class TaskQueue(BackgroundTask):
774         """A queue of tasks.
776         A task queue is a queue of other tasks. If you need, for example, to
777         do simple tasks A, B, and C, you can create a TaskQueue and add the
778         simple tasks to it:
780                 q = TaskQueue()
781                 q.add_task(A)
782                 q.add_task(B)
783                 q.add_task(C)
784                 q.start()
786         The task queue behaves as a single task. It will execute the
787         tasks in order and start the next one when the previous finishes."""
789         def __init__(self):
790                 BackgroundTask.__init__(self)
791                 self.waiting_tasks = []
792                 self.running_tasks = []
793                 self.finished_tasks = 0
794                 self.start_time = None
795                 self.count = 0
797         def add_task(self, task):
798                 """Add a task to the queue."""
799                 self.waiting_tasks.append(task)
800                 #if self.start_time and not self.running_tasks:
801                 if self.start_time:
802                         # add a task to a stalled taskqueue, shake it!
803                         self.start_next_task()
805         def get_current_task(self):
806                 if self.running and self.running_tasks:
807                         return self.running_tasks[0]
808                 else:
809                         return None
811         def start_next_task(self):
812                 to_start = get_option('jobs') - len(self.running_tasks)
813                 for i in range(to_start):
814                         try:
815                                 task = self.waiting_tasks.pop(0)
816                         except IndexError:
817                                 if not self.running_tasks:
818                                         self.done()
819                                 return
820                         self.running_tasks.append(task)
821                         task.add_listener('finished', self.task_finished)
822                         task.start()
823                         self.count += 1
824                 total = len(self.waiting_tasks) + self.finished_tasks
825                 self.progress = float(self.finished_tasks) / total if total else 0
827         def started(self):
828                 """ BackgroundTask setup callback """
829                 self.start_time = time.time()
830                 self.count = 0
831                 self.finished_tasks = 0
832                 self.start_next_task()
834         def finished(self):
835                 """ BackgroundTask finish callback """
836                 log('\nQueue done in %.3fs (%s tasks)' % (time.time() - self.start_time, self.count))
837                 self.queue_ended()
838                 self.running = False
840         def task_finished(self, task=None):
841                 if not self.running_tasks:
842                         return
843                 self.running_tasks.remove(task)
844                 self.finished_tasks += 1
845                 self.start_next_task()
847         def abort(self):
848                 for task in self.running_tasks:
849                         task.abort()
850                 BackgroundTask.abort(self)
851                 self.running_tasks = []
852                 self.waiting_tasks = []
853                 self.running = False
855         # The following is called when the Queue is finished
856         def queue_ended(self):
857                 pass
859         # The following when progress changed
860         def progress_hook(self, progress):
861                 pass
864 class NoLink(SoundConverterException):
866         def __init__(self):
867                 SoundConverterException.__init__(self, _('Internal error'),
868                                                                 _("Couldn't link GStreamer elements.\n Please report this as a bug."))
870 class UnknownType(SoundConverterException):
872         def __init__(self, uri, mime_type):
873                 SoundConverterException.__init__(self, _('Unknown type %s') % mime_type,
874                                                                 (_('The file %s is of an unknown type.\n Please ask the developers to add support\n for files of this type if it is important\n to you.')) % uri)
877 class Pipeline(BackgroundTask):
879         """A background task for running a GstPipeline."""
881         def __init__(self):
882                 BackgroundTask.__init__(self)
883                 self.pipeline = None
884                 self.command = []
885                 self.parsed = False
886                 self.signals = []
887                 self.processing = False
888                 self.eos = False
889                 self.error = None
890                 self.connected_signals = []
892         def started(self):
893                 self.play()
895         def finished(self):
896                 for element, sid in self.connected_signals:
897                         element.disconnect(sid)
898                 self.stop_pipeline()
900         def abort(self):
901                 self.finished()
903         def add_command(self, command):
904                 self.command.append(command)
906         def add_signal(self, name, signal, callback):
907                 self.signals.append( (name, signal, callback,) )
909         def toggle_pause(self, paused):
910                 if not self.pipeline:
911                         debug('toggle_pause(): pipeline is None !')
912                         return
914                 if paused:
915                         self.pipeline.set_state(gst.STATE_PAUSED)
916                 else:
917                         self.pipeline.set_state(gst.STATE_PLAYING)
919         def found_tag(self, decoder, something, taglist):
920                 pass
922         def install_plugin_cb(self, result):
923                 if result == gst.pbutils.INSTALL_PLUGINS_SUCCESS:
924                         gst.update_registry()
925                         self.parsed = False
926                         self.play()
927                         return
928                 self.done()
929                 if result == gst.pbutils.INSTALL_PLUGINS_USER_ABORT:
930                         dialog = gtk.MessageDialog(parent=None, flags=gtk.DIALOG_MODAL,
931                                 type=gtk.MESSAGE_INFO,
932                                 buttons=gtk.BUTTONS_OK,
933                                 message_format='Plugin installation aborted.')
934                         dialog.run()
935                         dialog.hide()
936                         return
938                 error.show('Error', 'failed to install plugins: %s' % markup_escape(str(result)))
940         def on_error(self, error):
941                 self.error = error
942                 log('error: %s (%s)' % (error,
943                         self.sound_file.get_filename_for_display()))
945         def on_message(self, bus, message):
946                 t = message.type
947                 import gst
948                 if t == gst.MESSAGE_ERROR:
949                         error, debug = message.parse_error()
950                         self.eos = True
951                         self.on_error(error)
952                         self.done()
954                 elif t == gst.MESSAGE_ELEMENT:
955                         st = message.structure
956                         if st and st.get_name().startswith('missing-'):
957                                 self.pipeline.set_state(gst.STATE_NULL)
958                                 if gst.pygst_version >= (0, 10, 10):
959                                         import gst.pbutils
960                                         detail = gst.pbutils.missing_plugin_message_get_installer_detail(message)
961                                         ctx = gst.pbutils.InstallPluginsContext()
962                                         gst.pbutils.install_plugins_async([detail], ctx, self.install_plugin_cb)
964                 elif t == gst.MESSAGE_EOS:
965                         self.eos = True
966                         self.done()
968                 elif t == gst.MESSAGE_TAG:
969                         self.found_tag(self, '', message.parse_tag())
971                 return True
973         def play(self):
974                 if not self.parsed:
975                         command = ' ! '.join(self.command)
976                         debug('launching: \'%s\'' % command)
977                         print command
978                         try:
979                                 self.pipeline = gst.parse_launch(command)
980                                 bus = self.pipeline.get_bus()
981                                 assert not self.connected_signals
982                                 self.connected_signals = []
983                                 for name, signal, callback in self.signals:
984                                         if name:
985                                                 element = self.pipeline.get_by_name(name)
986                                         else:
987                                                 element = bus
988                                         sid = element.connect(signal,callback)
989                                         self.connected_signals.append((element, sid,))
991                                 self.parsed = True
992                                 del self.command
993                                 del self.signals
994                         except gobject.GError, e:
995                                 error.show('GStreamer error when creating pipeline', str(e))
996                                 self.eos = True # TODO
997                                 self.done()
998                                 return
1000                 bus.add_signal_watch()
1001                 watch_id = bus.connect('message', self.on_message)
1002                 self.watch_id = watch_id
1004                 self.pipeline.set_state(gst.STATE_PLAYING)
1006         def stop_pipeline(self):
1007                 if not self.pipeline:
1008                         debug('pipeline already stopped!')
1009                         return
1010                 bus = self.pipeline.get_bus()
1011                 bus.disconnect(self.watch_id)
1012                 bus.remove_signal_watch()
1013                 self.pipeline.set_state(gst.STATE_NULL)
1014                 self.pipeline = None
1016         def get_position(self):
1017                 return NotImplementedError
1020 class TypeFinder(Pipeline):
1021         def __init__(self, sound_file):
1022                 Pipeline.__init__(self)
1023                 self.sound_file = sound_file
1025                 command = '%s location="%s" ! typefind name=typefinder ! fakesink' % \
1026                         (gstreamer_source, encode_filename(self.sound_file.get_uri()))
1027                 self.add_command(command)
1028                 self.add_signal('typefinder', 'have-type', self.have_type)
1030         def set_found_type_hook(self, found_type_hook):
1031                 self.found_type_hook = found_type_hook
1033         def have_type(self, typefind, probability, caps):
1034                 mime_type = caps.to_string()
1035                 debug('have_type:', mime_type, self.sound_file.get_filename_for_display())
1036                 self.sound_file.mime_type = None
1037                 #self.sound_file.mime_type = mime_type
1038                 for t in mime_whitelist:
1039                         if t in mime_type:
1040                                 self.sound_file.mime_type = mime_type
1041                 if not self.sound_file.mime_type:
1042                         log('Mime type skipped: %s' % mime_type)
1043                 self.pipeline.set_state(gst.STATE_NULL)
1044                 self.done()
1046         def finished(self):
1047                 Pipeline.finished(self)
1048                 if self.error:
1049                         print 'error:', self.error
1050                         return
1051                 if self.found_type_hook and self.sound_file.mime_type:
1052                         gobject.idle_add(self.found_type_hook, self.sound_file, self.sound_file.mime_type)
1053                         self.sound_file.mime_type = True # remove string
1056 class Decoder(Pipeline):
1058         """A GstPipeline background task that decodes data and finds tags."""
1060         def __init__(self, sound_file):
1061                 Pipeline.__init__(self)
1062                 self.sound_file = sound_file
1063                 self.time = 0
1064                 self.position = 0
1065                 self.probe_id = None
1067                 command = '%s location="%s" name=src ! decodebin name=decoder' % \
1068                         (gstreamer_source, encode_filename(self.sound_file.get_uri()))
1069                 self.add_command(command)
1070                 self.add_signal('decoder', 'new-decoded-pad', self.new_decoded_pad)
1072                 # TODO add error management
1074         def have_type(self, typefind, probability, caps):
1075                 pass
1077         def query_duration(self):
1078                 try:
1079                         if not self.sound_file.duration and self.pipeline:
1080                                 self.sound_file.duration = self.pipeline.query_duration(gst.FORMAT_TIME)[0] / gst.SECOND
1081                                 debug('got file duration:', self.sound_file.duration)
1082                 except gst.QueryError:
1083                         pass
1085         def found_tag(self, decoder, something, taglist):
1086                 debug('found_tags:', self.sound_file.get_filename_for_display())
1087                 for k in taglist.keys():
1088                         debug('\t%s=%s' % (k, taglist[k]))
1089                         if isinstance(taglist[k], gst.Date):
1090                                 taglist['year'] = taglist[k].year
1091                                 taglist['date'] = '%04d-%02d-%02d' % (taglist[k].year,
1092                                                                         taglist[k].month, taglist[k].day)
1093                                                                         
1094                 tag_whitelist = (
1095                         'artist',
1096                         'album',
1097                         'title',
1098                         'track-number',
1099                         'track-count',
1100                         'genre',
1101                         'date',
1102                         'year',
1103                         'timestamp',
1104                 )
1105                 tags = {}
1106                 for k in taglist.keys():
1107                         if k in tag_whitelist:
1108                                 tags[k] = taglist[k]
1110                 #print tags
1111                 self.sound_file.add_tags(tags)
1112                 self.sound_file.have_tags = True
1114                 try:
1115                         self.sound_file.duration = self.pipeline.query_duration(gst.FORMAT_TIME)[0] / gst.SECOND
1116                 except gst.QueryError:
1117                         pass
1119         def _buffer_probe(self, pad, buffer):
1120                 """buffer probe callback used to get real time since the beginning of the stream"""
1121                 if buffer.timestamp == gst.CLOCK_TIME_NONE:
1122                         debug('removing buffer probe')
1123                         pad.remove_buffer_probe(self.probe_id)
1124                         return False
1126                 self.position = float(buffer.timestamp) / gst.SECOND
1128                 return True
1130         def new_decoded_pad(self, decoder, pad, is_last):
1131                 """ called when a decoded pad is created """
1132                 self.probe_id = pad.add_buffer_probe(self._buffer_probe)
1133                 self.probed_pad = pad
1134                 self.processing = True
1135                 self.query_duration()
1137         def finished(self):
1138                 if self.probe_id:
1139                         self.probed_pad.remove_buffer_probe(self.probe_id)
1140                 Pipeline.finished(self)
1142         def get_sound_file(self):
1143                 return self.sound_file
1145         def get_input_uri(self):
1146                 return self.sound_file.get_uri()
1148         def get_duration(self):
1149                 """ return the total duration of the sound file """
1150                 self.query_duration()
1151                 return self.sound_file.duration
1153         def get_position(self):
1154                 """ return the current pipeline position in the stream """
1155                 return self.position
1158 class TagReader(Decoder):
1160         """A GstPipeline background task for finding meta tags in a file."""
1162         def __init__(self, sound_file):
1163                 Decoder.__init__(self, sound_file)
1164                 self.found_tag_hook = None
1165                 self.found_tags = False
1166                 self.run_start_time = 0
1167                 self.add_command('fakesink')
1168                 self.add_signal(None, 'message::state-changed', self.on_state_changed)
1169                 self.tagread = False
1171         def set_found_tag_hook(self, found_tag_hook):
1172                 self.found_tag_hook = found_tag_hook
1174         def on_state_changed(self, bus, message):
1175                 prev, new, pending = message.parse_state_changed()
1176                 if new == gst.STATE_PLAYING and not self.tagread:
1177                         self.tagread = True
1178                         debug('TagReading done...')
1179                         self.done()
1181         def finished(self):
1182                 Pipeline.finished(self)
1183                 self.sound_file.tags_read = True
1184                 if self.found_tag_hook:
1185                         gobject.idle_add(self.found_tag_hook, self)
1188 class ConversionTargetExists(SoundConverterException):
1190         def __init__(self, uri):
1191                 SoundConverterException.__init__(self, _('Target exists.'),
1192                                                                                  (_('The output file %s already exists.')) % uri)
1195 class Converter(Decoder):
1197         """A background task for converting files to another format."""
1199         def __init__(self, sound_file, output_filename, output_type, delete_original=False, output_resample=False, resample_rate=48000, force_mono=False):
1200                 Decoder.__init__(self, sound_file)
1202                 self.converting = True
1204                 self.output_filename = output_filename
1205                 self.output_type = output_type
1206                 self.vorbis_quality = None
1207                 self.aac_quality = None
1208                 self.mp3_bitrate = None
1209                 self.mp3_mode = None
1210                 self.mp3_quality = None
1211                 self.wav_sample_width = 16
1212                 self.flac_compression = 8
1214                 self.output_resample = output_resample
1215                 self.resample_rate = resample_rate
1216                 self.force_mono = force_mono
1218                 self.overwrite = False
1219                 self.delete_original = delete_original
1221         def init(self):
1222                 self.encoders = {
1223                         'audio/x-vorbis': self.add_oggvorbis_encoder,
1224                         'audio/x-flac': self.add_flac_encoder,
1225                         'audio/x-wav': self.add_wav_encoder,
1226                         'audio/mpeg': self.add_mp3_encoder,
1227                         'audio/x-m4a': self.add_aac_encoder,
1228                         'gst-profile': self.add_audio_profile,
1229                 }
1231                 self.add_command('audioconvert')
1232                 #TODO self.add_command('audioscale')
1234                 #Hacked in audio resampling support
1235                 if self.output_resample:
1236                         self.add_command('audioresample ! audio/x-raw-float,rate=%d' %
1237                                          (self.resample_rate))
1238                         self.add_command('audioconvert')
1240                 if self.force_mono:
1241                         self.add_command('audioresample ! audio/x-raw-float,channels=1')
1242                         self.add_command('audioconvert')
1244                 encoder = self.encoders[self.output_type]()
1245                 if not encoder:
1246                         # TODO: add proper error management when an encoder cannot be created
1247                         dialog = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR,
1248                                                 gtk.BUTTONS_OK, _("Cannot create a decoder for \'%s\' format.") % \
1249                                                 self.output_type )
1250                         dialog.run()
1251                         dialog.hide()
1252                         return
1254                 self.add_command(encoder)
1256                 uri = gnomevfs.URI(self.output_filename)
1257                 dirname = uri.parent
1258                 if dirname and not gnomevfs.exists(dirname):
1259                         log('Creating folder: \'%s\'' % dirname)
1260                         if not vfs_makedirs(str(dirname)):
1261                                 # TODO add error management
1262                                 dialog = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR,
1263                                                         gtk.BUTTONS_OK, _("Cannot create \'%s\' folder.") % \
1264                                                         dirname )
1265                                 dialog.run()
1266                                 dialog.hide()
1267                                 return
1269                 self.add_command('%s location="%s"' % (
1270                         gstreamer_sink, encode_filename(self.output_filename)))
1271                 if self.overwrite and vfs_exists(self.output_filename):
1272                         log('overwriting \'%s\'' % self.output_filename)
1273                         vfs_unlink(self.output_filename)
1275         def finished(self):
1276                 self.converting = False
1277                 Pipeline.finished(self)
1279                 # Copy file permissions
1280                 try:
1281                         info = gnomevfs.get_file_info( self.sound_file.get_uri(),gnomevfs.FILE_INFO_FIELDS_PERMISSIONS)
1282                         gnomevfs.set_file_info(self.output_filename, info, gnomevfs.SET_FILE_INFO_PERMISSIONS)
1283                 except:
1284                         log('Cannot set permission on \'%s\'' % gnomevfs.format_uri_for_display(self.output_filename))
1286                 if self.delete_original and self.processing and not self.error:
1287                         log('deleting: \'%s\'' % self.sound_file.get_uri())
1288                         try:
1289                                 gnomevfs.unlink(self.sound_file.get_uri())
1290                         except:
1291                                 log('Cannot remove \'%s\'' % gnomevfs.format_uri_for_display(self.output_filename))
1293         def on_error(self, err):
1294                 error.show('<b>%s</b>' % _('GStreamer Error:'), '%s\n<i>(%s)</i>' % (err,
1295                         self.sound_file.get_filename_for_display()))
1297         def set_vorbis_quality(self, quality):
1298                 self.vorbis_quality = quality
1300         def set_aac_quality(self, quality):
1301                 self.aac_quality = quality
1303         def set_mp3_mode(self, mode):
1304                 self.mp3_mode = mode
1306         def set_mp3_quality(self, quality):
1307                 self.mp3_quality = quality
1309         def set_flac_compression(self, compression):
1310                 self.flac_compression = compression
1312         def set_wav_sample_width(self, sample_width):
1313                 self.wav_sample_width = sample_width
1315         def set_audio_profile(self, audio_profile):
1316                 self.audio_profile = audio_profile
1318         def add_flac_encoder(self):
1319                 s = 'flacenc mid-side-stereo=true quality=%s' % self.flac_compression
1320                 return s
1322         def add_wav_encoder(self):
1323                 return 'audio/x-raw-int,width=%d ! audioconvert ! wavenc' % self.wav_sample_width
1325         def add_oggvorbis_encoder(self):
1326                 cmd = 'vorbisenc'
1327                 if self.vorbis_quality is not None:
1328                         cmd += ' quality=%s' % self.vorbis_quality
1329                 cmd += ' ! oggmux '
1330                 return cmd
1332         def add_mp3_encoder(self):
1334                 cmd = 'lame quality=2 '
1336                 if self.mp3_mode is not None:
1337                         properties = {
1338                                 'cbr' : (0,'bitrate'),
1339                                 'abr' : (3,'vbr-mean-bitrate'),
1340                                 'vbr' : (4,'vbr-quality')
1341                         }
1343                         cmd += 'vbr=%s ' % properties[self.mp3_mode][0]
1344                         if self.mp3_quality == 9:
1345                                 # GStreamer set max bitrate to 320 but lame uses
1346                                 # mpeg2 with vbr-quality==9, so max bitrate is 160
1347                                 # - update: now set to 128 since lame don't accept 160 anymore.
1348                                 cmd += 'vbr-max-bitrate=128 '
1349                         elif properties[self.mp3_mode][0]:
1350                                 cmd += 'vbr-max-bitrate=320 '
1351                         cmd += '%s=%s ' % (properties[self.mp3_mode][1], self.mp3_quality)
1353                         if have_xingmux and properties[self.mp3_mode][0]:
1354                                 # add xing header when creating VBR mp3
1355                                 cmd += '! xingmux '
1357                 if have_id3v2mux:
1358                         # add tags
1359                         cmd += '! id3v2mux '
1361                 return cmd
1363         def add_aac_encoder(self):
1364                 return 'faac profile=2 bitrate=%s ! mp4mux' % \
1365                         (self.aac_quality * 1000)
1367         def add_audio_profile(self):
1368                 pipeline = audio_profiles_dict[self.audio_profile][2]
1369                 return pipeline
1372 class FileList:
1373         """List of files added by the user."""
1375         # List of MIME types which we accept for drops.
1376         drop_mime_types = ['text/uri-list', 'text/plain', 'STRING']
1378         def __init__(self, window, glade):
1379                 self.window = window
1380                 self.typefinders = TaskQueue()
1381                 self.tagreaders  = TaskQueue()
1382                 self.filelist={}
1384                 args = []
1385                 for name in ALL_COLUMNS:
1386                         if name in VISIBLE_COLUMNS:
1387                                 args.append(gobject.TYPE_STRING)
1388                         else:
1389                                 args.append(gobject.TYPE_PYOBJECT)
1390                 self.model = apply(gtk.ListStore, args)
1392                 self.widget = glade.get_widget('filelist')
1393                 self.widget.set_model(self.model)
1394                 self.widget.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
1396                 self.widget.drag_dest_set(gtk.DEST_DEFAULT_ALL,
1397                                                                         map(lambda i:
1398                                                                                 (self.drop_mime_types[i], 0, i),
1399                                                                                 range(len(self.drop_mime_types))),
1400                                                                                 gtk.gdk.ACTION_COPY)
1401                 self.widget.connect('drag_data_received', self.drag_data_received)
1403                 renderer = gtk.CellRendererText()
1404                 for name in VISIBLE_COLUMNS:
1405                         column = gtk.TreeViewColumn(name,
1406                                                                                 renderer,
1407                                                                                 markup=ALL_COLUMNS.index(name))
1408                         self.widget.append_column(column)
1409                 self.window.progressbarstatus.hide()
1411         def drag_data_received(self, widget, context, x, y, selection,
1412                                                          mime_id, time):
1414                 if mime_id >= 0 and mime_id < len(self.drop_mime_types):
1415                         file_list = []
1416                         self.add_uris([uri.strip() for uri in selection.data.split('\n')])
1417                         context.finish(True, False, time)
1419         def get_files(self):
1420                 files = []
1421                 i = self.model.get_iter_first()
1422                 while i:
1423                         f = {}
1424                         for c in ALL_COLUMNS:
1425                                 f[c] = self.model.get_value(i, ALL_COLUMNS.index(c))
1426                         files.append(f['META'])
1428                         i = self.model.iter_next(i)
1429                 return files
1431         def update_progress(self, queue):
1432                 if queue.running:
1433                         progress = queue.progress
1434                         self.window.progressbarstatus.set_fraction(progress if progress else 0)
1435                         return True
1436                 return False
1439         def found_type(self, sound_file, mime):
1440                 debug('found_type', sound_file.get_filename())
1442                 self.append_file(sound_file)
1443                 self.window.set_sensitive()
1445                 tagreader = TagReader(sound_file)
1446                 tagreader.set_found_tag_hook(self.append_file_tags)
1448                 self.tagreaders.add_task(tagreader)
1450         def add_uris(self, uris, base=None, extensions=None):
1451                 files = []
1452                 self.window.set_status(_('Adding files...'))
1454                 for uri in uris:
1455                         if not uri:
1456                                 continue
1457                         if uri.startswith('cdda:'):
1458                                 error.show('Cannot read from Audio CD.',
1459                                         'Use SoundJuicer Audio CD Extractor instead.')
1460                                 return
1461                         try:
1462                                 info = gnomevfs.get_file_info(gnomevfs.URI(uri), gnomevfs.FILE_INFO_FOLLOW_LINKS)
1463                         except gnomevfs.NotFoundError:
1464                                 log('uri not found: \'%s\'' % uri)
1465                                 continue
1466                         except gnomevfs.InvalidURIError:
1467                                 log('unvalid uri: \'%s\'' % uri)
1468                                 continue
1469                         except gnomevfs.AccessDeniedError:
1470                                 log('access denied: \'%s\'' % uri)
1471                                 continue
1472                         except TypeError, e:
1473                                 log('add error: %s (\'%s\')' % (e, uri))
1474                                 continue
1475                         except :
1476                                 log('error in get_file_info: %s' % (uri))
1477                                 continue
1479                         if info.type == gnomevfs.FILE_TYPE_DIRECTORY:
1480                                 log('walking: \'%s\'' % uri)
1481                                 filelist = vfs_walk(gnomevfs.URI(uri))
1482                                 accepted = []
1483                                 if extensions:
1484                                         for f in filelist:
1485                                                 for extension in extensions:
1486                                                         if f.lower().endswith(extension):
1487                                                                 accepted.append(f)
1488                                         filelist = accepted
1490                                 files.extend(filelist)
1491                         else:
1492                                 files.append(uri)
1494                 base,notused = os.path.split(os.path.commonprefix(files))
1495                 base += '/'
1497                 for f in files:
1498                         sound_file = SoundFile(f, base)
1499                         if sound_file.get_uri() in self.filelist:
1500                                 log('file already present: \'%s\'' % sound_file.get_uri())
1501                                 continue
1503                         self.filelist[sound_file.get_uri()] = True
1505                         typefinder = TypeFinder(sound_file)
1506                         typefinder.set_found_type_hook(self.found_type)
1507                         self.typefinders.add_task(typefinder)
1509                 self.skiptags = False #len(files) > 100
1510                 if self.skiptags:
1511                         log(_('too much files, skipping tag reading.'))
1512                         for i in self.model:
1513                                 i[0] = self.format_cell(i[1])
1515                 if files and not self.typefinders.running:
1516                         self.window.progressbarstatus.show()
1517                         self.typefinders.queue_ended = self.typefinder_queue_ended
1518                         self.typefinders.start()
1519                         gobject.timeout_add(100, self.update_progress, self.typefinders)
1520                         self.tagreaders.queue_ended = self.tagreader_queue_ended
1521                 else:
1522                         self.window.set_status()
1525         def typefinder_queue_ended(self):
1527                 if self.skiptags:
1528                         self.window.set_status()
1529                         self.window.progressbarstatus.hide()
1530                 else:
1531                         if not self.tagreaders.running:
1532                                 self.window.set_status(_('Reading tags...'))
1533                                 self.tagreaders.start()
1534                                 gobject.timeout_add(100, self.update_progress, self.tagreaders)
1535                                 self.tagreaders.queue_ended = self.tagreader_queue_ended
1538         def tagreader_queue_ended(self):
1539                 self.window.progressbarstatus.hide()
1540                 self.window.set_status()
1542         def abort(self):
1543                 self.typefinders.abort()
1544                 self.tagreaders.abort()
1547         def format_cell(self, sound_file):
1548                 template_tags = '%(artist)s - <i>%(album)s</i> - <b>%(title)s</b>\n<small>%(filename)s</small>'
1549                 template_loading = '<i>%s</i>\n<small>%%(filename)s</small>' \
1550                                                         % _('loading tags...')
1551                 template_notags  = '<span foreground=\'red\'>%s</span>\n<small>%%(filename)s</small>' \
1552                                                         % _('no tags')
1553                 template_skiptags  = '%(filename)s'
1555                 params = {}
1556                 params['filename'] = markup_escape(unquote_filename(sound_file.get_filename()))
1557                 for item in ('title', 'artist', 'album'):
1558                         params[item] = markup_escape(format_tag(sound_file.get_tag(item)))
1559                 if sound_file['bitrate']:
1560                         params['bitrate'] = ', %s kbps' % (sound_file['bitrate'] / 1000)
1561                 else:
1562                         params['bitrate'] = ''
1564                 if sound_file.have_tags:
1565                         template = template_tags
1566                 else:
1567                         if self.skiptags:
1568                                 template = template_skiptags
1569                         elif sound_file.tags_read:
1570                                 template = template_notags
1571                         else:
1572                                 template = template_loading
1574                 for tag, unicode_string in params.items():
1575                         try:
1576                                 if not isinstance(unicode_string, unicode):
1577                                         # try to convert from utf-8
1578                                         unicode_string = unicode(unicode_string, 'utf-8')
1579                         except UnicodeDecodeError:
1580                                 # well, let's fool python and use some 8bit codec...
1581                                 unicode_string = unicode(unicode_string, 'iso-8859-1')
1582                         params[tag] = unicode_string
1584                 s = template % params
1586                 return s
1588         def append_file(self, sound_file):
1589                 iter = self.model.append([self.format_cell(sound_file), sound_file])
1592         def append_file_tags(self, tagreader):
1593                 sound_file = tagreader.get_sound_file()
1595                 fields = {}
1596                 for key in ALL_COLUMNS:
1597                         fields[key] = _('unknown')
1598                 fields['META'] = sound_file
1599                 fields['filename'] = sound_file.get_filename_for_display()
1601                 # TODO: SLOW!
1602                 for i in self.model:
1603                         if i[1] == sound_file:
1604                                 i[0] = self.format_cell(sound_file)
1605                 self.window.set_sensitive()
1607         def remove(self, iter):
1608                 uri = self.model.get(iter, 1)[0].get_uri()
1609                 del self.filelist[uri]
1610                 self.model.remove(iter)
1612         def is_nonempty(self):
1613                 try:
1614                         self.model.get_iter((0,))
1615                 except ValueError:
1616                         return False
1617                 return True
1620 class GladeWindow(object):
1622         def __init__(self, glade):
1623                 self.glade = glade
1624                 glade.signal_autoconnect(self)
1626         def __getattr__(self, attribute):
1627                 '''Allow direct use of window widget.'''
1628                 widget = self.glade.get_widget(attribute)
1629                 if widget is None:
1630                         raise AttributeError('Widget \'%s\' not found' % attribute)
1631                 self.__dict__[attribute] = widget # cache result
1632                 return widget
1635 class GConfStore(object):
1637         def __init__(self, root, defaults):
1638                 self.gconf = gconf.client_get_default()
1639                 self.gconf.add_dir(root, gconf.CLIENT_PRELOAD_ONELEVEL)
1640                 self.root = root
1641                 self.defaults = defaults
1643         def get_with_default(self, getter, key):
1644                 if self.gconf.get(self.path(key)) is None:
1645                         return self.defaults[key]
1646                 else:
1647                         return getter(self.path(key))
1649         def get_int(self, key):
1650                 return self.get_with_default(self.gconf.get_int, key)
1652         def set_int(self, key, value):
1653                 self.gconf.set_int(self.path(key), value)
1655         def get_float(self, key):
1656                 return self.get_with_default(self.gconf.get_float, key)
1658         def set_float(self, key, value):
1659                 self.gconf.set_float(self.path(key), value)
1661         def get_string(self, key):
1662                 return self.get_with_default(self.gconf.get_string, key)
1664         def set_string(self, key, value):
1665                 self.gconf.set_string(self.path(key), value)
1667         def path(self, key):
1668                 assert self.defaults.has_key(key), 'missing gconf default:%s' % key
1669                 return '%s/%s' % (self.root, key)
1672 class PreferencesDialog(GladeWindow, GConfStore):
1674         basename_patterns = [
1675                 ('%(.inputname)s', _('Same as input, but replacing the suffix')),
1676                 ('%(.inputname)s%(.ext)s', _('Same as input, but with an additional suffix')),
1677                 ('%(track-number)02d-%(title)s', _('Track number - title')),
1678                 ('%(title)s', _('Track title')),
1679                 ('%(artist)s-%(title)s', _('Artist - title')),
1680                 ('Custom', _('Custom filename pattern')),
1681         ]
1683         subfolder_patterns = [
1684                 ('%(artist)s/%(album)s', _('artist/album')),
1685                 ('%(artist)s-%(album)s', _('artist-album')),
1686                 ('%(artist)s - %(album)s', _('artist - album')),
1687         ]
1689         defaults = {
1690                 'same-folder-as-input': 1,
1691                 'selected-folder': os.path.expanduser('~'),
1692                 'create-subfolders': 0,
1693                 'subfolder-pattern-index': 0,
1694                 'name-pattern-index': 0,
1695                 'custom-filename-pattern': '{Track} - {Title}',
1696                 'replace-messy-chars': 0,
1697                 'output-mime-type': 'audio/x-vorbis',
1698                 'output-suffix': '.ogg',
1699                 'vorbis-quality': 0.6,
1700                 'vorbis-oga-extension': 0,
1701                 'mp3-mode': 'vbr',                      # 0: cbr, 1: abr, 2: vbr
1702                 'mp3-cbr-quality': 192,
1703                 'mp3-abr-quality': 192,
1704                 'mp3-vbr-quality': 3,
1705                 'aac-quality': 192,
1706                 'flac-compression': 8,
1707                 'wav-sample-width': 16,
1708                 'delete-original': 0,
1709                 'output-resample': 0,
1710                 'resample-rate': 48000,
1711                 'flac-speed': 0,
1712                 'force-mono': 0,
1713                 'last-used-folder': None,
1714                 'audio-profile': None,
1715         }
1717         sensitive_names = ['vorbis_quality', 'choose_folder', 'create_subfolders',
1718                                                  'subfolder_pattern']
1720         def __init__(self, glade):
1721                 GladeWindow.__init__(self, glade)
1722                 GConfStore.__init__(self, '/apps/SoundConverter', self.defaults)
1724                 self.dialog = glade.get_widget('prefsdialog')
1725                 self.example = glade.get_widget('example_filename')
1726                 self.force_mono = glade.get_widget('force-mono')
1728                 self.target_bitrate = None
1729                 self.convert_setting_from_old_version()
1731                 self.sensitive_widgets = {}
1732                 for name in self.sensitive_names:
1733                         self.sensitive_widgets[name] = glade.get_widget(name)
1734                         assert self.sensitive_widgets[name] != None
1735                 self.set_widget_initial_values(glade)
1736                 self.set_sensitive()
1738                 tip = [_('Available patterns:')]
1739                 for k in locale_patterns_dict.values():
1740                         tip.append(k)
1741                 self.custom_filename.set_tooltip_text('\n'.join(tip))
1744         def convert_setting_from_old_version(self):
1745                 """ try to convert previous settings"""
1747                 # vorbis quality was once stored as an int enum
1748                 try:
1749                         self.get_float('vorbis-quality')
1750                 except gobject.GError:
1751                         log('deleting old settings...')
1752                         [self.gconf.unset(self.path(k)) for k in self.defaults.keys()]
1754                 self.gconf.clear_cache()
1756         def set_widget_initial_values(self, glade):
1758                 self.quality_tabs.set_show_tabs(False)
1760                 if self.get_int('same-folder-as-input'):
1761                         w = self.same_folder_as_input
1762                 else:
1763                         w = self.into_selected_folder
1764                 w.set_active(True)
1766                 uri = filename_to_uri(self.get_string('selected-folder'))
1767                 self.target_folder_chooser.set_uri(uri)
1768                 self.update_selected_folder()
1770                 w = self.create_subfolders
1771                 w.set_active(self.get_int('create-subfolders'))
1773                 w = self.subfolder_pattern
1774                 active = self.get_int('subfolder-pattern-index')
1775                 model = w.get_model()
1776                 model.clear()
1777                 for pattern, desc in self.subfolder_patterns:
1778                         i = model.append()
1779                         model.set(i, 0, desc)
1780                 w.set_active(active)
1782                 if self.get_int('replace-messy-chars'):
1783                         w = self.replace_messy_chars
1784                         w.set_active(True)
1786                 if self.get_int('delete-original'):
1787                         self.delete_original.set_active(True)
1789                 mime_type = self.get_string('output-mime-type')
1791                 widgets = (     ('audio/x-vorbis', have_vorbisenc),
1792                                         ('audio/mpeg'    , have_lame),
1793                                         ('audio/x-flac'  , have_flacenc),
1794                                         ('audio/x-wav'   , have_wavenc),
1795                                         ('audio/x-m4a'   , have_faac),
1796                                         ('gst-profile'   , True),
1797                                         ) # must be in same order in output_mime_type
1799                 # desactivate output if encoder plugin is not present
1800                 widget = self.output_mime_type
1801                 model = widget.get_model()
1802                 assert len(model) == len(widgets), 'model:%d widgets:%d' % (len(model), len(widgets))
1804                 self.present_mime_types = []
1805                 i = 0
1806                 for b in widgets:
1807                         mime, encoder_present = b
1808                         if not encoder_present:
1809                                 del model[i]
1810                                 if mime_type == mime:
1811                                         mime_type = self.defaults['output-mime-type']
1812                         else:
1813                                 self.present_mime_types.append(mime)
1814                                 i += 1
1815                 for i, mime in enumerate(self.present_mime_types):
1816                         if mime_type == mime:
1817                                 widget.set_active(i)
1818                 self.change_mime_type(mime_type)
1820                 # display information about mp3 encoding
1821                 if not have_lame:
1822                         w = self.lame_absent
1823                         w.show()
1825                 w = self.vorbis_quality
1826                 quality = self.get_float('vorbis-quality')
1827                 quality_setting = {0:0 ,0.2:1 ,0.4:2 ,0.6:3 , 0.8:4, 1.0:5}
1828                 for k, v in quality_setting.iteritems():
1829                         if abs(quality-k) < 0.01:
1830                                 self.vorbis_quality.set_active(v)
1831                 if self.get_int('vorbis-oga-extension'):
1832                         self.vorbis_oga_extension.set_active(True)
1834                 w = self.aac_quality
1835                 quality = self.get_int('aac-quality')
1836                 quality_setting = {64:0, 96:1, 128:2, 192:3, 256:4, 320:5}
1837                 w.set_active(quality_setting.get(quality, -1))
1839                 w = self.flac_compression
1840                 quality = self.get_int('flac-compression')
1841                 quality_setting = {0:0, 5:1, 8:2}
1842                 w.set_active(quality_setting.get(quality, -1))
1844                 w = self.wav_sample_width
1845                 quality = self.get_int('wav-sample-width')
1846                 quality_setting = {8:0, 16:1, 32:2}
1847                 w.set_active(quality_setting.get(quality, -1))
1849                 self.mp3_quality = self.mp3_quality
1850                 self.mp3_mode = self.mp3_mode
1852                 mode = self.get_string('mp3-mode')
1853                 self.change_mp3_mode(mode)
1855                 w = self.basename_pattern
1856                 active = self.get_int('name-pattern-index')
1857                 model = w.get_model()
1858                 model.clear()
1859                 for pattern, desc in self.basename_patterns:
1860                         iter = model.append()
1861                         model.set(iter, 0, desc)
1862                 w.set_active(active)
1864                 self.custom_filename.set_text(self.get_string('custom-filename-pattern'))
1865                 if self.basename_pattern.get_active() == len(self.basename_patterns)-1:
1866                         self.custom_filename_box.set_sensitive(True)
1867                 else:
1868                         self.custom_filename_box.set_sensitive(False)
1870                 if self.get_int('output-resample'):
1871                         self.resample_toggle.set_active(self.get_int('output-resample'))
1872                         self.resample_rate.set_sensitive(1)
1873                         rates = [11025, 22050, 44100, 48000, 72000, 96000, 128000]
1874                         rate = self.get_int('resample-rate')
1875                         try:
1876                                 idx = rates.index(rate)
1877                         except ValueError:
1878                                 self.resample_rate.insert_text(0, str(rate))
1879                                 idx = 0
1880                         self.resample_rate.set_active(idx)
1882                 self.force_mono.set_active(self.get_int('force-mono'))
1885                 for i, profile in enumerate(audio_profiles_list):
1886                         description, extension, pipeline = profile
1887                         self.gstprofile.append_text('%s (.%s)' % (description, extension))
1888                         if description == self.get_string('audio-profile'):
1889                                 self.gstprofile.set_active(i)
1892                 self.update_example()
1894         def update_selected_folder(self):
1895                 self.into_selected_folder.set_use_underline(False)
1896                 self.into_selected_folder.set_label(_('Into folder %s') %
1897                         beautify_uri(self.get_string('selected-folder')))
1900         def get_bitrate_from_settings(self):
1901                 bitrate = 0
1902                 aprox = True
1903                 mode = self.get_string('mp3-mode')
1905                 mime_type = self.get_string('output-mime-type')
1907                 if mime_type == 'audio/x-vorbis':
1908                         quality = self.get_float('vorbis-quality')*10
1909                         quality = int(quality)
1910                         bitrates = (64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 500)
1911                         bitrate = bitrates[quality]
1913                 elif mime_type == 'audio/x-m4a':
1914                         bitrate = self.get_int('aac-quality')
1916                 elif mime_type == 'audio/mpeg':
1917                         quality = {
1918                                 'cbr': 'mp3-cbr-quality',
1919                                 'abr': 'mp3-abr-quality',
1920                                 'vbr': 'mp3-vbr-quality'
1921                         }
1922                         bitrate = self.get_int(quality[mode])
1923                         if mode == 'vbr':
1924                                 # hum, not really, but who cares? :)
1925                                 bitrates = (320, 256, 224, 192, 160, 128, 112, 96, 80, 64)
1926                                 bitrate = bitrates[bitrate]
1927                         if mode == 'cbr':
1928                                 aprox = False
1930                 if bitrate:
1931                         if aprox:
1932                                 return '~%d kbps' % bitrate
1933                         else:
1934                                 return '%d kbps' % bitrate
1935                 else:
1936                         return 'N/A'
1939         def update_example(self):
1940                 sound_file = SoundFile('foo/bar.flac')
1941                 sound_file.add_tags({
1942                         'track-number': 1L,
1943                         'track-count': 99L,
1944                 })
1945                 sound_file.add_tags(locale_patterns_dict)
1947                 s = markup_escape(beautify_uri(self.generate_filename(sound_file, for_display=True)))
1948                 p = 0
1949                 replaces = []
1951                 while 1:
1952                         b = s.find('{', p)
1953                         if b == -1:
1954                                 break
1955                         e = s.find('}',b)
1957                         tag = s[b:e+1]
1958                         if tag.lower() in [v.lower() for v in locale_patterns_dict.values()]:
1959                                 k = tag
1960                                 l = k.replace('{','<b>{')
1961                                 l = l.replace('}','}</b>')
1962                                 replaces.append([k,l])
1963                         else:
1964                                 k = tag
1965                                 l = k.replace('{','<span foreground=\'red\'><i>{')
1966                                 l = l.replace('}','}</i></span>')
1967                                 replaces.append([k,l])
1968                         p = b+1
1970                 for k,l in replaces:
1971                         s = s.replace(k, l)
1973                 self.example.set_markup(s)
1975                 markup = '<small>%s</small>' % (_('Target bitrate: %s') %
1976                                         self.get_bitrate_from_settings())
1977                 self.aprox_bitrate.set_markup( markup )
1979         def generate_filename(self, sound_file, for_display=False):
1980                 self.gconf.clear_cache()
1981                 output_type = self.get_string('output-mime-type')
1982                 profile = self.get_string('audio-profile')
1983                 profile_ext = audio_profiles_dict[profile][1] if profile else ''
1984                 output_suffix = {
1985                                                 'audio/x-vorbis': '.ogg',
1986                                                 'audio/x-flac': '.flac',
1987                                                 'audio/x-wav': '.wav',
1988                                                 'audio/mpeg': '.mp3',
1989                         'audio/x-m4a': '.m4a',
1990                         'gst-profile': '.' + profile_ext,
1991                                         }.get(output_type, None)
1993                 generator = TargetNameGenerator()
1995                 if output_suffix == '.ogg' and self.get_int('vorbis-oga-extension'):
1996                         output_suffix = '.oga'
1998                 generator.set_target_suffix(output_suffix)
2000                 if not self.get_int('same-folder-as-input'):
2001                         folder = self.get_string('selected-folder')
2002                         folder = filename_to_uri(folder)
2003                         generator.set_folder(folder)
2005                 if self.get_int('create-subfolders'):
2006                         generator.set_subfolder_pattern(
2007                                 self.get_subfolder_pattern())
2009                 generator.set_basename_pattern(self.get_basename_pattern())
2010                 if for_display:
2011                         generator.set_replace_messy_chars(False)
2012                         return unquote_filename(generator.get_target_name(sound_file))
2013                 else:
2014                         generator.set_replace_messy_chars(
2015                                 self.get_int('replace-messy-chars'))
2016                         return generator.get_target_name(sound_file)
2018         def process_custom_pattern(self, pattern):
2020                 for k in custom_patterns:
2021                         pattern = pattern.replace(k, custom_patterns[k])
2022                 return pattern
2024         def set_sensitive(self):
2026                 #TODO
2027                 return
2029                 for widget in self.sensitive_widgets.values():
2030                         widget.set_sensitive(False)
2032                 x = self.get_int('same-folder-as-input')
2033                 for name in ['choose_folder', 'create_subfolders',
2034                                          'subfolder_pattern']:
2035                         self.sensitive_widgets[name].set_sensitive(not x)
2037                 self.sensitive_widgets['vorbis_quality'].set_sensitive(
2038                         self.get_string('output-mime-type') == 'audio/x-vorbis')
2042         def run(self):
2043                 self.dialog.run()
2044                 self.dialog.hide()
2046         def on_delete_original_toggled(self, button):
2047                 if button.get_active():
2048                         self.set_int('delete-original', 1)
2049                 else:
2050                         self.set_int('delete-original', 0)
2052         def on_same_folder_as_input_toggled(self, button):
2053                 if button.get_active():
2054                         self.set_int('same-folder-as-input', 1)
2055                         self.set_sensitive()
2056                         self.update_example()
2058         def on_into_selected_folder_toggled(self, button):
2059                 if button.get_active():
2060                         self.set_int('same-folder-as-input', 0)
2061                         self.set_sensitive()
2062                         self.update_example()
2064         def on_choose_folder_clicked(self, button):
2065                 ret = self.target_folder_chooser.run()
2066                 self.target_folder_chooser.hide()
2067                 if ret == gtk.RESPONSE_OK:
2068                         folder = self.target_folder_chooser.get_uri()
2069                         if folder:
2070                                 self.set_string('selected-folder', urllib.unquote(folder))
2071                                 self.update_selected_folder()
2072                                 self.update_example()
2074         def on_create_subfolders_toggled(self, button):
2075                 if button.get_active():
2076                         self.set_int('create-subfolders', 1)
2077                 else:
2078                         self.set_int('create-subfolders', 0)
2079                 self.update_example()
2081         def on_subfolder_pattern_changed(self, combobox):
2082                 self.set_int('subfolder-pattern-index', combobox.get_active())
2083                 self.update_example()
2085         def get_subfolder_pattern(self):
2086                 index = self.get_int('subfolder-pattern-index')
2087                 if index < 0 or index >= len(self.subfolder_patterns):
2088                         index = 0
2089                 return self.subfolder_patterns[index][0]
2091         def on_basename_pattern_changed(self, combobox):
2092                 self.set_int('name-pattern-index', combobox.get_active())
2093                 if combobox.get_active() == len(self.basename_patterns)-1:
2094                         self.custom_filename_box.set_sensitive(True)
2095                 else:
2096                         self.custom_filename_box.set_sensitive(False)
2097                 self.update_example()
2099         def get_basename_pattern(self):
2100                 index = self.get_int('name-pattern-index')
2101                 if index < 0 or index >= len(self.basename_patterns):
2102                         index = 0
2103                 if self.basename_pattern.get_active() == len(self.basename_patterns)-1:
2104                         return self.process_custom_pattern(self.custom_filename.get_text())
2105                 else:
2106                         return self.basename_patterns[index][0]
2108         def on_custom_filename_changed(self, entry):
2109                 self.set_string('custom-filename-pattern', entry.get_text())
2110                 self.update_example()
2112         def on_replace_messy_chars_toggled(self, button):
2113                 if button.get_active():
2114                         self.set_int('replace-messy-chars', 1)
2115                 else:
2116                         self.set_int('replace-messy-chars', 0)
2117                 self.update_example()
2119         def change_mime_type(self, mime_type):
2120                 self.set_string('output-mime-type', mime_type)
2121                 self.set_sensitive()
2122                 self.update_example()
2123                 tabs = {
2124                                                 'audio/x-vorbis': 0,
2125                                                 'audio/mpeg': 1,
2126                                                 'audio/x-flac': 2,
2127                                                 'audio/x-wav': 3,
2128                                                 'audio/x-m4a': 4,
2129                                                 'gst-profile': 5,
2130                 }
2131                 self.quality_tabs.set_current_page(tabs[mime_type])
2133         def on_output_mime_type_changed(self, combo):
2134                 self.change_mime_type(
2135                         self.present_mime_types[combo.get_active()]
2136                 )
2138         def on_output_mime_type_ogg_vorbis_toggled(self, button):
2139                 if button.get_active():
2140                         self.change_mime_type('audio/x-vorbis')
2142         def on_output_mime_type_flac_toggled(self, button):
2143                 if button.get_active():
2144                         self.change_mime_type('audio/x-flac')
2146         def on_output_mime_type_wav_toggled(self, button):
2147                 if button.get_active():
2148                         self.change_mime_type('audio/x-wav')
2150         def on_output_mime_type_mp3_toggled(self, button):
2151                 if button.get_active():
2152                         self.change_mime_type('audio/mpeg')
2154         def on_output_mime_type_aac_toggled(self, button):
2155                 if button.get_active():
2156                         self.change_mime_type('audio/x-m4a')
2158         def on_vorbis_quality_changed(self, combobox):
2159                 if combobox.get_active() == -1:
2160                         return # just de-selectionning
2161                 quality = (0,0.2,0.4,0.6,0.8,1.0)
2162                 fquality = quality[combobox.get_active()]
2163                 self.set_float('vorbis-quality', fquality)
2164                 self.hscale_vorbis_quality.set_value(fquality*10)
2165                 self.update_example()
2167         def on_hscale_vorbis_quality_value_changed(self, hscale):
2168                 fquality = hscale.get_value()
2169                 if abs(self.get_float('vorbis-quality') - fquality/10.0) < 0.001:
2170                         return # already at right value
2171                 self.set_float('vorbis-quality', fquality/10.0)
2172                 self.vorbis_quality.set_active(-1)
2173                 self.update_example()
2175         def on_vorbis_oga_extension_toggled(self, toggle):
2176                 self.set_int('vorbis-oga-extension', toggle.get_active())
2177                 self.update_example()
2179         def on_aac_quality_changed(self, combobox):
2180                 quality = (64, 96, 128, 192, 256, 320)
2181                 self.set_int('aac-quality', quality[combobox.get_active()])
2182                 self.update_example()
2184         def on_wav_sample_width_changed(self, combobox):
2185                 quality = (8, 16, 32)
2186                 self.set_int('wav-sample-width', quality[combobox.get_active()])
2187                 self.update_example()
2189         def on_flac_compression_changed(self, combobox):
2190                 quality = (0, 5, 8)
2191                 self.set_int('flac-compression', quality[combobox.get_active()])
2192                 self.update_example()
2194         def on_gstprofile_changed(self, combobox):
2195                 profile = audio_profiles_list[combobox.get_active()]
2196                 description, extension, pipeline = profile
2197                 self.set_string('audio-profile', description)
2198                 self.update_example()
2200         def on_force_mono_toggle(self, button):
2201                 if button.get_active():
2202                         self.set_int('force-mono', 1)
2203                 else:
2204                         self.set_int('force-mono', 0)
2205                 self.update_example()
2207         def change_mp3_mode(self, mode):
2209                 keys = { 'cbr': 0, 'abr': 1, 'vbr': 2 }
2210                 self.mp3_mode.set_active(keys[mode]);
2212                 keys = {
2213                         'cbr': 'mp3-cbr-quality',
2214                         'abr': 'mp3-abr-quality',
2215                         'vbr': 'mp3-vbr-quality',
2216                 }
2217                 quality = self.get_int(keys[mode])
2219                 quality_to_preset = {
2220                         'cbr': {64:0, 96:1, 128:2, 192:3, 256:4, 320:5},
2221                         'abr': {64:0, 96:1, 128:2, 192:3, 256:4, 320:5},
2222                         'vbr': {9:0,   7:1,   5:2,   3:3,   1:4,   0:5}, # inverted !
2223                 }
2225                 range_ = {
2226                         'cbr': 14,
2227                         'abr': 14,
2228                         'vbr': 10,
2229                 }
2230                 self.hscale_mp3.set_range(0,range_[mode])
2232                 if quality in quality_to_preset[mode]:
2233                         self.mp3_quality.set_active(quality_to_preset[mode][quality])
2234                 self.update_example()
2236         def on_mp3_mode_changed(self, combobox):
2237                 mode = ('cbr','abr','vbr')[combobox.get_active()]
2238                 self.set_string('mp3-mode', mode)
2239                 self.change_mp3_mode(mode)
2241         def on_mp3_quality_changed(self, combobox):
2242                 keys = {
2243                         'cbr': 'mp3-cbr-quality',
2244                         'abr': 'mp3-abr-quality',
2245                         'vbr': 'mp3-vbr-quality'
2246                 }
2247                 quality = {
2248                         'cbr': (64, 96, 128, 192, 256, 320),
2249                         'abr': (64, 96, 128, 192, 256, 320),
2250                         'vbr': (9, 7, 5, 3, 1, 0),
2251                 }
2252                 mode = self.get_string('mp3-mode')
2253                 self.set_int(keys[mode], quality[mode][combobox.get_active()])
2254                 self.update_example()
2256         def on_hscale_mp3_value_changed(self, widget):
2257                 mode = self.get_string('mp3-mode')
2258                 keys = {
2259                         'cbr': 'mp3-cbr-quality',
2260                         'abr': 'mp3-abr-quality',
2261                         'vbr': 'mp3-vbr-quality'
2262                 }
2263                 quality = {
2264                         'cbr': (32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320),
2265                         'abr': (32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320),
2266                         'vbr': (9, 8, 7, 6, 5, 4, 3, 2, 1, 0),
2267                 }
2268                 self.set_int(keys[mode], quality[mode][int(widget.get_value())])
2269                 self.mp3_quality.set_active(-1)
2270                 self.update_example()
2272         def on_resample_rate_changed(self, combobox):
2273                 changeto = combobox.get_active_text()
2274                 if int(changeto) >= 2:
2275                         self.set_int('resample-rate', int(changeto))
2277         def on_resample_toggle(self, rstoggle):
2278                 self.set_int('output-resample', rstoggle.get_active())
2279                 self.resample_rate.set_sensitive(rstoggle.get_active())
2282 class ConverterQueueCanceled(SoundConverterException):
2284         """Exception thrown when a ConverterQueue is canceled."""
2286         def __init__(self):
2287                 SoundConverterException.__init__(self, _('Conversion Canceled'), '')
2290 class ConverterQueue(TaskQueue):
2292         """Background task for converting many files."""
2294         def __init__(self, window):
2295                 TaskQueue.__init__(self)
2296                 self.window = window
2297                 self.overwrite_action = None
2298                 self.reset_counters()
2300         def reset_counters(self):
2301                 self.total_duration = 0
2302                 self.duration_processed = 0
2303                 self.overwrite_action = None
2305         def add(self, sound_file):
2306                 output_filename = self.window.prefs.generate_filename(sound_file)
2307                 path = urlparse.urlparse(output_filename) [2]
2308                 path = unquote_filename(path)
2310                 exists = True
2311                 try:
2312                         gnomevfs.get_file_info(gnomevfs.URI((output_filename)))
2313                 except gnomevfs.NotFoundError:
2314                         exists = False
2315                 except :
2316                         log('Invalid URI: \'%s\'' % output_filename)
2317                         return
2319                 # do not overwrite source file !!
2320                 if output_filename == sound_file.get_uri():
2321                         error.show(_('Cannot overwrite source file(s)!'), '')
2322                         raise ConverterQueueCanceled()
2324                 if exists:
2325                         if self.overwrite_action != None:
2326                                 result = self.overwrite_action
2327                         else:
2328                                 dialog = self.window.existsdialog
2330                                 dpath = os.path.basename(path)
2331                                 dpath = markup_escape(dpath)
2333                                 msg = \
2334                                 _('The output file <i>%s</i>\n exists already.\n Do you want to skip the file, overwrite it or cancel the conversion?\n') % \
2335                                 ( dpath )
2337                                 dialog.message.set_markup(msg)
2339                                 if self.overwrite_action != None:
2340                                         dialog.apply_to_all.set_active(True)
2341                                 else:
2342                                         dialog.apply_to_all.set_active(False)
2344                                 result = dialog.run()
2345                                 dialog.hide()
2347                                 if dialog.apply_to_all.get_active():
2348                                         if result == 1 or result == 0:
2349                                                 self.overwrite_action = result
2352                         if result == 1:
2353                                 # overwrite
2354                                 try:
2355                                         vfs_unlink(output_filename)
2356                                 except gnomevfs.NotFoundError:
2357                                         pass
2358                         elif result == 0:
2359                                 # skip file
2360                                 return
2361                         else:
2362                                 # cancel operation
2363                                 # TODO
2364                                 raise ConverterQueueCanceled()
2366                 c = Converter(sound_file, output_filename,
2367                                                 self.window.prefs.get_string('output-mime-type'),
2368                                                 self.window.prefs.get_int('delete-original'),
2369                                                 self.window.prefs.get_int('output-resample'),
2370                                                 self.window.prefs.get_int('resample-rate'),
2371                                                 self.window.prefs.get_int('force-mono'),
2372                                                 )
2373                 c.set_vorbis_quality(self.window.prefs.get_float('vorbis-quality'))
2374                 c.set_aac_quality(self.window.prefs.get_int('aac-quality'))
2375                 c.set_flac_compression(self.window.prefs.get_int('flac-compression'))
2376                 c.set_wav_sample_width(self.window.prefs.get_int('wav-sample-width'))
2377                 c.set_audio_profile(self.window.prefs.get_string('audio-profile'))
2379                 quality = {
2380                         'cbr': 'mp3-cbr-quality',
2381                         'abr': 'mp3-abr-quality',
2382                         'vbr': 'mp3-vbr-quality'
2383                 }
2384                 mode = self.window.prefs.get_string('mp3-mode')
2385                 c.set_mp3_mode(mode)
2386                 c.set_mp3_quality(self.window.prefs.get_int(quality[mode]))
2387                 c.init()
2388                 self.add_task(c)
2389                 c.add_listener('finished', self.on_task_finished)
2390                 c.got_duration = False
2391                 #self.total_duration += c.get_duration()
2392                 gobject.timeout_add(100, self.set_progress)
2393                 self.all_tasks = None
2395         def get_progress(self, task):
2396                 return (self.duration_processed + task.get_position()) / self.total_duration
2398         def set_progress(self, tasks=None):
2400                 tasks = self.running_tasks
2401                 filename = ''
2402                 if tasks and tasks[0]:
2403                         filename = tasks[0].sound_file.get_filename_for_display()
2405                 # try to get all tasks durations
2406                 total_duration = self.total_duration
2407                 if not self.all_tasks:
2408                         self.all_tasks = []
2409                         self.all_tasks.extend(self.waiting_tasks)
2410                         self.all_tasks.extend(self.running_tasks)
2411                         #self.all_tasks.extend(self.finished_tasks)
2413                 for task in self.all_tasks:
2414                         if not task.got_duration:
2415                                 duration = task.sound_file.duration
2416                                 if duration:
2417                                         self.total_duration += duration
2418                                         task.got_duration = True
2419                                 else:
2420                                         total_duration = 0
2422                 position = 0
2423                 for task in tasks:
2424                         if task.converting :
2425                                 position += task.get_position()
2427                 #print self.duration_processed, position, total_duration
2428                 self.window.set_progress(self.duration_processed + position,
2429                                                          total_duration, filename)
2430                 return True
2432         def on_task_finished(self, task):
2433                 self.duration_processed += task.get_duration()
2435         def finished(self):
2436                 TaskQueue.finished(self)
2437                 self.reset_counters()
2438                 self.window.set_progress(0, 0)
2439                 self.window.set_sensitive()
2440                 self.window.conversion_ended()
2441                 total_time = self.run_finish_time - self.run_start_time
2442                 msg = _('Conversion done, in %s') % self.format_time(total_time)
2443                 notification(msg)
2444                 self.window.set_status(msg)
2446         def format_time(self, seconds):
2447                 units = [(86400, 'd'),
2448                                  (3600, 'h'),
2449                                  (60, 'm'),
2450                                  (1, 's')]
2451                 seconds = round(seconds)
2452                 result = []
2453                 for factor, name in units:
2454                         count = int(seconds / factor)
2455                         seconds -= count * factor
2456                         if count > 0 or (factor == 1 and not result):
2457                                 result.append('%d %s' % (count, name))
2458                 assert seconds == 0
2459                 return ' '.join(result)
2461         def abort(self):
2462                 TaskQueue.abort(self)
2463                 self.window.set_progress(0, 0)
2464                 self.window.set_sensitive()
2465                 self.reset_counters()
2467 class CustomFileChooser:
2468         """
2469         Custom file chooser.\n
2470         """
2471         def __init__(self, parent):
2472                 """
2473                 Constructor
2474                 Load glade object, create a combobox
2475                 """
2476                 xml = gtk.glade.XML(GLADE,'custom_file_chooser')
2477                 self.dlg = xml.get_widget('custom_file_chooser')
2478                 self.dlg.set_title(_('Open a file'))
2479                 self.dlg.set_transient_for(parent)
2481                 # setup
2482                 self.fcw = xml.get_widget('filechooserwidget')
2483                 self.fcw.set_local_only(not use_gnomevfs)
2484                 self.fcw.set_select_multiple(True)
2486                 self.pattern = []
2488                 # Create combobox model
2489                 self.combo = xml.get_widget('filtercombo')
2490                 self.combo.connect('changed',self.on_combo_changed)
2491                 self.store = gtk.ListStore(str)
2492                 self.combo.set_model(self.store)
2493                 combo_rend = gtk.CellRendererText()
2494                 self.combo.pack_start(combo_rend, True)
2495                 self.combo.add_attribute(combo_rend, 'text', 0)
2497                 # get all (gstreamer) knew files Todo
2498                 for name, pattern in filepattern:
2499                         self.add_pattern(name,pattern)
2500                 self.combo.set_active(0)
2502         def add_pattern(self,name,pat):
2503                 """
2504                 Add a new pattern to the combobox.
2505                 @param name: The pattern name.
2506                 @type name: string
2507                 @param pat: the pattern
2508                 @type pat: string
2509                 """
2510                 self.pattern.append(pat)
2511                 self.store.append(['%s (%s)' %(name,pat)])
2513         def filter_cb(self, info, pattern):
2514                 filename = info[2]
2515                 return filename.lower().endswith(pattern[1:])
2517         def on_combo_changed(self,w):
2518                 """
2519                 Callback for combobox 'changed' signal\n
2520                 Set a new filter for the filechooserwidget
2521                 """
2522                 filter = gtk.FileFilter()
2523                 active = self.combo.get_active()
2524                 if active:
2525                         filter.add_custom(gtk.FILE_FILTER_DISPLAY_NAME, self.filter_cb, self.pattern[self.combo.get_active()])
2526                 else:
2527                         filter.add_pattern('*.*')
2528                 self.fcw.set_filter(filter)
2530         def __getattr__(self, attr):
2531                 """
2532                 Redirect all missing attributes/methods
2533                 to dialog.
2534                 """
2535                 try:
2536                         # defaut to dialog attributes
2537                         return getattr(self.dlg, attr)
2538                 except AttributeError:
2539                         # fail back to inner file chooser widget
2540                         return getattr(self.fcw, attr)
2544 class SoundConverterWindow(GladeWindow):
2546         """Main application class."""
2548         sensitive_names = [ 'remove', 'clearlist', 'toolbutton_clearlist', 'convert_button' ]
2549         unsensitive_when_converting = [ 'remove', 'clearlist', 'prefs_button' ,'toolbutton_addfile', 'toolbutton_addfolder', 'toolbutton_clearlist', 'filelist', 'menubar' ]
2551         def __init__(self, glade):
2552                 GladeWindow.__init__(self, glade)
2554                 self.widget = glade.get_widget('window')
2555                 self.filelist = FileList(self, glade)
2556                 self.filelist_selection = self.filelist.widget.get_selection()
2557                 self.filelist_selection.connect('changed', self.selection_changed)
2558                 self.existsdialog = glade.get_widget('existsdialog')
2559                 self.existsdialog.message = glade.get_widget('exists_message')
2560                 self.existsdialog.apply_to_all = glade.get_widget('apply_to_all')
2561                 self.status = glade.get_widget('statustext')
2562                 self.prefs = PreferencesDialog(glade)
2563                 #self.progressfile = glade.get_widget('progressfile')
2565                 self.addchooser = CustomFileChooser(self.widget)
2566                 self.addfolderchooser = gtk.FileChooserDialog(_('Add Folder...'),
2567                         self.widget, gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
2568                         (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK))
2569                 self.addfolderchooser.set_select_multiple(True)
2570                 self.addfolderchooser.set_local_only(not use_gnomevfs)
2572                 self.combo = gtk.ComboBox()
2573                 #self.combo.connect('changed',self.on_combo_changed)
2574                 self.store = gtk.ListStore(str)
2575                 self.combo.set_model(self.store)
2576                 combo_rend = gtk.CellRendererText()
2577                 self.combo.pack_start(combo_rend, True)
2578                 self.combo.add_attribute(combo_rend, 'text', 0)
2580                 # get all (gstreamer) knew files Todo
2581                 for files in filepattern:
2582                         self.store.append(['%s (%s)' %(files[0],files[1])])
2584                 self.combo.set_active(0)
2585                 self.addfolderchooser.set_extra_widget(self.combo)
2587 # XFCEpatch
2588 #               self.about.set_property('name', NAME)
2589 #               self.about.set_property('version', VERSION)
2591                 self.convertion_waiting = False
2593                 self.converter = ConverterQueue(self)
2595                 self._lock_convert_button = False
2597                 self.sensitive_widgets = {}
2598                 for name in self.sensitive_names:
2599                         self.sensitive_widgets[name] = glade.get_widget(name)
2600                 for name in self.unsensitive_when_converting:
2601                         self.sensitive_widgets[name] = glade.get_widget(name)
2603                 self.set_sensitive()
2604                 self.set_status()
2606         # This bit of code constructs a list of methods for binding to Gtk+
2607         # signals. This way, we don't have to maintain a list manually,
2608         # saving editing effort. It's enough to add a method to the suitable
2609         # class and give the same name in the .glade file.
2612         def __getattr__(self, attribute):
2613                 """Allow direct use of window widget."""
2614                 widget = self.glade.get_widget(attribute)
2615                 if widget is None:
2616                         raise AttributeError('Widget \'%s\' not found' % attribute)
2617                 self.__dict__[attribute] = widget # cache result
2618                 return widget
2620         def close(self, *args):
2621                 debug('closing...')
2622                 self.filelist.abort()
2623                 self.converter.abort()
2624                 self.widget.hide_all()
2625                 self.widget.destroy()
2626                 # wait one second...
2627                 # yes, this sucks badly, but signals can still be called by gstreamer so wait
2628                 # a bit for things to calm down, and quit.
2629                 gtk_sleep(1)
2630                 gtk.main_quit()
2631                 return True
2633         on_window_delete_event = close
2634         on_quit_activate = close
2635         on_quit_button_clicked = close
2637         def on_add_activate(self, *args):
2638                 last_folder = self.prefs.get_string('last-used-folder')
2639                 if last_folder:
2640                         self.addchooser.set_current_folder_uri(last_folder)
2642                 ret = self.addchooser.run()
2643                 self.addchooser.hide()
2644                 if ret == gtk.RESPONSE_OK:
2645                         self.filelist.add_uris(self.addchooser.get_uris())
2646                         self.prefs.set_string('last-used-folder', self.addchooser.get_current_folder_uri())
2647                 self.set_sensitive()
2650         def on_addfolder_activate(self, *args):
2651                 last_folder = self.prefs.get_string('last-used-folder')
2652                 if last_folder:
2653                         self.addfolderchooser.set_current_folder_uri(last_folder)
2655                 ret = self.addfolderchooser.run()
2656                 self.addfolderchooser.hide()
2657                 if ret == gtk.RESPONSE_OK:
2659                         folders = self.addfolderchooser.get_uris()
2660                         extensions = None
2661                         if self.combo.get_active():
2662                                 patterns = filepattern[self.combo.get_active()][1].split(';')
2663                                 extensions = [os.path.splitext(p)[1] for p in patterns]
2665                         self.filelist.add_uris(folders, extensions=extensions)
2667                         self.prefs.set_string('last-used-folder', self.addfolderchooser.get_current_folder_uri())
2669                 self.set_sensitive()
2671         def on_remove_activate(self, *args):
2672                 model, paths = self.filelist_selection.get_selected_rows()
2673                 while paths:
2674                         i = self.filelist.model.get_iter(paths[0])
2675                         self.filelist.remove(i)
2676                         model, paths = self.filelist_selection.get_selected_rows()
2677                 self.set_sensitive()
2679         def on_clearlist_activate(self, *args):
2680                 self.filelist_selection.select_all();
2681                 model, paths = self.filelist_selection.get_selected_rows()
2682                 while paths:
2683                         i = self.filelist.model.get_iter(paths[0])
2684                         self.filelist.remove(i)
2685                         model, paths = self.filelist_selection.get_selected_rows()
2686                 self.set_sensitive()
2687                 self.set_status()
2689         def read_tags(self, sound_file):
2690                 tagreader = TagReader(sound_file)
2691                 tagreader.set_found_tag_hook(self.tags_read)
2692                 tagreader.start()
2694         def tags_read(self, tagreader):
2695                 sound_file = tagreader.get_sound_file()
2696                 self.converter.add(sound_file)
2698         def do_convert(self):
2699                 try:
2700                         for sound_file in self.filelist.get_files():
2701                                 if sound_file.tags_read:
2702                                         self.converter.add(sound_file)
2703                                 else:
2704                                         self.read_tags(sound_file)
2705                 except ConverterQueueCanceled:
2706                         log('cancelling conversion.')
2707                         self.conversion_ended()
2708                         self.set_status(_('Conversion cancelled'))
2709                 else:
2710                         self.set_status('')
2711                         self.converter.start()
2712                         self.convertion_waiting = False
2713                         self.set_sensitive()
2714                 return False
2716         def wait_tags_and_convert(self):
2717                 not_ready = [s for s in self.filelist.get_files() if not s.tags_read]
2718                 #if not_ready:
2719                 #       self.progressbar.pulse()
2720                 return True
2722                 self.do_convert()
2723                 return False
2726         def on_convert_button_clicked(self, *args):
2727                 if self._lock_convert_button:
2728                         return
2730                 if not self.converter.running:
2731                         self.set_status(_('Waiting for tags'))
2732                         self.progress_frame.show()
2733                         self.status_frame.hide()
2734                         self.progress_time = time.time()
2735                         #self.widget.set_sensitive(False)
2737                         self.convertion_waiting = True
2738                         self.set_status(_('Waiting for tags...'))
2740                         #thread.start_thread(self.do_convert, ())
2741                         self.do_convert()
2742                         #gobject.timeout_add(100, self.wait_tags_and_convert)
2743                 else:
2744                         self.converter.paused = not self.converter.paused
2745                         if self.converter.paused:
2746                                 self.set_status(_('Paused'))
2747                         else:
2748                                 self.set_status('')
2749                 self.set_sensitive()
2751         def on_button_pause_clicked(self, *args):
2752                 task = self.converter.get_current_task()
2753                 if task:
2754                         self.converter.paused = not self.converter.paused
2755                         task.toggle_pause(self.converter.paused)
2756                 else:
2757                         return
2758                 if self.converter.paused:
2759                         self.display_progress(_('Paused'))
2761         def on_button_cancel_clicked(self, *args):
2762                 self.converter.abort()
2763                 self.set_status(_('Canceled'))
2764                 self.set_sensitive()
2765                 self.conversion_ended()
2767         def on_select_all_activate(self, *args):
2768                 self.filelist.widget.get_selection().select_all()
2770         def on_clear_activate(self, *args):
2771                 self.filelist.widget.get_selection().unselect_all()
2773         def on_preferences_activate(self, *args):
2774                 self.prefs.run()
2776         on_prefs_button_clicked = on_preferences_activate
2778         def on_about_activate(self, *args):
2779 # XFCEpatch
2780                 pass
2781 #               about = gtk.glade.XML(GLADE, 'about').get_widget('about')
2782 #               about.set_property('name', NAME)
2783 #               about.set_property('version', VERSION)
2784 #               about.set_property('translator_credits', TRANSLATORS)
2785 #               about.set_transient_for(self.widget)
2786 #               about.show()
2788         def selection_changed(self, *args):
2789                 self.set_sensitive()
2791         def conversion_ended(self):
2792                 self.progress_frame.hide()
2793                 self.status_frame.show()
2794                 self.widget.set_sensitive(True)
2796         def set_widget_sensitive(self, name, sensitivity):
2797                 self.sensitive_widgets[name].set_sensitive(sensitivity)
2799         def set_sensitive(self):
2801                 for w in self.unsensitive_when_converting:
2802                         self.set_widget_sensitive(w, not self.converter.running)
2804                 self.set_widget_sensitive('remove',
2805                         self.filelist_selection.count_selected_rows() > 0)
2806                 self.set_widget_sensitive('convert_button',
2807                                                                         self.filelist.is_nonempty())
2809                 self._lock_convert_button = True
2810                 self.sensitive_widgets['convert_button'].set_active(
2811                         self.converter.running and not self.converter.paused )
2812                 self._lock_convert_button = False
2814         def display_progress(self, remaining):
2815                 done = self.converter.finished_tasks
2816                 total = done + len(self.converter.waiting_tasks) + len(self.converter.running_tasks)
2817                 self.progressbar.set_text(_('Converting file %d of %d  (%s)') % ( done + 1, total, remaining ))
2819         def set_progress(self, done_so_far, total, current_file=None):
2820                 if (total==0) or (done_so_far==0):
2821                         self.progressbar.set_text(' ')
2822                         self.progressbar.set_fraction(0.0)
2823                         self.progressbar.pulse()
2824                         return
2825                 if time.time() < self.progress_time + 0.10:
2826                         # ten updates per second should be enough
2827                         return
2828                 self.progress_time = time.time()
2830                 self.set_status(_('Converting'))
2832                 if current_file:
2833                         self.progressfile.set_markup('<i><small>%s</small></i>' % markup_escape(current_file))
2834                 else:
2835                         self.progressfile.set_markup('')
2837                 fraction = float(done_so_far) / total
2839                 self.progressbar.set_fraction( min(fraction, 1.0) )
2840                 t = time.time() - self.converter.run_start_time - self.converter.paused_time
2842                 if (t<1):
2843                         # wait a bit not to display crap
2844                         self.progressbar.pulse()
2845                         return
2847                 r = (t / fraction - t)
2848                 #return
2849                 s = r%60
2850                 m = r/60
2851                 remaining = _('%d:%02d left') % (m,s)
2852                 self.display_progress(remaining)
2854         def set_status(self, text=None):
2855                 if not text:
2856                         text = _('Ready')
2857                 self.status.set_markup(text)
2858                 gtk_iteration()
2861 def gui_main(input_files):
2862 # XFCEpatch
2863 #       gnome.init(NAME, VERSION)
2864         glade = gtk.glade.XML(GLADE)
2865         win = SoundConverterWindow(glade)
2866         global error
2867         error = ErrorDialog(glade)
2868         #TODO
2869         gobject.idle_add(win.filelist.add_uris, input_files)
2870         win.set_sensitive()
2871         #gtk.threads_enter()
2872         gtk.main()
2873         #gtk.threads_leave()
2875 def cli_tags_main(input_files):
2876         global error
2877         error = ErrorPrinter()
2878         for input_file in input_files:
2879                 input_file = SoundFile(input_file)
2880                 if not get_option('quiet'):
2881                         print input_file.get_uri()
2882                 t = TagReader(input_file)
2883                 t.start()
2884                 while t.running:
2885                         gtk_sleep(0.1)
2886                 if not get_option('quiet'):
2887                         keys = input_file.tags.keys()
2888                         keys.sort()
2889                         for key in keys:
2890                                 print '         %s: %s' % (key, input_file[key])
2893 class CliProgress:
2895         def __init__(self):
2896                 self.current_text = ''
2898         def show(self, new_text):
2899                 if new_text != self.current_text:
2900                         self.clear()
2901                         sys.stdout.write(new_text)
2902                         sys.stdout.flush()
2903                         self.current_text = new_text
2905         def clear(self):
2906                 sys.stdout.write('\b \b' * len(self.current_text))
2907                 sys.stdout.flush()
2910 def cli_convert_main(input_files):
2911         global error
2912         error = ErrorPrinter()
2914         output_type = get_option('cli-output-type')
2915         output_suffix = get_option('cli-output-suffix')
2917         generator = TargetNameGenerator()
2918         generator.set_target_suffix(output_suffix)
2920         progress = CliProgress()
2922         queue = TaskQueue()
2923         for input_name in input_files:
2924                 input_file = SoundFile(input_name)
2925                 output_name = generator.get_target_name(input_file)
2926                 if input_name == output_name:
2927                         print 'WARNING: Not reconverting', input_name
2928                         continue
2929                 c = Converter(input_file, output_name, output_type)
2930                 c.overwrite = True
2931                 c.init()
2932                 queue.add_task(c)
2934         previous_filename = None
2935         queue.start()
2936         while queue.running:
2937                 t = queue.get_current_task()
2938                 if t and not get_option('quiet'):
2939                         if previous_filename != t.sound_file.get_filename_for_display():
2940                                 if previous_filename:
2941                                         print
2942                                         print _('%s: OK') % previous_filename
2943                                 previous_filename = t.sound_file.get_filename_for_display()
2945                         percent = 0
2946                         if t.get_duration():
2947                                 percent = '%.1f %%' % ( 100.0* (t.get_position() / t.get_duration() ))
2948                         else:
2949                                 percent = '/-\|' [int(time.time()) % 4]
2950                         progress.show('%s: %s' % (t.sound_file.get_filename_for_display()[-65:], percent ))
2951                 gtk_sleep(0.1)
2953         if not get_option('quiet'):
2954                 progress.clear()
2958 def main(args):
2959         args = map(filename_to_uri, args)
2960         print '  using %d thread(s)' % get_option('jobs')
2961         if get_option('mode') == 'gui':
2962                 gui_main(args)
2963         elif get_option('mode') == 'tags':
2964                 cli_tags_main(args)
2965         else:
2966                 cli_convert_main(args)
2969 if __name__ == '__main__':
2970         main(args)