2
# -*- coding: utf-8 -*-
4
# SoundConverter - GNOME application for converting between audio formats.
5
# Copyright 2004 Lars Wirzenius
6
# Copyright 2005-2006 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 2 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 = "@datadir@/soundconverter/soundconverter.glade"
26
if "datadir" in GLADE:
29
print "%s %s" % (NAME, VERSION)
31
# Python standard stuff.
43
# GNOME and related stuff.
52
gobject.threads_init()
55
# gnome.vfs is deprecated
71
print " using Gstreamer version: %s, Python binding version: %s" % (
72
".".join([str(s) for s in gst.gst_version]),
73
".".join([str(s) for s in gst.pygst_version]) )
75
# This is missing from gst, for some reason.
76
FORMAT_PERCENT_SCALE = 10000
81
PACKAGE = "soundconverter"
82
gettext.bindtextdomain(PACKAGE,"@datadir@/locale")
83
locale.setlocale(locale.LC_ALL,"")
84
gettext.textdomain(PACKAGE)
85
gettext.install(PACKAGE,localedir=None,unicode=1)
87
gtk.glade.bindtextdomain(PACKAGE,"@datadir@/locale")
88
gtk.glade.textdomain(PACKAGE)
91
Guillaume Bedot <guillaume.bedot wanadoo.fr> (French)
92
Dominik Zabłotny <dominz wp.pl> (Polish)
93
Jonh Wendell <wendell bani.com.br> (Portuguese Brazilian)
94
Marc E. <m4rccd yahoo.com> (Spanish)
97
# Names of columns in the file list
98
VISIBLE_COLUMNS = ["filename"]
99
ALL_COLUMNS = VISIBLE_COLUMNS + ["META"]
101
MP3_CBR, MP3_ABR, MP3_VBR = range(3)
103
# add here any format you want to be read
110
"application/vnd.rn-realmedia",
111
"application/x-shockwave-flash",
114
# add here the formats not containing tags
115
# not to bother searching in them
121
# Name and pattern for CustomFileChooser
124
("MPEG 1 Layer 3 files","*.mp3"),
125
("Ogg Vorbis files","*.ogg"),
126
("iTunes AAC files","*.m4a"),
127
("WAVEform audio format","*.wav"),
128
("Advanced Audio Coding","*.aac"),
129
("Free Lossless Audio Codec","*.flac"),
130
("Audio Codec 3","*.ac3")
134
"""similar to os.path.walk, but with gnomevfs.
136
uri -- the base folder uri.
137
return a list of uri.
140
if str(uri)[-1] != '/':
141
uri = uri.append_string("/")
146
dirlist = gnomevfs.open_directory(uri)
149
log(_("skipping: '%s'") % uri)
152
for file_info in dirlist:
153
if file_info.name[0] == ".":
156
if file_info.type == gnomevfs.FILE_TYPE_DIRECTORY:
158
vfs_walk(uri.append_path(file_info.name)) )
160
if file_info.type == gnomevfs.FILE_TYPE_REGULAR:
161
filelist.append( str(uri.append_file_name(file_info.name)) )
164
def vfs_makedirs(path_to_create):
165
"""Similar to os.makedirs, but with gnomevfs"""
167
uri = gnomevfs.URI(path_to_create)
171
uri = uri.resolve_relative("/")
173
for folder in path.split("/"):
176
uri = uri.append_string(folder)
178
gnomevfs.make_directory(uri, 0777)
179
except gnomevfs.FileExistsError:
185
def vfs_unlink(filename):
186
gnomevfs.unlink(gnomevfs.URI(filename))
188
def vfs_exists(filename):
189
return gnomevfs.exists(filename)
191
# GStreamer gnomevfssrc helpers
193
def vfs_encode_filename(filename):
196
def file_encode_filename(filename):
197
filename = gnomevfs.get_local_path_from_uri(filename)
198
filename = filename.replace(" ", "\ ");
203
def markup_escape(str):
204
str = "&".join(str.split("&"))
205
str = "<".join(str.split("<"))
206
str = ">".join(str.split(">"))
209
def filename_escape(str):
210
str = str.replace("'","\'")
211
str = str.replace("\"","\\\"")
212
str = str.replace("!","\!")
215
if gst.element_factory_find("gnomevfssrc"):
216
gstreamer_source = "gnomevfssrc"
217
gstreamer_sink = "gnomevfssink"
218
encode_filename = vfs_encode_filename
220
print " using gnomevfssrc"
222
gstreamer_source = "filesrc"
223
gstreamer_sink = "filesink"
224
encode_filename = file_encode_filename
225
print " NOT using gnomevfssrc, look for a gnomevfs gstreamer package."
228
# logging & debugging
231
if get_option("quiet") == False:
232
print " ".join([str(msg) for msg in args])
235
if get_option("debug") == True:
236
print " ".join([str(msg) for msg in args])
238
def gtk_sleep(duration):
240
while time.time() < start + duration:
241
while gtk.events_pending():
242
gtk.main_iteration(False)
246
def UNUSED_display_from_mime(mime):
249
"application/ogg": "Ogg Vorbis",
250
"audio/x-wav": "MS WAV",
251
"audio/mpeg": "MPEG 1 Layer 3 (MP3)",
252
"audio/x-flac": "FLAC",
253
"audio/x-musepack": "MusePack",
256
return mime_dict[mime]
259
class SoundConverterException(Exception):
261
def __init__(self, primary, secondary):
262
Exception.__init__(self)
263
self.primary = primary
264
self.secondary = secondary
267
def filename_to_uri(filename):
268
return "file://" + urllib.quote(os.path.abspath(filename))
273
"""Meta data information about a sound file (uri, tags)."""
275
#def __init__(self, base_path, filename=None):
276
def __init__(self, uri, base_path=None):
280
self.base_path = base_path
281
self.filename = uri[len(base_path):]
283
self.base_path, self.filename = os.path.split(self.uri)
284
self.base_path += "/"
285
self.filename_for_display = gnomevfs.unescape_string_for_display(self.filename)
289
"title": "Unknown Title",
290
"artist": "Unknown Artist",
291
"album": "Unknown Album",
293
self.have_tags = False
294
self.tags_read = False
296
self.mime_type = None
301
def get_base_path(self):
302
return self.base_path
304
def get_filename(self):
307
def get_filename_for_display(self):
308
return self.filename_for_display
310
def add_tags(self, taglist):
311
for key in taglist.keys():
312
self.tags[key] = taglist[key]
314
def get_tag_names(self):
315
return self.tags.key()
317
def get_tag(self, key, default=""):
318
return self.tags.get(key, default)
321
__getitem__ = get_tag
324
return self.tags.keys()
327
class TargetNameCreationFailure(SoundConverterException):
329
"""Exception thrown when TargetNameGenerator can't create name."""
331
def __init__(self, name):
332
SoundConverterException.__init__(self, _("File exists."),
333
_("The file %s exists already"))
335
class TargetNameGenerator:
337
"""Generator for creating the target name from an input name."""
339
nice_chars = string.ascii_letters + string.digits + ".-_/"
344
self.basename= "%(.inputname)s"
346
self.replace_messy_chars = False
349
self.exists = gnomevfs.exists
351
self.exists = os.path.exists
353
# This is useful for unit testing.
354
def set_exists(self, exists):
357
def set_target_suffix(self, suffix):
360
def set_folder(self, folder):
363
def set_subfolder_pattern(self, pattern):
364
self.subfolders = pattern
366
def set_basename_pattern(self, pattern):
367
self.basename = pattern
369
def set_replace_messy_chars(self, yes_or_no):
370
self.replace_messy_chars = yes_or_no
372
def get_target_name(self, sound_file):
374
u = gnomevfs.URI(sound_file.get_uri())
375
root, ext = os.path.splitext(u.path)
377
host = "%s:%s" % (u.host_name, u.host_port)
381
root = urlparse.urlsplit(sound_file.get_base_path())[2]
382
basename, ext = os.path.splitext(urllib.unquote(sound_file.get_filename()))
385
".inputname": basename,
391
for key in sound_file.keys():
392
dict[key] = sound_file[key]
394
pattern = os.path.join(self.subfolders, self.basename + self.suffix)
395
result = urllib.quote(pattern % dict)
396
if self.replace_messy_chars:
398
result = urllib.unquote(result)
400
if c not in self.nice_chars:
404
result = urllib.quote(s)
406
if self.folder is None:
410
result = os.path.join(folder, result)
412
tuple = (u.scheme, host, result, "", u.fragment_identifier)
413
u2 = urlparse.urlunsplit(tuple)
417
# raise TargetNameCreationFailure(u2)
423
def __init__(self, glade):
424
self.dialog = glade.get_widget("error_dialog")
425
self.primary = glade.get_widget("primary_error_label")
426
self.secondary = glade.get_widget("secondary_error_label")
428
def show(self, primary, secondary):
429
self.primary.set_markup(primary)
430
self.secondary.set_markup(secondary)
434
def show_exception(self, exception):
435
self.show("<b>%s</b>" % markup_escape(exception.primary),
441
def show(self, primary, secondary):
442
sys.stderr.write(_("\n\nError: %s\n%s\n") % (primary, secondary))
445
def show_exception(self, e):
446
self.show(e.primary, e.secondary)
452
#_thread_method = "thread"
453
#_thread_method = "idle"
454
_thread_method = "timer"
456
class BackgroundTask:
458
"""A background task.
460
To use: derive a subclass and define the methods setup, work, and
461
finish. Then call the run method when you want to start the task.
462
Call the stop method if you want to stop the task before it finishes
467
self.current_paused_time = 0
470
"""Start running the task. Call setup()."""
473
except SoundConverterException, e:
474
error.show_exception(e)
477
self.run_start_time = time.time()
478
self.current_paused_time = 0
481
if _thread_method == "timer":
482
self.id = gobject.timeout_add( int(_thread_sleep*1000), self.do_work)
483
elif _thread_method == "idle":
484
self.id = gobject.idle_add(self.do_work)
486
thread.start_new_thread(self.thread_work, ())
488
def thread_work(self):
490
while self and working:
492
working = self.do_work_()
495
while gtk.events_pending():
501
working = self.do_work_()
507
"""Do some work by calling work(). Call finish() if work is done."""
509
if _thread_method == "idle":
510
time.sleep(_thread_sleep)
512
if not self.current_paused_time:
513
self.current_paused_time = time.time()
516
if self.current_paused_time:
517
self.paused_time += time.time() - self.current_paused_time
518
self.current_paused_time = 0
523
self.run_finish_time = time.time()
528
except SoundConverterException, e:
530
error.show_exception(e)
534
"""Stop task processing. Finish() is not called."""
535
if 'id' in dir(self) and self.id is not None:
536
gobject.source_remove(self.id)
540
"""Set up the task so it can start running."""
544
"""Do some work. Return False if done, True if more work to do."""
548
"""Clean up the task after all work has been done."""
552
class TaskQueue(BackgroundTask):
556
A task queue is a queue of other tasks. If you need, for example, to
557
do simple tasks A, B, and C, you can create a TaskQueue and add the
566
The task queue behaves as a single task. It will execute the
567
tasks in order and start the next one when the previous finishes."""
570
BackgroundTask.__init__(self)
576
def is_running(self):
580
self.tasks.append(task)
581
self.tasks_number += 1
583
def get_current_task(self):
590
""" BackgroundTask setup callback """
592
self.start_time = time.time()
595
self.tasks[0].setup()
596
self.setup_hook(self.tasks[0])
600
""" BackgroundTask work callback """
602
ret = self.tasks[0].work()
603
self.work_hook(self.tasks[0])
605
self.tasks[0].finish()
606
self.finish_hook(self.tasks[0])
607
self.tasks = self.tasks[1:]
609
self.tasks_current += 1
610
self.tasks[0].setup()
611
return len(self.tasks) > 0
614
""" BackgroundTask finish callback """
616
log("Queue done in %ds" % (time.time() - self.start_time))
622
BackgroundTask.stop(self)
626
# The following hooks are called after each sub-task has been set up,
627
# after its work method has been called, and after it has finished.
628
# Subclasses may override these to provide additional processing.
630
def setup_hook(self, task):
633
def work_hook(self, task):
636
def finish_hook(self, task):
639
# The following is called when the Queue is finished
640
def queue_ended(self):
644
class NoLink(SoundConverterException):
647
SoundConverterException.__init__(self, _("Internal error"),
648
_("Couldn't link GStreamer elements.\n Please report this as a bug."))
650
class UnknownType(SoundConverterException):
652
def __init__(self, uri, mime_type):
653
SoundConverterException.__init__(self, _("Unknown type %s") % mime_type,
654
(_("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)
657
class Pipeline(BackgroundTask):
659
"""A background task for running a GstPipeline."""
662
BackgroundTask.__init__(self)
663
self.pipeline = None #gst.Pipeline()
667
self.processing = False
671
#print "Pipeline.setup()"
675
#if self.pipeline.get_state() == gst.STATE_NULL:
676
# log("error: pipeline.state == null")
681
#print " got eos:", self.sound_file.get_filename_for_display()
686
#print "Pipeline.finish()"
689
def add_command(self, command):
691
self.command += " ! "
692
self.command += command
694
def add_signal(self, name, signal, callback):
695
self.signals.append( (name, signal, callback,) )
697
def toggle_pause(self, paused):
699
self.pipeline.set_state(gst.STATE_PAUSED)
701
self.pipeline.set_state(gst.STATE_PLAYING)
703
def found_tag(self, decoder, something, taglist):
706
def on_message(self, bus, message):
708
if t == gst.MESSAGE_ERROR:
709
err, debug = message.parse_error()
711
log("error:%s (%s)" % (err, self.sound_file.get_filename_for_display()))
712
elif t == gst.MESSAGE_EOS:
714
if message.type.value_nicks[1] == "tag":
715
self.found_tag(self, "", message.parse_tag())
720
debug("launching: '%s'" % self.command)
721
self.pipeline = gst.parse_launch(self.command)
722
for name, signal, callback in self.signals:
723
self.pipeline.get_by_name(name).connect(signal,callback)
726
bus = self.pipeline.get_bus()
727
bus.add_signal_watch()
728
watch_id = bus.connect('message', self.on_message)
729
self.watch_id = watch_id
731
self.pipeline.set_state(gst.STATE_PLAYING)
733
def stop_pipeline(self):
734
if not self.pipeline:
735
log("pipeline already stopped!")
737
bus = self.pipeline.get_bus()
738
bus.disconnect(self.watch_id)
739
bus.remove_signal_watch()
740
self.pipeline.set_state(gst.STATE_NULL)
744
def get_position(self):
747
class TypeFinder(Pipeline):
748
def __init__(self, sound_file):
749
Pipeline.__init__(self)
750
self.sound_file = sound_file
752
command = '%s location="%s" ! typefind name=typefinder ! fakesink' % \
753
(gstreamer_source, encode_filename(self.sound_file.get_uri()))
754
self.add_command(command)
755
self.add_signal("typefinder", "have-type", self.have_type)
757
def set_found_type_hook(self, found_type_hook):
758
self.found_type_hook = found_type_hook
760
def have_type(self, typefind, probability, caps):
761
mime_type = caps.to_string()
762
#debug("have_type:", mime_type, self.sound_file.get_filename_for_display())
763
self.sound_file.mime_type = None
764
for t in mime_whitelist:
766
self.sound_file.mime_type = mime_type
767
if not self.sound_file.mime_type:
768
log("Mime type skipped: %s (mail us if this is an error)" % mime_type)
771
return Pipeline.work(self) and not self.sound_file.mime_type
774
Pipeline.finish(self)
775
if self.found_type_hook and self.sound_file.mime_type:
776
gobject.idle_add(self.found_type_hook, self.sound_file, self.sound_file.mime_type)
779
class Decoder(Pipeline):
781
"""A GstPipeline background task that decodes data and finds tags."""
783
def __init__(self, sound_file):
785
Pipeline.__init__(self)
786
self.sound_file = sound_file
790
command = '%s location="%s" name=src ! decodebin name=decoder' % \
791
(gstreamer_source, encode_filename(self.sound_file.get_uri()))
792
self.add_command(command)
793
self.add_signal("decoder", "new-decoded-pad", self.new_decoded_pad)
795
# TODO add error management
797
def have_type(self, typefind, probability, caps):
800
def query_duration(self):
802
if not self.sound_file.duration:
803
self.sound_file.duration = self.pipeline.query_duration(gst.FORMAT_TIME)[0] / gst.SECOND
804
debug("got file duration:", self.sound_file.duration)
805
except gst.QueryError:
808
def found_tag(self, decoder, something, taglist):
811
def _buffer_probe(self, pad, buffer):
812
"""buffer probe callback used to get real time since the beginning of the stream"""
813
if time.time() > self.time + 0.1:
814
self.time = time.time()
815
self.position = float(buffer.timestamp) / gst.SECOND
817
if buffer.timestamp == gst.CLOCK_TIME_NONE:
818
debug("removing probe")
819
pad.remove_buffer_probe(self.probe_id)
822
def new_decoded_pad(self, decoder, pad, is_last):
823
""" called when a decoded pad is created """
824
self.probe_id = pad.add_buffer_probe(self._buffer_probe)
825
self.processing = True
826
self.query_duration()
827
#self.sound_file.duration = self.pipeline.query_duration(gst.FORMAT_TIME)[0] / gst.SECOND
828
#print "new_decoded_pad duration:", self.sound_file.duration
830
def get_sound_file(self):
831
return self.sound_file
833
def get_input_uri(self):
834
return self.sound_file.get_uri()
836
def get_duration(self):
837
""" return the total duration of the sound file """
838
if not self.pipeline:
841
self.query_duration()
842
return self.sound_file.duration
844
def get_position(self):
845
""" return the current pipeline position in the stream """
848
class TagReader(Decoder):
850
"""A GstPipeline background task for finding meta tags in a file."""
852
def __init__(self, sound_file):
853
Decoder.__init__(self, sound_file)
854
self.found_tag_hook = None
855
self.found_tags = False
856
self.run_start_time = 0
857
self.add_command("fakesink")
859
def set_found_tag_hook(self, found_tag_hook):
860
self.found_tag_hook = found_tag_hook
863
def found_tag(self, decoder, something, taglist):
865
#debug("found_tags:", self.sound_file.get_filename_for_display())
866
#debug("\ttitle=%s" % (taglist["title"]))
867
#for k in taglist.keys():
868
# debug("\t%s=%s" % (k, taglist[k]))
869
self.sound_file.add_tags(taglist)
871
# tags from ogg vorbis files comes with two callbacks,
872
# the first callback containing just the stream serial number.
873
# The second callback contains the tags we're interested in.
874
#if "serial" in taglist.keys():
875
# print "Error, serial tag in ogg/vorbis, tags will be lost"
878
# we want something useful in tags
879
if "title" not in taglist.keys():
882
self.found_tags = True
883
self.sound_file.have_tags = True
886
self.sound_file.duration = self.pipeline.query_duration(gst.FORMAT_TIME)[0] / gst.SECOND
887
except gst.QueryError:
891
if not self.run_start_time:
892
self.run_start_time = time.time()
893
if self.sound_file.mime_type in tag_blacklist:
894
log("%s: type is %s, tag reading blacklisted" % (self.sound_file.get_filename_for_display(), self.sound_file.mime_type))
897
if time.time()-self.run_start_time > 5:
898
log("TagReader timeout:", self.sound_file.get_filename_for_display())
899
# stop looking for tags after 5s
901
return Decoder.work(self) and not self.found_tags
905
self.sound_file.tags_read = True
906
if self.found_tag_hook:
907
gobject.idle_add(self.found_tag_hook, self)
910
class ConversionTargetExists(SoundConverterException):
912
def __init__(self, uri):
913
SoundConverterException.__init__(self, _("Target exists."),
914
(_("The output file %s already exists.")) % uri)
917
class Converter(Decoder):
919
"""A background task for converting files to another format."""
921
def __init__(self, sound_file, output_filename, output_type):
923
Decoder.__init__(self, sound_file)
925
self.converting = True
927
self.output_filename = output_filename
928
self.output_type = output_type
929
self.vorbis_quality = None
930
self.mp3_bitrate = None
932
self.mp3_quality = None
934
self.overwrite = False
941
#print "Converter.init()"
943
"audio/x-vorbis": self.add_oggvorbis_encoder,
944
"audio/x-flac": self.add_flac_encoder,
945
"audio/x-wav": self.add_wav_encoder,
946
"audio/mpeg": self.add_mp3_encoder,
949
self.add_command("audioconvert")
950
#TODO self.add_command("audioscale")
952
encoder = self.encoders[self.output_type]()
954
# TODO: add proper error management when an encoder cannot be created
955
dialog = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR,
956
gtk.BUTTONS_OK, _("Cannot create a decoder for '%s' format.") % \
962
self.add_command(encoder)
964
uri = gnomevfs.URI(self.output_filename)
966
if dirname and not gnomevfs.exists(dirname):
967
log(_("Creating folder: '%s'") % dirname)
968
if not vfs_makedirs(str(dirname)):
969
# TODO add error management
970
dialog = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR,
971
gtk.BUTTONS_OK, _("Cannot create '%s' folder.") % \
977
self.add_command('gnomevfssink location=%s' % uri)
978
if self.overwrite and vfs_exists(self.output_filename):
979
log("overwriting '%s'" % self.output_filename)
980
vfs_unlink(self.output_filename)
981
#log( _("Writing to: '%s'") % urllib.unquote(self.output_filename) )
984
Pipeline.finish(self)
986
# Copy file permissions
988
info = gnomevfs.get_file_info( self.sound_file.get_uri(),gnomevfs.FILE_INFO_FIELDS_PERMISSIONS)
989
gnomevfs.set_file_info(self.output_filename, info, gnomevfs.SET_FILE_INFO_PERMISSIONS)
991
log(_("Cannot set permission on '%s'") % gnomevfs.format_uri_for_display(self.output_filename))
993
def get_position(self):
996
def set_vorbis_quality(self, quality):
997
self.vorbis_quality = quality
999
def set_mp3_mode(self, mode):
1000
self.mp3_mode = mode
1002
def set_mp3_quality(self, quality):
1003
self.mp3_quality = quality
1005
def add_flac_encoder(self):
1008
def add_wav_encoder(self):
1011
def add_oggvorbis_encoder(self):
1013
if self.vorbis_quality is not None:
1014
cmd += " quality=%s" % self.vorbis_quality
1018
def add_mp3_encoder(self):
1020
cmd = "lame quality=2 "
1022
if self.mp3_mode is not None:
1024
"cbr" : (0,"bitrate"),
1025
"abr" : (3,"vbr-mean-bitrate"),
1026
"vbr" : (4,"vbr-quality")
1029
if properties[self.mp3_mode][0]:
1030
cmd += "xingheader=true "
1032
cmd += "vbr=%s " % properties[self.mp3_mode][0]
1033
if self.mp3_quality == 9:
1034
# GStreamer set max bitrate to 320 but lame uses
1035
# mpeg2 with vbr-quality==9, so max bitrate is 160
1036
cmd += "vbr-max-bitrate=160 "
1038
cmd += "%s=%s " % (properties[self.mp3_mode][1], self.mp3_quality)
1043
"""List of files added by the user."""
1045
# List of MIME types which we accept for drops.
1046
drop_mime_types = ["text/uri-list", "text/plain", "STRING"]
1048
def __init__(self, window, glade):
1049
self.window = window
1050
self.tagreaders = TaskQueue()
1051
self.typefinders = TaskQueue()
1052
# handle the current task for status
1057
for name in ALL_COLUMNS:
1058
if name in VISIBLE_COLUMNS:
1059
args.append(gobject.TYPE_STRING)
1061
args.append(gobject.TYPE_PYOBJECT)
1062
self.model = apply(gtk.ListStore, args)
1064
self.widget = glade.get_widget("filelist")
1065
self.widget.set_model(self.model)
1066
self.widget.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
1068
self.widget.drag_dest_set(gtk.DEST_DEFAULT_ALL,
1070
(self.drop_mime_types[i], 0, i),
1071
range(len(self.drop_mime_types))),
1072
gtk.gdk.ACTION_COPY)
1073
self.widget.connect("drag_data_received", self.drag_data_received)
1075
renderer = gtk.CellRendererText()
1076
for name in VISIBLE_COLUMNS:
1077
column = gtk.TreeViewColumn(name,
1079
markup=ALL_COLUMNS.index(name))
1080
self.widget.append_column(column)
1082
def drag_data_received(self, widget, context, x, y, selection,
1085
if mime_id >= 0 and mime_id < len(self.drop_mime_types):
1087
for uri in selection.data.split("\n"):
1090
info = gnomevfs.get_file_info(uri, gnomevfs.FILE_INFO_DEFAULT)
1091
if info.type == gnomevfs.FILE_TYPE_DIRECTORY:
1092
file_list.extend(vfs_walk(gnomevfs.URI(uri)))
1094
file_list.append(uri)
1095
context.finish(True, False, time)
1096
base = os.path.commonprefix(file_list)
1097
#[self.add_file(SoundFile(base, uri[len(base):])) for uri in file_list]
1098
#[self.add_file(SoundFile(uri, base)) for uri in file_list]
1099
self.add_uris(file_list)
1101
def get_files(self):
1103
i = self.model.get_iter_first()
1106
for c in ALL_COLUMNS:
1107
f[c] = self.model.get_value(i, ALL_COLUMNS.index(c))
1108
files.append(f["META"])
1110
i = self.model.iter_next(i)
1113
def found_type(self, sound_file, mime):
1114
#debug("found_type", sound_file.get_filename())
1116
self.append_file(sound_file)
1117
self.window.set_sensitive()
1119
tagreader = TagReader(sound_file)
1120
tagreader.set_found_tag_hook(self.append_file_tags)
1122
self.tagreaders.add(tagreader)
1123
if not self.tagreaders.is_running():
1124
self.tagreaders.run()
1126
def add_uris(self, uris, base=None):
1132
info = gnomevfs.get_file_info(gnomevfs.URI(uri))
1133
except gnomevfs.NotFoundError:
1135
except gnomevfs.InvalidURIError:
1138
if info.type == gnomevfs.FILE_TYPE_DIRECTORY:
1139
filelist = vfs_walk(gnomevfs.URI(uri))
1142
#f = f[len(base)+1:]
1147
base,notused = os.path.split(os.path.commonprefix(files))
1151
sound_file = SoundFile(f, base)
1152
if sound_file.get_uri() in self.filelist:
1153
log(_("file already present: '%s'") % sound_file.get_uri())
1155
#print "adding: '%s'" % sound_file.get_filename_for_display()
1156
self.filelist[sound_file.get_uri()] = True
1158
typefinder = TypeFinder(sound_file)
1159
typefinder.set_found_type_hook(self.found_type)
1160
self.typefinders.add(typefinder)
1162
if not self.typefinders.is_running():
1163
self.typefinders.queue_ended = self.typefinder_queue_ended
1164
self.typefinders.run()
1166
def typefinder_queue_ended(self):
1167
print "typefinder_queue_ended"
1169
def format_cell(self, sound_file):
1171
template_tags = "%(artist)s - <i>%(album)s</i> - <b>%(title)s</b>\n<small>%(filename)s</small>"
1172
template_loading = "<i>%s</i>\n<small>%%(filename)s</small>" \
1173
% _("loading tags...")
1174
template_notags = '<span foreground="red">%s</span>\n<small>%%(filename)s</small>' \
1178
params["filename"] = markup_escape(urllib.unquote(sound_file.get_filename_for_display()))
1179
for item in ("title", "artist", "album"):
1180
params[item] = markup_escape(sound_file.get_tag(item))
1181
if sound_file["bitrate"]:
1182
params["bitrate"] = ", %s kbps" % (sound_file["bitrate"] / 1000)
1184
params["bitrate"] = ""
1187
if sound_file.have_tags:
1188
template = template_tags
1190
if sound_file.tags_read:
1191
template = template_notags
1193
template = template_loading
1195
s = template % params
1196
except UnicodeDecodeError:
1198
for c in markup_escape(urllib.unquote(sound_file.get_uri())):
1202
str += '<span foreground="yellow" background="red"><b>!</b></span>'
1204
error.show(_("Invalid character in filename!"), str)
1209
def append_file(self, sound_file):
1211
#print "+", sound_file.get_filename_for_display()
1212
iter = self.model.append()
1213
sound_file.model = iter
1214
self.model.set(iter, 0, self.format_cell(sound_file))
1215
self.model.set(iter, 1, sound_file)
1216
self.window.progressbar.pulse()
1219
def append_file_tags(self, tagreader):
1220
sound_file = tagreader.get_sound_file()
1223
for key in ALL_COLUMNS:
1224
fields[key] = _("unknown")
1225
fields["META"] = sound_file
1226
fields["filename"] = sound_file.get_filename_for_display()
1228
self.model.set(sound_file.model, 0, self.format_cell(sound_file))
1229
self.window.set_sensitive()
1230
self.window.progressbar.pulse()
1232
def remove(self, iter):
1233
uri = self.model.get(iter, 1)[0].get_uri()
1234
del self.filelist[uri]
1235
self.model.remove(iter)
1237
def is_nonempty(self):
1239
self.model.get_iter((0,))
1245
class PreferencesDialog:
1247
root = "/apps/SoundConverter"
1249
basename_patterns = [
1250
("%(.inputname)s", _("Same as input, but with new suffix")),
1251
("%(track-number)02d-%(title)s", _("Track number - title")),
1252
("%(title)s", _("Track title")),
1253
("%(artist)s-%(title)s", _("Artist - title")),
1254
("Custom", _("Custom filename pattern")),
1257
subfolder_patterns = [
1258
("%(artist)s/%(album)s", _("artist/album")),
1259
("%(artist)s-%(album)s", _("artist-album")),
1263
"same-folder-as-input": 1,
1264
"selected-folder": os.path.expanduser("~"),
1265
"create-subfolders": 0,
1266
"subfolder-pattern-index": 0,
1267
"name-pattern-index": 0,
1268
"custom-filename-pattern": "{Track} - {Title}",
1269
"replace-messy-chars": 0,
1270
"output-mime-type": "audio/x-vorbis",
1271
"output-suffix": ".ogg",
1272
"vorbis-quality": 0.6,
1273
"mp3-mode": "vbr", # 0: cbr, 1: abr, 2: vbr
1274
"mp3-cbr-quality": 192,
1275
"mp3-abr-quality": 192,
1276
"mp3-vbr-quality": 3,
1279
sensitive_names = ["vorbis_quality", "choose_folder", "create_subfolders",
1280
"subfolder_pattern"]
1282
def __init__(self, glade):
1283
self.gconf = gconf.client_get_default()
1284
self.gconf.add_dir(self.root, gconf.CLIENT_PRELOAD_ONELEVEL)
1285
self.dialog = glade.get_widget("prefsdialog")
1286
self.into_selected_folder = glade.get_widget("into_selected_folder")
1287
self.target_folder_chooser = glade.get_widget("target_folder_chooser")
1288
self.basename_pattern = glade.get_widget("basename_pattern")
1289
self.custom_filename_box = glade.get_widget("custom_filename_box")
1290
self.custom_filename = glade.get_widget("custom_filename")
1291
self.example = glade.get_widget("example_filename")
1292
self.aprox_bitrate = glade.get_widget("aprox_bitrate")
1293
self.quality_tabs = glade.get_widget("quality_tabs")
1295
self.target_bitrate = None
1296
self.convert_setting_from_old_version()
1298
self.sensitive_widgets = {}
1299
for name in self.sensitive_names:
1300
self.sensitive_widgets[name] = glade.get_widget(name)
1301
assert self.sensitive_widgets[name] != None
1302
self.set_widget_initial_values(glade)
1303
self.set_sensitive()
1306
def convert_setting_from_old_version(self):
1307
""" try to convert previous settings"""
1309
# TODO: why not just reseting the settings if we cannot load them ?
1311
# vorbis quality was stored as an int enum
1313
self.get_float("vorbis-quality")
1314
except gobject.GError:
1315
log("converting old vorbis setting...")
1316
old_quality = self.get_int("vorbis-quality")
1317
self.gconf.unset(self.path("vorbis-quality"))
1318
quality_setting = (0,0.2,0.3,0.6,0.8)
1319
self.set_float("vorbis-quality", quality_setting[old_quality])
1321
# mp3 quality was stored as an int enum
1322
cbr = self.get_int("mp3-cbr-quality")
1324
log("converting old mp3 quality setting... (%d)" % cbr)
1326
abr = self.get_int("mp3-abr-quality")
1327
vbr = self.get_int("mp3-vbr-quality")
1329
cbr_quality = (64, 96, 128, 192, 256)
1330
vbr_quality = (9, 7, 5, 3, 1)
1332
self.set_int("mp3-cbr-quality", cbr_quality[cbr])
1333
self.set_int("mp3-abr-quality", cbr_quality[abr])
1334
self.set_int("mp3-vbr-quality", vbr_quality[vbr])
1336
# mp3 mode was stored as an int enum
1338
self.get_string("mp3-mode")
1339
except gobject.GError:
1340
log("converting old mp3 mode setting...")
1341
old_mode = self.get_int("mp3-mode")
1342
self.gconf.unset(self.path("mp3-mode"))
1343
modes = ("cbr","abr","vbr")
1344
self.set_string("mp3-mode", modes[old_mode])
1346
self.gconf.clear_cache()
1348
def set_widget_initial_values(self, glade):
1350
self.quality_tabs.set_show_tabs(False)
1352
if self.get_int("same-folder-as-input"):
1353
w = glade.get_widget("same_folder_as_input")
1355
w = glade.get_widget("into_selected_folder")
1358
self.target_folder_chooser.set_filename(
1359
self.get_string("selected-folder"))
1360
self.update_selected_folder()
1362
w = glade.get_widget("create_subfolders")
1363
w.set_active(self.get_int("create-subfolders"))
1365
w = glade.get_widget("subfolder_pattern")
1366
model = w.get_model()
1368
for pattern, desc in self.subfolder_patterns:
1370
model.set(i, 0, desc)
1371
w.set_active(self.get_int("subfolder-pattern-index"))
1373
if self.get_int("replace-messy-chars"):
1374
w = glade.get_widget("replace_messy_chars")
1377
mime_type = self.get_string("output-mime-type")
1379
# desactivate mp3 output if encoder plugin is not present
1380
if not gst.element_factory_find("lame"):
1381
log("LAME GStreamer plugin not found, desactivating MP3 output.")
1382
w = glade.get_widget("output_mime_type_mp3")
1383
w.set_sensitive(False)
1384
mime_type = self.defaults["output-mime-type"]
1388
"audio/x-vorbis": "output_mime_type_ogg_vorbis",
1389
"audio/x-flac": "output_mime_type_flac",
1390
"audio/x-wav": "output_mime_type_wav",
1391
"audio/mpeg": "output_mime_type_mp3",
1392
}.get(mime_type, None)
1394
w = glade.get_widget(widget_name)
1396
self.change_mime_type(mime_type)
1398
w = glade.get_widget("vorbis_quality")
1399
quality = self.get_float("vorbis-quality")
1400
quality_setting = {0:0 ,0.2:1 ,0.4:2 ,0.6:3 , 0.8:4}
1401
for k, v in quality_setting.iteritems():
1402
if abs(quality-k) < 0.01:
1405
self.mp3_quality = glade.get_widget("mp3_quality")
1406
self.mp3_mode = glade.get_widget("mp3_mode")
1408
mode = self.get_string("mp3-mode")
1409
self.change_mp3_mode(mode)
1411
w = glade.get_widget("basename_pattern")
1412
model = w.get_model()
1414
for pattern, desc in self.basename_patterns:
1415
iter = model.append()
1416
model.set(iter, 0, desc)
1417
w.set_active(self.get_int("name-pattern-index"))
1420
self.custom_filename.set_text(self.get_string("custom-filename-pattern"))
1421
if self.basename_pattern.get_active() == len(self.basename_patterns)-1:
1422
self.custom_filename_box.set_sensitive(True)
1424
self.custom_filename_box.set_sensitive(False)
1426
self.update_example()
1428
def update_selected_folder(self):
1429
self.into_selected_folder.set_label(_("Into folder %s") %
1430
self.get_string("selected-folder"))
1433
def get_bitrate_from_settings(self):
1436
mode = self.get_string("mp3-mode")
1438
mime_type = self.get_string("output-mime-type")
1440
if mime_type == "audio/x-vorbis":
1441
quality = self.get_float("vorbis-quality")*10
1442
quality = int(quality)
1443
bitrates = (64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 500)
1444
bitrate = bitrates[quality]
1446
elif mime_type == "audio/mpeg":
1448
"cbr": "mp3-cbr-quality",
1449
"abr": "mp3-abr-quality",
1450
"vbr": "mp3-vbr-quality"
1452
bitrate = self.get_int(quality[mode])
1454
# hum, not really, but who cares? :)
1455
bitrates = (320, 256, 224, 192, 160, 128, 112, 96, 80, 64)
1456
bitrate = bitrates[bitrate]
1462
return "~%d kbps" % bitrate
1464
return "%d kbps" % bitrate
1469
def update_example(self):
1470
sound_file = SoundFile(os.path.expanduser("~/foo/bar.flac"))
1471
sound_file.add_tags({
1472
"artist": "<b>{Artist}</b>",
1473
"title": "<b>{Title}</b>",
1474
"album": "<b>{Album}</b>",
1478
self.example.set_markup(self.generate_filename(sound_file, for_display=True))
1480
markup = _("<small>Target bitrate: %s</small>") % self.get_bitrate_from_settings()
1481
self.aprox_bitrate.set_markup( markup )
1483
def generate_filename(self, sound_file, for_display=False):
1484
self.gconf.clear_cache()
1485
output_type = self.get_string("output-mime-type")
1487
"audio/x-vorbis": ".ogg",
1488
"audio/x-flac": ".flac",
1489
"audio/x-wav": ".wav",
1490
"audio/mpeg": ".mp3",
1491
}.get(output_type, None)
1493
generator = TargetNameGenerator()
1494
generator.set_target_suffix(output_suffix)
1495
if self.get_int("same-folder-as-input"):
1496
tuple = urlparse.urlparse(sound_file.get_uri())
1498
generator.set_folder(os.path.dirname(path))
1500
path, filename = os.path.split(sound_file.get_filename())
1503
generator.set_folder(os.path.join(self.get_string("selected-folder"), path))
1504
if self.get_int("create-subfolders"):
1505
generator.set_subfolder_pattern(
1506
self.get_subfolder_pattern())
1507
generator.set_basename_pattern(self.get_basename_pattern())
1509
generator.set_replace_messy_chars(False)
1510
return urllib.unquote(generator.get_target_name(sound_file))
1512
generator.set_replace_messy_chars(
1513
self.get_int("replace-messy-chars"))
1514
return generator.get_target_name(sound_file)
1516
def process_custom_pattern(self, pattern):
1517
pattern = pattern.replace("{Artist}", "%(artist)s")
1518
pattern = pattern.replace("{Album}", "%(album)s")
1519
pattern = pattern.replace("{Title}", "%(title)s")
1520
pattern = pattern.replace("{Track}", "%(track-number)02d")
1521
pattern = pattern.replace("{Total}", "%(track-total)02d")
1524
def set_sensitive(self):
1529
for widget in self.sensitive_widgets.values():
1530
widget.set_sensitive(False)
1532
x = self.get_int("same-folder-as-input")
1533
for name in ["choose_folder", "create_subfolders",
1534
"subfolder_pattern"]:
1535
self.sensitive_widgets[name].set_sensitive(not x)
1537
self.sensitive_widgets["vorbis_quality"].set_sensitive(
1538
self.get_string("output-mime-type") == "audio/x-vorbis")
1540
def path(self, key):
1541
assert self.defaults.has_key(key)
1542
return "%s/%s" % (self.root, key)
1544
def get_with_default(self, getter, key):
1545
if self.gconf.get(self.path(key)) is None:
1546
return self.defaults[key]
1548
return getter(self.path(key))
1550
def get_int(self, key):
1551
return self.get_with_default(self.gconf.get_int, key)
1553
def set_int(self, key, value):
1554
self.gconf.set_int(self.path(key), value)
1556
def get_float(self, key):
1557
return self.get_with_default(self.gconf.get_float, key)
1559
def set_float(self, key, value):
1560
self.gconf.set_float(self.path(key), value)
1562
def get_string(self, key):
1563
return self.get_with_default(self.gconf.get_string, key)
1565
def set_string(self, key, value):
1566
self.gconf.set_string(self.path(key), value)
1572
def on_same_folder_as_input_toggled(self, button):
1573
if button.get_active():
1574
self.set_int("same-folder-as-input", 1)
1575
self.set_sensitive()
1576
self.update_example()
1578
def on_into_selected_folder_toggled(self, button):
1579
if button.get_active():
1580
self.set_int("same-folder-as-input", 0)
1581
self.set_sensitive()
1582
self.update_example()
1584
def on_choose_folder_clicked(self, button):
1585
ret = self.target_folder_chooser.run()
1586
self.target_folder_chooser.hide()
1587
if ret == gtk.RESPONSE_OK:
1588
folder = self.target_folder_chooser.get_filename()
1590
self.set_string("selected-folder", folder)
1591
self.update_selected_folder()
1592
self.update_example()
1594
def on_create_subfolders_toggled(self, button):
1595
if button.get_active():
1596
self.set_int("create-subfolders", 1)
1598
self.set_int("create-subfolders", 0)
1599
self.update_example()
1601
def on_subfolder_pattern_changed(self, combobox):
1602
self.set_int("subfolder-pattern-index", combobox.get_active())
1603
self.update_example()
1605
def get_subfolder_pattern(self):
1606
index = self.get_int("subfolder-pattern-index")
1607
if index < 0 or index >= len(self.subfolder_patterns):
1609
return self.subfolder_patterns[index][0]
1611
def on_basename_pattern_changed(self, combobox):
1612
self.set_int("name-pattern-index", combobox.get_active())
1613
if combobox.get_active() == len(self.basename_patterns)-1:
1614
self.custom_filename_box.set_sensitive(True)
1616
self.custom_filename_box.set_sensitive(False)
1617
self.update_example()
1619
def get_basename_pattern(self):
1620
index = self.get_int("name-pattern-index")
1621
if index < 0 or index >= len(self.basename_patterns):
1623
if self.basename_pattern.get_active() == len(self.basename_patterns)-1:
1624
return self.process_custom_pattern(self.custom_filename.get_text())
1626
return self.basename_patterns[index][0]
1628
def on_custom_filename_changed(self, entry):
1629
self.set_string("custom-filename-pattern", entry.get_text())
1630
self.update_example()
1632
def on_replace_messy_chars_toggled(self, button):
1633
if button.get_active():
1634
self.set_int("replace-messy-chars", 1)
1636
self.set_int("replace-messy-chars", 0)
1637
self.update_example()
1639
def change_mime_type(self, mime_type):
1640
self.set_string("output-mime-type", mime_type)
1641
self.set_sensitive()
1642
self.update_example()
1644
"audio/x-vorbis": 0,
1649
self.quality_tabs.set_current_page(tabs[mime_type])
1651
def on_output_mime_type_ogg_vorbis_toggled(self, button):
1652
if button.get_active():
1653
self.change_mime_type("audio/x-vorbis")
1655
def on_output_mime_type_flac_toggled(self, button):
1656
if button.get_active():
1657
self.change_mime_type("audio/x-flac")
1659
def on_output_mime_type_wav_toggled(self, button):
1660
if button.get_active():
1661
self.change_mime_type("audio/x-wav")
1663
def on_output_mime_type_mp3_toggled(self, button):
1664
if button.get_active():
1665
self.change_mime_type("audio/mpeg")
1667
def on_vorbis_quality_changed(self, combobox):
1668
quality = (0,0.2,0.4,0.6,0.8)
1669
self.set_float("vorbis-quality", quality[combobox.get_active()])
1671
self.update_example()
1673
def change_mp3_mode(self, mode):
1675
keys = { "cbr": 0, "abr": 1, "vbr": 2 }
1676
self.mp3_mode.set_active(keys[mode]);
1679
"cbr": "mp3-cbr-quality",
1680
"abr": "mp3-abr-quality",
1681
"vbr": "mp3-vbr-quality",
1683
quality = self.get_int(keys[mode])
1685
quality_to_preset = {
1686
"cbr": {64:0, 96:1, 128:2, 192:3, 256:4},
1687
"abr": {64:0, 96:1, 128:2, 192:3, 256:4},
1688
"vbr": {9:0, 7:1, 5:2, 3:3, 1:4}, # inverted !
1691
if quality in quality_to_preset[mode]:
1692
self.mp3_quality.set_active(quality_to_preset[mode][quality])
1694
self.update_example()
1696
def on_mp3_mode_changed(self, combobox):
1697
mode = ("cbr","abr","vbr")[combobox.get_active()]
1698
self.set_string("mp3-mode", mode)
1699
self.change_mp3_mode(mode)
1701
def on_mp3_quality_changed(self, combobox):
1703
"cbr": "mp3-cbr-quality",
1704
"abr": "mp3-abr-quality",
1705
"vbr": "mp3-vbr-quality"
1708
"cbr": (64, 96, 128, 192, 256),
1709
"abr": (64, 96, 128, 192, 256),
1710
"vbr": (9, 7, 5, 3, 1),
1712
mode = self.get_string("mp3-mode")
1713
self.set_int(keys[mode], quality[mode][combobox.get_active()])
1715
self.update_example()
1718
class ConverterQueueCanceled(SoundConverterException):
1720
"""Exception thrown when a ConverterQueue is canceled."""
1723
SoundConverterException.__init__(self, _("Convertion Canceled"), "")
1726
class ConverterQueue(TaskQueue):
1728
"""Background task for converting many files."""
1730
def __init__(self, window):
1731
TaskQueue.__init__(self)
1732
self.window = window
1733
self.overwrite_action = None
1734
self.reset_counters()
1736
def reset_counters(self):
1737
self.total_duration = 0
1738
self.duration_processed = 0
1739
self.overwrite_action = None
1741
def add(self, sound_file):
1743
output_filename = self.window.prefs.generate_filename(sound_file)
1744
path = urlparse.urlparse(output_filename) [2]
1745
path = urllib.unquote(path)
1749
gnomevfs.get_file_info(gnomevfs.URI((output_filename)))
1750
except gnomevfs.NotFoundError:
1752
except gnomevfs.InvalidURIError:
1753
log("Invalid URI: '%s'" % output_filename)
1757
if self.overwrite_action != None:
1758
result = self.overwrite_action
1760
dialog = self.window.existsdialog
1762
dpath = os.path.basename(path)
1763
dpath = markup_escape(dpath)
1766
_("The output file <i>%s</i>\n exists already.\n Do you want to skip the file, overwrite it or cancel the conversion?\n") % \
1769
dialog.message.set_markup(msg)
1771
if self.overwrite_action != None:
1772
dialog.apply_to_all.set_active(True)
1774
dialog.apply_to_all.set_active(False)
1776
result = dialog.run()
1779
if dialog.apply_to_all.get_active():
1780
if result == 1 or result == 0:
1781
self.overwrite_action = result
1787
vfs_unlink(output_filename)
1788
except gnomevfs.NotFoundError:
1796
raise ConverterQueueCanceled()
1798
c = Converter(sound_file, output_filename,
1799
self.window.prefs.get_string("output-mime-type"))
1800
c.set_vorbis_quality(self.window.prefs.get_float("vorbis-quality"))
1803
"cbr": "mp3-cbr-quality",
1804
"abr": "mp3-abr-quality",
1805
"vbr": "mp3-vbr-quality"
1807
mode = self.window.prefs.get_string("mp3-mode")
1808
c.set_mp3_mode(mode)
1809
c.set_mp3_quality(self.window.prefs.get_int(quality[mode]))
1811
TaskQueue.add(self, c)
1812
self.total_duration += c.get_duration()
1814
def work_hook(self, task):
1815
gobject.idle_add(self.set_progress, (task))
1817
def get_progress(self, task):
1818
return (self.duration_processed + task.get_position()) / self.total_duration
1820
def set_progress(self, task):
1821
t = self.get_current_task()
1824
f = t.sound_file.get_filename_for_display()
1826
self.window.set_progress(self.duration_processed + task.get_position(),
1827
self.total_duration, f)
1830
def finish_hook(self, task):
1831
#print "finished: %d+=%d" % (self.duration_processed, task.get_duration())
1832
self.duration_processed += task.get_duration()
1835
TaskQueue.finish(self)
1836
self.reset_counters()
1837
self.window.set_progress(0, 0)
1838
self.window.set_sensitive()
1839
self.window.conversion_ended()
1840
total_time = self.run_finish_time - self.run_start_time
1841
self.window.set_status(_("Conversion done, in %s") %
1842
self.format_time(total_time))
1844
def format_time(self, seconds):
1845
units = [(86400, "d"),
1849
seconds = round(seconds)
1851
for factor, name in units:
1852
count = int(seconds / factor)
1853
seconds -= count * factor
1854
if count > 0 or (factor == 1 and not result):
1855
result.append("%d %s" % (count, name))
1857
return " ".join(result)
1860
TaskQueue.stop(self)
1861
self.window.set_progress(0, 0)
1862
self.window.set_sensitive()
1864
class CustomFileChooser:
1866
Custom file chooser.\n
1871
Load glade object, create a combobox
1873
xml = gtk.glade.XML(GLADE,"custom_file_chooser")
1874
self.dlg = xml.get_widget("custom_file_chooser")
1875
self.dlg.set_title(_("Open a file"))
1878
self.fcw = xml.get_widget("filechooserwidget")
1879
self.fcw.set_local_only(not use_gnomevfs)
1880
self.fcw.set_select_multiple(True)
1884
# Create combobox model
1885
self.combo = xml.get_widget("filtercombo")
1886
self.combo.connect("changed",self.on_combo_changed)
1887
self.store = gtk.ListStore(str)
1888
self.combo.set_model(self.store)
1889
combo_rend = gtk.CellRendererText()
1890
self.combo.pack_start(combo_rend, True)
1891
self.combo.add_attribute(combo_rend, 'text', 0)
1893
# get all (gstreamer) knew files Todo
1894
for files in filepattern:
1895
self.add_pattern(files[0],files[1])
1896
self.combo.set_active(0)
1898
def add_pattern(self,name,pat):
1900
Add a new pattern to the combobox.
1901
@param name: The pattern name.
1903
@param pat: the pattern
1906
self.pattern.append(pat)
1907
self.store.append(["%s (%s)" %(name,pat)])
1909
def on_combo_changed(self,w):
1911
Callback for combobox "changed" signal\n
1912
Set a new filter for the filechooserwidget
1914
filter = gtk.FileFilter()
1915
filter.add_pattern(self.pattern[self.combo.get_active()])
1916
self.fcw.set_filter(filter)
1922
return self.dlg.run()
1932
Return all the selected uris
1934
return self.fcw.get_uris()
1937
class SoundConverterWindow:
1939
"""Main application class."""
1941
sensitive_names = [ "remove", "convert_button" ]
1942
unsensitive_when_converting = [ "remove", "prefs_button" ,"toolbutton_addfile", "toolbutton_addfolder", "filelist", "menubar" ]
1944
def __init__(self, glade):
1946
self.widget = glade.get_widget("window")
1947
self.filelist = FileList(self, glade)
1948
self.filelist_selection = self.filelist.widget.get_selection()
1949
self.filelist_selection.connect("changed", self.selection_changed)
1950
self.existsdialog = glade.get_widget("existsdialog")
1951
self.existsdialog.message = glade.get_widget("exists_message")
1952
self.existsdialog.apply_to_all = glade.get_widget("apply_to_all")
1953
self.existslabel = glade.get_widget("existslabel")
1954
self.progressbar = glade.get_widget("progressbar")
1955
self.status = glade.get_widget("statustext")
1956
self.about = glade.get_widget("about")
1957
self.prefs = PreferencesDialog(glade)
1959
self.progressframe = glade.get_widget("progress_frame")
1960
self.statusframe = glade.get_widget("status_frame")
1961
self.progressfile = glade.get_widget("progressfile")
1963
self.addchooser = CustomFileChooser()
1964
self.addfolderchooser = gtk.FileChooserDialog(_("Add Folder..."),
1966
gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
1968
gtk.RESPONSE_CANCEL,
1971
self.addfolderchooser.set_select_multiple(True)
1972
self.addfolderchooser.set_local_only(not use_gnomevfs)
1974
self.connect(glade, [self.prefs])
1976
self.about.set_property("name", NAME)
1977
self.about.set_property("version", VERSION)
1979
self.convertion_waiting = False
1981
self.converter = ConverterQueue(self)
1983
self._lock_convert_button = False
1985
self.sensitive_widgets = {}
1986
for name in self.sensitive_names:
1987
self.sensitive_widgets[name] = glade.get_widget(name)
1988
for name in self.unsensitive_when_converting:
1989
self.sensitive_widgets[name] = glade.get_widget(name)
1991
self.set_sensitive()
1995
# This bit of code constructs a list of methods for binding to Gtk+
1996
# signals. This way, we don't have to maintain a list manually,
1997
# saving editing effort. It's enough to add a method to the suitable
1998
# class and give the same name in the .glade file.
2000
def connect(self, glade, objects):
2002
for o in [self] + objects:
2003
for name, member in inspect.getmembers(o):
2004
dicts[name] = member
2005
glade.signal_autoconnect(dicts)
2007
def close(self, *args):
2008
self.converter.stop()
2009
self.widget.destroy()
2013
on_window_delete_event = close
2014
on_quit_activate = close
2015
on_quit_button_clicked = close
2017
def on_add_activate(self, *args):
2018
ret = self.addchooser.run()
2019
self.addchooser.hide()
2020
if ret == gtk.RESPONSE_OK:
2022
#for uri in self.addchooser.get_uris():
2023
# files.append(SoundFile(uri))
2024
#self.filelist.add_files(files)
2025
self.filelist.add_uris(self.addchooser.get_uris())
2026
self.set_sensitive()
2029
def on_addfolder_activate(self, *args):
2030
ret = self.addfolderchooser.run()
2031
self.addfolderchooser.hide()
2032
if ret == gtk.RESPONSE_OK:
2033
folders = self.addfolderchooser.get_uris()
2035
self.filelist.add_uris(folders)
2037
#base,notused = os.path.split(os.path.commonprefix(folders))
2040
#for folder in folders:
2041
# filelist.extend(vfs_walk(gnomevfs.URI(folder)))
2043
# f = f[len(base)+1:]
2044
# files.append(SoundFile(base+"/", f))
2045
#self.filelist.add_files(files)
2046
self.set_sensitive()
2048
def on_remove_activate(self, *args):
2049
model, paths = self.filelist_selection.get_selected_rows()
2051
i = self.filelist.model.get_iter(paths[0])
2052
self.filelist.remove(i)
2053
model, paths = self.filelist_selection.get_selected_rows()
2054
self.set_sensitive()
2056
def do_convert(self):
2058
for sound_file in self.filelist.get_files():
2059
self.converter.add(sound_file)
2060
except ConverterQueueCanceled:
2061
log(_("canceling conversion."))
2064
self.converter.run()
2065
self.convertion_waiting = False
2066
self.set_sensitive()
2069
def wait_tags_and_convert(self):
2070
not_ready = [s for s in self.filelist.get_files() if not s.tags_read]
2072
self.progressbar.pulse()
2079
def on_convert_button_clicked(self, *args):
2080
if self._lock_convert_button:
2083
if not self.converter.is_running():
2084
self.set_status(_("Waiting for tags"))
2085
self.progressframe.show()
2086
self.statusframe.hide()
2087
self.progress_time = time.time()
2088
#self.widget.set_sensitive(False)
2090
self.convertion_waiting = True
2091
self.set_status(_("Waiting for tags..."))
2093
#thread.start_thread(self.do_convert, ())
2095
#gobject.timeout_add(100, self.wait_tags_and_convert)
2097
self.converter.paused = not self.converter.paused
2098
if self.converter.paused:
2099
self.set_status(_("Paused"))
2102
self.set_sensitive()
2104
def on_button_pause_clicked(self, *args):
2105
task = self.converter.get_current_task()
2107
self.converter.paused = not self.converter.paused
2108
task.toggle_pause(self.converter.paused)
2111
if self.converter.paused:
2112
self.display_progress(_("Paused"))
2114
def on_button_cancel_clicked(self, *args):
2115
self.converter.stop()
2116
self.set_status(_("Canceled"))
2117
self.set_sensitive()
2118
self.conversion_ended()
2120
def on_select_all_activate(self, *args):
2121
self.filelist.widget.get_selection().select_all()
2123
def on_clear_activate(self, *args):
2124
self.filelist.widget.get_selection().unselect_all()
2126
def on_preferences_activate(self, *args):
2129
on_prefs_button_clicked = on_preferences_activate
2131
def on_about_activate(self, *args):
2132
about = gtk.glade.XML(GLADE, "about").get_widget("about")
2133
about.set_property("name", NAME)
2134
about.set_property("version", VERSION)
2135
about.set_property("translator_credits", TRANSLATORS)
2138
def selection_changed(self, *args):
2139
self.set_sensitive()
2141
def conversion_ended(self):
2142
self.progressframe.hide()
2143
self.statusframe.show()
2144
self.widget.set_sensitive(True)
2146
def set_widget_sensitive(self, name, sensitivity):
2147
self.sensitive_widgets[name].set_sensitive(sensitivity)
2149
def set_sensitive(self):
2151
[self.set_widget_sensitive(w, not self.converter.is_running())
2152
for w in self.unsensitive_when_converting]
2154
self.set_widget_sensitive("remove",
2155
self.filelist_selection.count_selected_rows() > 0)
2156
self.set_widget_sensitive("convert_button",
2157
self.filelist.is_nonempty())
2159
self._lock_convert_button = True
2160
self.sensitive_widgets["convert_button"].set_active(
2161
self.converter.is_running() and not self.converter.paused )
2162
self._lock_convert_button = False
2164
def display_progress(self, remaining):
2165
self.progressbar.set_text(_("Converting file %d of %d (%s)") % ( self.converter.tasks_current+1, self.converter.tasks_number, remaining ))
2167
def set_progress(self, done_so_far, total, current_file=""):
2168
if (total==0) or (done_so_far==0):
2169
self.progressbar.set_text(" ")
2170
self.progressbar.set_fraction(0.0)
2171
self.progressbar.pulse()
2173
if time.time() < self.progress_time + 0.10:
2174
# ten updates per second should be enough
2176
self.progress_time = time.time()
2178
self.set_status(_("Converting"))
2180
self.progressfile.set_markup("<i><small>%s</small></i>" % current_file)
2181
fraction = float(done_so_far) / total
2183
self.progressbar.set_fraction( min(fraction, 1.0) )
2184
t = time.time() - self.converter.run_start_time - self.converter.paused_time
2187
# wait a bit not to display crap
2188
self.progressbar.pulse()
2191
r = (t / fraction - t)
2195
remaining = _("%d:%02d left") % (m,s)
2196
self.display_progress(remaining)
2198
def set_status(self, text=None):
2201
self.status.set_markup(text)
2204
def gui_main(input_files):
2205
gnome.init(NAME, VERSION)
2206
glade = gtk.glade.XML(GLADE)
2207
win = SoundConverterWindow(glade)
2209
error = ErrorDialog(glade)
2211
gobject.idle_add(win.filelist.add_uris, input_files)
2213
#gtk.threads_enter()
2215
#gtk.threads_leave()
2217
def cli_tags_main(input_files):
2219
error = ErrorPrinter()
2220
for input_file in input_files:
2221
if not get_option("quiet"):
2222
print input_file.get_uri()
2223
t = TagReader(input_file)
2228
if not get_option("quiet"):
2229
keys = input_file.keys()
2232
print " %s: %s" % (key, input_file[key])
2238
self.current_text = ""
2240
def show(self, new_text):
2241
if new_text != self.current_text:
2243
sys.stdout.write(new_text)
2245
self.current_text = new_text
2248
sys.stdout.write("\b \b" * len(self.current_text))
2252
def cli_convert_main(input_files):
2254
error = ErrorPrinter()
2256
output_type = get_option("cli-output-type")
2257
output_suffix = get_option("cli-output-suffix")
2259
generator = TargetNameGenerator()
2260
generator.set_target_suffix(output_suffix)
2262
progress = CliProgress()
2265
for input_file in input_files:
2266
input_file = SoundFile(input_file)
2267
output_name = generator.get_target_name(input_file)
2268
c = Converter(input_file, output_name, output_type)
2273
previous_filename = None
2275
while queue.is_running():
2276
t = queue.get_current_task()
2277
if not get_option("quiet"):
2278
if previous_filename != t.sound_file.get_filename_for_display():
2279
if previous_filename:
2280
print _("%s: OK") % previous_filename
2281
previous_filename = t.sound_file.get_filename_for_display()
2284
if t.get_duration():
2285
percent = "%.1f %%" % ( 100.0* (t.get_position() / t.get_duration() ))
2287
percent = "/-\|" [int(time.time()) % 4]
2288
progress.show("%s: %s" % (t.sound_file.get_filename_for_display()[-65:], percent ))
2291
if not get_option("quiet"):
2299
"cli-output-type": "audio/x-vorbis",
2300
"cli-output-suffix": ".ogg",
2304
def set_option(key, value):
2305
assert key in settings
2306
settings[key] = value
2309
def get_option(key):
2310
assert key in settings
2311
return settings[key]
2314
def print_help(*args):
2315
print _("Usage: %s [options] [soundfile ...]") % sys.argv[0]
2316
for short_arg, long_arg, func, doc in options:
2318
if short_arg[-1] == ":":
2319
print " -%s arg, --%sarg" % (short_arg[:1], long_arg)
2321
print " -%s, --%s" % (short_arg[:1], long_arg)
2322
for line in textwrap.wrap(doc):
2329
("h", "help", print_help,
2330
_("Print out a usage summary.")),
2332
("b", "batch", lambda optarg: set_option("mode", "batch"),
2333
_("Convert in batch mode, from command line, without a graphical user\n interface. You can use this from, say, shell scripts.")),
2335
("m:", "mime-type=", lambda optarg: set_option("cli-output-type", optarg),
2336
_("Set the output MIME type for batch mode. The default is\n %s . Note that you probably want to set\n the output suffix as well.") % get_option("cli-output-type")),
2338
("q", "quiet", lambda optarg: set_option("quiet", True),
2339
_("Be quiet. Don't write normal output, only errors.")),
2341
("d", "debug", lambda optarg: set_option("debug", True),
2342
_("Print additionnal debug information")),
2344
("s:", "suffix=", lambda optarg: set_option("cli-output-suffix", optarg),
2345
_("Set the output filename suffix for batch mode. The default is \n %s . Note that the suffix does not affect\n the output MIME type.") % get_option("cli-output-suffix")),
2347
("t", "tags", lambda optarg: set_option("mode", "tags"),
2348
_("Show tags for input files instead of converting them. This indicates \n command line batch mode and disables the graphical user interface.")),
2354
shortopts = "".join(map(lambda opt: opt[0], options))
2355
longopts = map(lambda opt: opt[1], options)
2357
opts, args = getopt.getopt(sys.argv[1:], shortopts, longopts)
2358
for opt, optarg in opts:
2359
for tuple in options:
2360
short = "-" + tuple[0][:1]
2361
long = "--" + tuple[1]
2362
if long.endswith("="):
2364
if opt in [short, long]:
2369
for key in settings:
2370
print key, settings[key]
2373
args = map(filename_to_uri, args)
2375
if get_option("mode") == "gui":
2377
elif get_option("mode") == "tags":
2380
cli_convert_main(args)
2383
if __name__ == "__main__":