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
22 NAME = 'SoundConverter'
24 GLADE = '/usr/share/soundconverter/soundconverter.glade'
26 print '%s %s' % (NAME, VERSION)
28 # Python standard stuff.
39 from optparse import OptionParser
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)
53 Returns the number of CPUs in the system.
56 if sys.platform == 'win32':
58 num = int(os.environ['NUMBER_OF_PROCESSORS'])
59 except (ValueError, KeyError):
61 elif sys.platform == 'darwin':
63 num = int(os.popen('sysctl -n hw.ncpu').read())
68 num = os.sysconf('SC_NPROCESSORS_ONLN')
69 except (ValueError, OSError, AttributeError):
80 'cli-output-type': 'audio/x-vorbis',
81 'cli-output-suffix': '.ogg',
87 assert key in settings
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'))
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)
131 settings['mode'] = options.mode
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):
146 # GNOME and related stuff.
155 # gnome.ui.authentication_manager_init()
158 gobject.threads_init()
160 except ImportError, e:
161 print 'error: %s' % e
162 print '%s needs gnome-python 2.10!' % NAME
168 pygst.require('0.10')
171 print '%s needs python-gstreamer 0.10!' % NAME
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
181 def notification(message):
187 if pynotify.init('Basics'):
188 def notification(message):
190 n = pynotify.Notification(NAME, message)
198 gtk.glade.bindtextdomain(PACKAGE,'/usr/share/locale')
199 gtk.glade.textdomain(PACKAGE)
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)
213 rainofchaos (Simplified Chinese)
214 Pavol Klačanský (Slovak)
215 Moshe Basanchig <moshe.basanchig gmail.com> (Hebrew)
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
231 'application/vnd.rn-realmedia',
232 'application/x-pn-realaudio',
233 'application/x-shockwave-flash',
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')
247 '%(track-number)02d',
255 # add english and locale
256 custom_patterns = english_patterns + ' ' + locale_patterns
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
272 # Name and pattern for CustomFileChooser
274 (_('All files'),'*.*'),
276 ('Ogg Vorbis','*.ogg;*.oga'),
277 ('AAC','*.m4a;*.aac'),
283 def beautify_uri(uri):
284 return unquote_filename(uri).split('file://')[-1]
288 """similar to os.path.walk, but with gnomevfs.
290 uri -- the base folder uri.
291 return a list of uri.
294 if str(uri)[-1] != '/':
295 uri = uri.append_string('/')
300 dirlist = gnomevfs.open_directory(uri, gnomevfs.FILE_INFO_FOLLOW_LINKS)
302 log("skipping: '%s\'" % uri)
305 for file_info in dirlist:
307 if file_info.name[0] == '.':
310 if file_info.type == gnomevfs.FILE_TYPE_DIRECTORY:
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)) )
317 # this can happen when you do not have sufficent
318 # permissions to read file info.
319 log("skipping: \'%s\'" % uri)
322 def vfs_makedirs(path_to_create):
323 """Similar to os.makedirs, but with gnomevfs"""
325 uri = gnomevfs.URI(path_to_create)
329 uri = uri.resolve_relative('/')
331 for folder in path.split('/'):
334 uri = uri.append_string(folder.replace('%2f', '/'))
336 gnomevfs.make_directory(uri, 0777)
337 except gnomevfs.FileExistsError:
343 def vfs_unlink(filename):
344 gnomevfs.unlink(gnomevfs.URI(filename))
346 def vfs_exists(filename):
348 return gnomevfs.exists(filename)
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.
356 url = urlparse.urlparse(filename)
358 filename = urllib.pathname2url(os.path.abspath(filename))
359 filename = str(gnomevfs.URI(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)
379 if isinstance(tag, list):
381 tag = ', '.join(tag[:-1]) + ' & ' + tag[-1]
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
402 if gst.element_factory_find('giosrc'):
403 gstreamer_source = 'giosrc'
404 gstreamer_sink = 'giosink'
405 encode_filename = vfs_encode_filename
408 elif gst.element_factory_find('gnomevfssrc'):
409 gstreamer_source = 'gnomevfssrc'
410 gstreamer_sink = 'gnomevfssink'
411 encode_filename = vfs_encode_filename
413 print ' using deprecated gnomevfssrc'
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.'
424 ('vorbisenc', 'Ogg Vorbis'),
425 ('oggmux', 'Ogg Vorbis'),
426 ('id3v2mux', 'MP3 Tags'),
427 ('xingmux', 'Xing Header'),
433 for encoder, name in encoders:
434 have_it = bool(gst.element_factory_find(encoder))
436 print ("\t'%s' gstreamer element not found"
437 ", disabling %s." % (encoder, name))
438 exec('have_%s = %s' % (encoder, have_it))
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
463 if get_option('quiet') == False:
464 print ' '.join([str(msg) for msg in args])
467 if get_option('debug') == True:
468 print ' '.join([str(msg) for msg in args])
471 while gtk.events_pending():
472 gtk.main_iteration(False)
474 def gtk_sleep(duration):
476 while time.time() < start + duration:
480 def UNUSED_display_from_mime(mime):
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',
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
504 """Meta data information about a sound file (uri, tags)."""
506 def __init__(self, uri, base_path=None):
511 self.base_path = base_path
512 self.filename = self.uri[len(self.base_path):]
514 self.base_path, self.filename = os.path.split(self.uri)
515 self.base_path += '/'
519 'title': 'Unknown Title',
520 'artist': 'Unknown Artist',
521 'album': 'Unknown Album',
523 self.have_tags = False
524 self.tags_read = False
526 self.mime_type = None
531 def get_base_path(self):
532 return self.base_path
534 def get_filename(self):
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)
552 __getitem__ = get_tag
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 + '.-_/'
567 self.basename= '%(.inputname)s'
568 self.ext = '%(.ext)s'
570 self.replace_messy_chars = False
573 self.exists = gnomevfs.exists
575 self.exists = os.path.exists
577 # This is useful for unit testing.
578 def set_exists(self, exists):
581 def set_target_suffix(self, suffix):
584 def set_folder(self, 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
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)
613 host = '%s:%s' % (u.host_name, u.host_port)
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)
624 '.inputname': basename,
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)
655 if c not in self.nice_chars:
662 if self.folder is None:
664 folder = os.path.join(root, basefolder)
667 # destination folder + folders from tags
671 folder = os.path.join(self.folder, basefolder)
673 result = os.path.join(folder, urllib.quote(result))
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)
691 def show_exception(self, exception):
692 self.show('<b>%s</b>' % markup_escape(exception.primary),
698 def show(self, primary, secondary):
699 sys.stderr.write(_('\n\nError: %s\n%s\n') % (primary, secondary))
702 def show_exception(self, e):
703 self.show(e.primary, e.secondary)
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
721 self.current_paused_time = 0
726 """Start running the task. Call started()."""
729 except SoundConverterException, e:
730 error.show_exception(e)
734 self.run_start_time = time.time()
735 self.current_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)
753 """Call to end normally the task."""
754 self.run_finish_time = time.time()
757 self.emit('finished')
760 """Stop task processing. finished() is not called."""
764 """called when the task starts."""
768 """Clean up the task after all work has been done."""
772 class TaskQueue(BackgroundTask):
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
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."""
790 BackgroundTask.__init__(self)
791 self.waiting_tasks = []
792 self.running_tasks = []
793 self.finished_tasks = 0
794 self.start_time = None
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:
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]
811 def start_next_task(self):
812 to_start = get_option('jobs') - len(self.running_tasks)
813 for i in range(to_start):
815 task = self.waiting_tasks.pop(0)
817 if not self.running_tasks:
820 self.running_tasks.append(task)
821 task.add_listener('finished', self.task_finished)
824 total = len(self.waiting_tasks) + self.finished_tasks
825 self.progress = float(self.finished_tasks) / total if total else 0
828 """ BackgroundTask setup callback """
829 self.start_time = time.time()
831 self.finished_tasks = 0
832 self.start_next_task()
835 """ BackgroundTask finish callback """
836 log('\nQueue done in %.3fs (%s tasks)' % (time.time() - self.start_time, self.count))
840 def task_finished(self, task=None):
841 if not self.running_tasks:
843 self.running_tasks.remove(task)
844 self.finished_tasks += 1
845 self.start_next_task()
848 for task in self.running_tasks:
850 BackgroundTask.abort(self)
851 self.running_tasks = []
852 self.waiting_tasks = []
855 # The following is called when the Queue is finished
856 def queue_ended(self):
859 # The following when progress changed
860 def progress_hook(self, progress):
864 class NoLink(SoundConverterException):
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."""
882 BackgroundTask.__init__(self)
887 self.processing = False
890 self.connected_signals = []
896 for element, sid in self.connected_signals:
897 element.disconnect(sid)
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 !')
915 self.pipeline.set_state(gst.STATE_PAUSED)
917 self.pipeline.set_state(gst.STATE_PLAYING)
919 def found_tag(self, decoder, something, taglist):
922 def install_plugin_cb(self, result):
923 if result == gst.pbutils.INSTALL_PLUGINS_SUCCESS:
924 gst.update_registry()
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.')
938 error.show('Error', 'failed to install plugins: %s' % markup_escape(str(result)))
940 def on_error(self, error):
942 log('error: %s (%s)' % (error,
943 self.sound_file.get_filename_for_display()))
945 def on_message(self, bus, message):
948 if t == gst.MESSAGE_ERROR:
949 error, debug = message.parse_error()
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):
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:
968 elif t == gst.MESSAGE_TAG:
969 self.found_tag(self, '', message.parse_tag())
975 command = ' ! '.join(self.command)
976 debug('launching: \'%s\'' % command)
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:
985 element = self.pipeline.get_by_name(name)
988 sid = element.connect(signal,callback)
989 self.connected_signals.append((element, sid,))
994 except gobject.GError, e:
995 error.show('GStreamer error when creating pipeline', str(e))
996 self.eos = True # TODO
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!')
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:
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)
1047 Pipeline.finished(self)
1049 print 'error:', self.error
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
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):
1077 def query_duration(self):
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:
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)
1106 for k in taglist.keys():
1107 if k in tag_whitelist:
1108 tags[k] = taglist[k]
1111 self.sound_file.add_tags(tags)
1112 self.sound_file.have_tags = True
1115 self.sound_file.duration = self.pipeline.query_duration(gst.FORMAT_TIME)[0] / gst.SECOND
1116 except gst.QueryError:
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)
1126 self.position = float(buffer.timestamp) / gst.SECOND
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()
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:
1178 debug('TagReading done...')
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
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,
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')
1241 self.add_command('audioresample ! audio/x-raw-float,channels=1')
1242 self.add_command('audioconvert')
1244 encoder = self.encoders[self.output_type]()
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.") % \
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.") % \
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)
1276 self.converting = False
1277 Pipeline.finished(self)
1279 # Copy file permissions
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)
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())
1289 gnomevfs.unlink(self.sound_file.get_uri())
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
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):
1327 if self.vorbis_quality is not None:
1328 cmd += ' quality=%s' % self.vorbis_quality
1332 def add_mp3_encoder(self):
1334 cmd = 'lame quality=2 '
1336 if self.mp3_mode is not None:
1338 'cbr' : (0,'bitrate'),
1339 'abr' : (3,'vbr-mean-bitrate'),
1340 'vbr' : (4,'vbr-quality')
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
1359 cmd += '! id3v2mux '
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]
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()
1385 for name in ALL_COLUMNS:
1386 if name in VISIBLE_COLUMNS:
1387 args.append(gobject.TYPE_STRING)
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,
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,
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,
1414 if mime_id >= 0 and mime_id < len(self.drop_mime_types):
1416 self.add_uris([uri.strip() for uri in selection.data.split('\n')])
1417 context.finish(True, False, time)
1419 def get_files(self):
1421 i = self.model.get_iter_first()
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)
1431 def update_progress(self, queue):
1433 progress = queue.progress
1434 self.window.progressbarstatus.set_fraction(progress if progress else 0)
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):
1452 self.window.set_status(_('Adding files...'))
1457 if uri.startswith('cdda:'):
1458 error.show('Cannot read from Audio CD.',
1459 'Use SoundJuicer Audio CD Extractor instead.')
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)
1466 except gnomevfs.InvalidURIError:
1467 log('unvalid uri: \'%s\'' % uri)
1469 except gnomevfs.AccessDeniedError:
1470 log('access denied: \'%s\'' % uri)
1472 except TypeError, e:
1473 log('add error: %s (\'%s\')' % (e, uri))
1476 log('error in get_file_info: %s' % (uri))
1479 if info.type == gnomevfs.FILE_TYPE_DIRECTORY:
1480 log('walking: \'%s\'' % uri)
1481 filelist = vfs_walk(gnomevfs.URI(uri))
1485 for extension in extensions:
1486 if f.lower().endswith(extension):
1490 files.extend(filelist)
1494 base,notused = os.path.split(os.path.commonprefix(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())
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
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
1522 self.window.set_status()
1525 def typefinder_queue_ended(self):
1528 self.window.set_status()
1529 self.window.progressbarstatus.hide()
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()
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>' \
1553 template_skiptags = '%(filename)s'
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)
1562 params['bitrate'] = ''
1564 if sound_file.have_tags:
1565 template = template_tags
1568 template = template_skiptags
1569 elif sound_file.tags_read:
1570 template = template_notags
1572 template = template_loading
1574 for tag, unicode_string in params.items():
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
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()
1596 for key in ALL_COLUMNS:
1597 fields[key] = _('unknown')
1598 fields['META'] = sound_file
1599 fields['filename'] = sound_file.get_filename_for_display()
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):
1614 self.model.get_iter((0,))
1620 class GladeWindow(object):
1622 def __init__(self, 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)
1630 raise AttributeError('Widget \'%s\' not found' % attribute)
1631 self.__dict__[attribute] = widget # cache result
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)
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]
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')),
1683 subfolder_patterns = [
1684 ('%(artist)s/%(album)s', _('artist/album')),
1685 ('%(artist)s-%(album)s', _('artist-album')),
1686 ('%(artist)s - %(album)s', _('artist - album')),
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,
1706 'flac-compression': 8,
1707 'wav-sample-width': 16,
1708 'delete-original': 0,
1709 'output-resample': 0,
1710 'resample-rate': 48000,
1713 'last-used-folder': None,
1714 'audio-profile': None,
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():
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
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
1763 w = self.into_selected_folder
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()
1777 for pattern, desc in self.subfolder_patterns:
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
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 = []
1807 mime, encoder_present = b
1808 if not encoder_present:
1810 if mime_type == mime:
1811 mime_type = self.defaults['output-mime-type']
1813 self.present_mime_types.append(mime)
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
1822 w = self.lame_absent
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()
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)
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')
1876 idx = rates.index(rate)
1878 self.resample_rate.insert_text(0, str(rate))
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):
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':
1918 'cbr': 'mp3-cbr-quality',
1919 'abr': 'mp3-abr-quality',
1920 'vbr': 'mp3-vbr-quality'
1922 bitrate = self.get_int(quality[mode])
1924 # hum, not really, but who cares? :)
1925 bitrates = (320, 256, 224, 192, 160, 128, 112, 96, 80, 64)
1926 bitrate = bitrates[bitrate]
1932 return '~%d kbps' % bitrate
1934 return '%d kbps' % bitrate
1939 def update_example(self):
1940 sound_file = SoundFile('foo/bar.flac')
1941 sound_file.add_tags({
1945 sound_file.add_tags(locale_patterns_dict)
1947 s = markup_escape(beautify_uri(self.generate_filename(sound_file, for_display=True)))
1958 if tag.lower() in [v.lower() for v in locale_patterns_dict.values()]:
1960 l = k.replace('{','<b>{')
1961 l = l.replace('}','}</b>')
1962 replaces.append([k,l])
1965 l = k.replace('{','<span foreground=\'red\'><i>{')
1966 l = l.replace('}','}</i></span>')
1967 replaces.append([k,l])
1970 for k,l in replaces:
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 ''
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())
2011 generator.set_replace_messy_chars(False)
2012 return unquote_filename(generator.get_target_name(sound_file))
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])
2024 def set_sensitive(self):
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')
2046 def on_delete_original_toggled(self, button):
2047 if button.get_active():
2048 self.set_int('delete-original', 1)
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()
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)
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):
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)
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):
2103 if self.basename_pattern.get_active() == len(self.basename_patterns)-1:
2104 return self.process_custom_pattern(self.custom_filename.get_text())
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)
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()
2124 'audio/x-vorbis': 0,
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()]
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):
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)
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]);
2213 'cbr': 'mp3-cbr-quality',
2214 'abr': 'mp3-abr-quality',
2215 'vbr': 'mp3-vbr-quality',
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 !
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):
2243 'cbr': 'mp3-cbr-quality',
2244 'abr': 'mp3-abr-quality',
2245 'vbr': 'mp3-vbr-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),
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')
2259 'cbr': 'mp3-cbr-quality',
2260 'abr': 'mp3-abr-quality',
2261 'vbr': 'mp3-vbr-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),
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."""
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)
2312 gnomevfs.get_file_info(gnomevfs.URI((output_filename)))
2313 except gnomevfs.NotFoundError:
2316 log('Invalid URI: \'%s\'' % output_filename)
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()
2325 if self.overwrite_action != None:
2326 result = self.overwrite_action
2328 dialog = self.window.existsdialog
2330 dpath = os.path.basename(path)
2331 dpath = markup_escape(dpath)
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') % \
2337 dialog.message.set_markup(msg)
2339 if self.overwrite_action != None:
2340 dialog.apply_to_all.set_active(True)
2342 dialog.apply_to_all.set_active(False)
2344 result = dialog.run()
2347 if dialog.apply_to_all.get_active():
2348 if result == 1 or result == 0:
2349 self.overwrite_action = result
2355 vfs_unlink(output_filename)
2356 except gnomevfs.NotFoundError:
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'),
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'))
2380 'cbr': 'mp3-cbr-quality',
2381 'abr': 'mp3-abr-quality',
2382 'vbr': 'mp3-vbr-quality'
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]))
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
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:
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
2417 self.total_duration += duration
2418 task.got_duration = True
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)
2432 def on_task_finished(self, task):
2433 self.duration_processed += task.get_duration()
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)
2444 self.window.set_status(msg)
2446 def format_time(self, seconds):
2447 units = [(86400, 'd'),
2451 seconds = round(seconds)
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))
2459 return ' '.join(result)
2462 TaskQueue.abort(self)
2463 self.window.set_progress(0, 0)
2464 self.window.set_sensitive()
2465 self.reset_counters()
2467 class CustomFileChooser:
2469 Custom file chooser.\n
2471 def __init__(self, parent):
2474 Load glade object, create a combobox
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)
2482 self.fcw = xml.get_widget('filechooserwidget')
2483 self.fcw.set_local_only(not use_gnomevfs)
2484 self.fcw.set_select_multiple(True)
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):
2504 Add a new pattern to the combobox.
2505 @param name: The pattern name.
2507 @param pat: the pattern
2510 self.pattern.append(pat)
2511 self.store.append(['%s (%s)' %(name,pat)])
2513 def filter_cb(self, info, pattern):
2515 return filename.lower().endswith(pattern[1:])
2517 def on_combo_changed(self,w):
2519 Callback for combobox 'changed' signal\n
2520 Set a new filter for the filechooserwidget
2522 filter = gtk.FileFilter()
2523 active = self.combo.get_active()
2525 filter.add_custom(gtk.FILE_FILTER_DISPLAY_NAME, self.filter_cb, self.pattern[self.combo.get_active()])
2527 filter.add_pattern('*.*')
2528 self.fcw.set_filter(filter)
2530 def __getattr__(self, attr):
2532 Redirect all missing attributes/methods
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)
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()
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)
2616 raise AttributeError('Widget \'%s\' not found' % attribute)
2617 self.__dict__[attribute] = widget # cache result
2620 def close(self, *args):
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.
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')
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')
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()
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()
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()
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()
2689 def read_tags(self, sound_file):
2690 tagreader = TagReader(sound_file)
2691 tagreader.set_found_tag_hook(self.tags_read)
2694 def tags_read(self, tagreader):
2695 sound_file = tagreader.get_sound_file()
2696 self.converter.add(sound_file)
2698 def do_convert(self):
2700 for sound_file in self.filelist.get_files():
2701 if sound_file.tags_read:
2702 self.converter.add(sound_file)
2704 self.read_tags(sound_file)
2705 except ConverterQueueCanceled:
2706 log('cancelling conversion.')
2707 self.conversion_ended()
2708 self.set_status(_('Conversion cancelled'))
2711 self.converter.start()
2712 self.convertion_waiting = False
2713 self.set_sensitive()
2716 def wait_tags_and_convert(self):
2717 not_ready = [s for s in self.filelist.get_files() if not s.tags_read]
2719 # self.progressbar.pulse()
2726 def on_convert_button_clicked(self, *args):
2727 if self._lock_convert_button:
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, ())
2742 #gobject.timeout_add(100, self.wait_tags_and_convert)
2744 self.converter.paused = not self.converter.paused
2745 if self.converter.paused:
2746 self.set_status(_('Paused'))
2749 self.set_sensitive()
2751 def on_button_pause_clicked(self, *args):
2752 task = self.converter.get_current_task()
2754 self.converter.paused = not self.converter.paused
2755 task.toggle_pause(self.converter.paused)
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):
2776 on_prefs_button_clicked = on_preferences_activate
2778 def on_about_activate(self, *args):
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)
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()
2825 if time.time() < self.progress_time + 0.10:
2826 # ten updates per second should be enough
2828 self.progress_time = time.time()
2830 self.set_status(_('Converting'))
2833 self.progressfile.set_markup('<i><small>%s</small></i>' % markup_escape(current_file))
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
2843 # wait a bit not to display crap
2844 self.progressbar.pulse()
2847 r = (t / fraction - t)
2851 remaining = _('%d:%02d left') % (m,s)
2852 self.display_progress(remaining)
2854 def set_status(self, text=None):
2857 self.status.set_markup(text)
2861 def gui_main(input_files):
2863 # gnome.init(NAME, VERSION)
2864 glade = gtk.glade.XML(GLADE)
2865 win = SoundConverterWindow(glade)
2867 error = ErrorDialog(glade)
2869 gobject.idle_add(win.filelist.add_uris, input_files)
2871 #gtk.threads_enter()
2873 #gtk.threads_leave()
2875 def cli_tags_main(input_files):
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)
2886 if not get_option('quiet'):
2887 keys = input_file.tags.keys()
2890 print ' %s: %s' % (key, input_file[key])
2896 self.current_text = ''
2898 def show(self, new_text):
2899 if new_text != self.current_text:
2901 sys.stdout.write(new_text)
2903 self.current_text = new_text
2906 sys.stdout.write('\b \b' * len(self.current_text))
2910 def cli_convert_main(input_files):
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()
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
2929 c = Converter(input_file, output_name, output_type)
2934 previous_filename = None
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:
2942 print _('%s: OK') % previous_filename
2943 previous_filename = t.sound_file.get_filename_for_display()
2946 if t.get_duration():
2947 percent = '%.1f %%' % ( 100.0* (t.get_position() / t.get_duration() ))
2949 percent = '/-\|' [int(time.time()) % 4]
2950 progress.show('%s: %s' % (t.sound_file.get_filename_for_display()[-65:], percent ))
2953 if not get_option('quiet'):
2959 args = map(filename_to_uri, args)
2960 print ' using %d thread(s)' % get_option('jobs')
2961 if get_option('mode') == 'gui':
2963 elif get_option('mode') == 'tags':
2966 cli_convert_main(args)
2969 if __name__ == '__main__':