#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# SoundConverter - GNOME application for converting between audio formats.
# Copyright 2004 Lars Wirzenius
# Copyright 2005-2012 Gautier Portet
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 3 of the License.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
import os
from os.path import basename, dirname
import time
import sys
import gtk
import gobject
import gnome
import gnomevfs
import urllib
from gettext import gettext as _
from gconfstore import GConfStore
from fileoperations import filename_to_uri, beautify_uri
from fileoperations import unquote_filename, vfs_walk
from fileoperations import use_gnomevfs
from gstreamer import ConverterQueue
from gstreamer import available_elements, TypeFinder, TagReader
from gstreamer import audio_profiles_list, audio_profiles_dict
from soundfile import SoundFile
from settings import locale_patterns_dict, custom_patterns, filepattern, settings
from namegenerator import TargetNameGenerator
from queue import TaskQueue
from utils import log, debug
from messagearea import MessageArea
from error import show_error
# Names of columns in the file list
MODEL = [ gobject.TYPE_STRING, # visible filename
gobject.TYPE_PYOBJECT, # soundfile
gobject.TYPE_FLOAT, # progress
gobject.TYPE_STRING, # status
gobject.TYPE_STRING, # complete filename
]
COLUMNS = ['filename']
#VISIBLE_COLUMNS = ['filename']
#ALL_COLUMNS = VISIBLE_COLUMNS + ['META']
MP3_CBR, MP3_ABR, MP3_VBR = range(3)
def gtk_iteration():
while gtk.events_pending():
gtk.main_iteration(False)
def gtk_sleep(duration):
start = time.time()
while time.time() < start + duration:
time.sleep(0.010)
gtk_iteration()
class ErrorDialog:
def __init__(self, builder):
self.dialog = builder.get_object('error_dialog')
self.dialog.set_transient_for(builder.get_object('window'))
self.primary = builder.get_object('primary_error_label')
self.secondary = builder.get_object('secondary_error_label')
def show_error(self, primary, secondary):
self.primary.set_markup(primary)
self.secondary.set_markup(secondary)
try:
sys.stderr.write(_('\nError: %s\n%s\n') % (primary, secondary))
except:
pass
self.dialog.run()
self.dialog.hide()
class MsgAreaErrorDialog_:
def __init__(self, builder):
self.dialog = builder.get_object('error_frame')
self.primary = builder.get_object('label_error')
def show_error(self, primary, secondary):
try:
sys.stderr.write(_('\nError: %s\n%s\n') % (primary, secondary))
except:
pass
#self.msg_area.set_text_and_icon(gtk.STOCK_DIALOG_ERROR, primary, secondary)
#self.msg_area.show()
self.primary.set_text(primary)
self.dialog.show()
def show_exception(self, exception):
self.show('%s' % gobject.markup_escape_text(exception.primary),
exception.secondary)
class FileList:
"""List of files added by the user."""
# List of MIME types which we accept for drops.
drop_mime_types = ['text/uri-list', 'text/plain', 'STRING']
def __init__(self, window, builder):
self.window = window
self.typefinders = TaskQueue()
self.filelist = set()
self.model = apply(gtk.ListStore, MODEL)
self.widget = builder.get_object('filelist')
self.sortedmodel = gtk.TreeModelSort(self.model)
self.widget.set_model(self.sortedmodel)
self.sortedmodel.set_sort_column_id(4, gtk.SORT_ASCENDING)
self.widget.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
self.widget.drag_dest_set(gtk.DEST_DEFAULT_ALL,
map(lambda i:
(self.drop_mime_types[i], 0, i),
range(len(self.drop_mime_types))),
gtk.gdk.ACTION_COPY)
self.widget.connect('drag_data_received', self.drag_data_received)
renderer = gtk.CellRendererProgress()
column = gtk.TreeViewColumn('progress',
renderer,
value=2,
text=3,
)
self.widget.append_column(column)
self.progress_column = column
self.progress_column.set_visible(False)
renderer = gtk.CellRendererText()
import pango
renderer.set_property('ellipsize', pango.ELLIPSIZE_MIDDLE)
column = gtk.TreeViewColumn('Filename',
renderer,
markup=0,
)
column.set_expand(True)
self.widget.append_column(column)
self.window.progressbarstatus.hide()
self.waiting_files = []
# add files to filelist in batches. Much faster, and suffisant.
gobject.timeout_add(100, self.commit_waiting_files)
self.waiting_files_last = 0
def drag_data_received(self, widget, context, x, y, selection,
mime_id, time):
widget.stop_emission('drag_data_received')
if mime_id >= 0 and mime_id < len(self.drop_mime_types):
self.add_uris([uri.strip() for uri in selection.data.split('\n')])
context.finish(True, False, time)
def get_files(self):
return [i[1] for i in self.sortedmodel]
def update_progress(self, queue):
if queue.running:
progress = queue.progress if queue.progress else 0
self.window.progressbarstatus.set_fraction(progress)
return True
return False
def found_type(self, sound_file, mime):
debug('found_type', sound_file.filename)
self.append_file(sound_file)
self.window.set_sensitive()
def add_uris(self, uris, base=None, extensions=None):
files = []
self.window.set_status(_('Scanning files...'))
base = None
for uri in uris:
if not uri:
continue
if uri.startswith('cdda:'):
show_error('Cannot read from Audio CD.',
'Use SoundJuicer Audio CD Extractor instead.')
return
try:
info = gnomevfs.get_file_info(gnomevfs.URI(uri),
gnomevfs.FILE_INFO_FOLLOW_LINKS)
except gnomevfs.NotFoundError:
log('uri not found: \'%s\'' % uri)
continue
except gnomevfs.InvalidURIError:
log('invalid uri: \'%s\'' % uri)
continue
except gnomevfs.AccessDeniedError:
log('access denied: \'%s\'' % uri)
continue
except TypeError, e:
log('add error: %s (\'%s\')' % (e, uri))
continue
except:
log('error in get_file_info: %s' % (uri))
continue
if info.type == gnomevfs.FILE_TYPE_DIRECTORY:
log('walking: \'%s\'' % uri)
if len(uris) == 1:
# if only one folder is passed to the function,
# use its parent as base path.
base = os.path.dirname(uri)
filelist = vfs_walk(gnomevfs.URI(uri))
accepted = []
if extensions:
for f in filelist:
for extension in extensions:
if f.lower().endswith(extension):
accepted.append(f)
filelist = accepted
files.extend(filelist)
else:
files.append(uri)
files = [f for f in files if not f.endswith('~SC~')]
if not base:
base = os.path.commonprefix(files)
if base and not base.endswith('/'):
# we want a common folder
base = base[0:base.rfind('/')]
base += '/'
else:
base += '/'
for f in files:
sound_file = SoundFile(f, base)
if sound_file.uri in self.filelist:
log('file already present: \'%s\'' % sound_file.uri)
continue
typefinder = TypeFinder(sound_file)
typefinder.set_found_type_hook(self.found_type)
self.typefinders.add_task(typefinder)
for i in self.model:
i[0] = self.format_cell(i[1])
if files and not self.typefinders.running:
self.window.progressbarstatus.show()
self.typefinders.queue_ended = self.typefinder_queue_ended
self.typefinders.start()
gobject.timeout_add(100, self.update_progress, self.typefinders)
else:
self.window.set_status()
def typefinder_queue_ended(self):
if not self.waiting_files:
self.window.set_status()
self.window.progressbarstatus.hide()
def abort(self):
self.typefinders.abort()
def format_cell(self, sound_file):
return '%s' % gobject.markup_escape_text(unquote_filename(
sound_file.filename))
def set_row_progress(self, number, progress=None, text=None):
self.progress_column.set_visible(True)
if progress is not None:
if self.model[number][2] == 1.0:
return # already...
self.model[number][2] = progress * 100.0
if text is not None:
self.model[number][3] = text
def hide_row_progress(self):
self.progress_column.set_visible(False)
def append_file(self, sound_file):
self.waiting_files.append(sound_file)
def commit_waiting_files(self):
if self.waiting_files_last != len(self.waiting_files):
# still adding files
self.waiting_files_last = len(self.waiting_files)
return True
if self.waiting_files:
self.window.set_status(_('Adding files...'))
save = self.widget.get_model()
self.widget.set_model(None)
n = 0.0
next = time.time()
while self.waiting_files:
self._append_file(self.waiting_files.pop())
n += 1
if time.time() > next:
# keep UI responsive
gtk_iteration()
self.window.progressbarstatus.set_fraction(n/self.waiting_files_last)
next = time.time() + 0.01
self.widget.set_model(save)
self.window.set_status()
self.window.progressbarstatus.hide()
return True
def _append_file(self, sound_file):
self.model.append([self.format_cell(sound_file), sound_file, 0.0, '',
sound_file.uri])
self.filelist.add(sound_file.uri)
sound_file.filelist_row = len(self.model) - 1
def remove(self, iter):
uri = self.model.get(iter, 1)[0].uri
self.filelist.remove(uri)
self.model.remove(iter)
def is_nonempty(self):
try:
self.model.get_iter((0,))
except ValueError:
return False
return True
class GladeWindow(object):
callbacks = {}
builder = None
def __init__(self, builder):
'''
Init GladeWindow, stores the objects's potential callbacks for later.
You have to call connect_signals() when all descendants are ready.'''
GladeWindow.builder = builder
GladeWindow.callbacks.update(dict([[x, getattr(self, x)]
for x in dir(self) if x.startswith('on_')]))
def __getattr__(self, attribute):
'''Allow direct use of window widget.'''
widget = GladeWindow.builder.get_object(attribute)
if widget is None:
raise AttributeError('Widget \'%s\' not found' % attribute)
self.__dict__[attribute] = widget # cache result
return widget
@staticmethod
def connect_signals():
'''Connect all GladeWindow objects to theirs respective signals'''
GladeWindow.builder.connect_signals(GladeWindow.callbacks)
class PreferencesDialog(GladeWindow, GConfStore):
basename_patterns = [
('%(.inputname)s', _('Same as input, but replacing the suffix')),
('%(.inputname)s%(.ext)s',
_('Same as input, but with an additional suffix')),
('%(track-number)02d-%(title)s', _('Track number - title')),
('%(title)s', _('Track title')),
('%(artist)s-%(title)s', _('Artist - title')),
('Custom', _('Custom filename pattern')),
]
subfolder_patterns = [
('%(artist)s/%(album)s', _('artist/album')),
('%(artist)s-%(album)s', _('artist-album')),
('%(artist)s - %(album)s', _('artist - album')),
]
defaults = {
'same-folder-as-input': 1,
'selected-folder': os.path.expanduser('~'),
'create-subfolders': 0,
'subfolder-pattern-index': 0,
'name-pattern-index': 0,
'custom-filename-pattern': '{Track} - {Title}',
'replace-messy-chars': 0,
'output-mime-type': 'audio/x-vorbis',
'output-suffix': '.ogg',
'vorbis-quality': 0.6,
'vorbis-oga-extension': 0,
'mp3-mode': 'vbr',
'mp3-cbr-quality': 192,
'mp3-abr-quality': 192,
'mp3-vbr-quality': 3,
'aac-quality': 192,
'opus-bitrate': 96,
'flac-compression': 8,
'wav-sample-width': 16,
'delete-original': 0,
'output-resample': 0,
'resample-rate': 48000,
'flac-speed': 0, # TODO used ?
'force-mono': 0,
'last-used-folder': None,
'audio-profile': None,
'limit-jobs': 0,
'number-of-jobs': 1,
}
sensitive_names = ['vorbis_quality', 'choose_folder', 'create_subfolders',
'subfolder_pattern', 'jobs_spinbutton', 'resample_hbox',
'force_mono']
def __init__(self, builder, parent):
GladeWindow.__init__(self, builder)
GConfStore.__init__(self, '/apps/SoundConverter', self.defaults)
self.dialog = builder.get_object('prefsdialog')
self.dialog.set_transient_for(parent)
self.example = builder.get_object('example_filename')
self.force_mono = builder.get_object('force_mono')
self.target_bitrate = None
self.convert_setting_from_old_version()
self.sensitive_widgets = {}
for name in self.sensitive_names:
self.sensitive_widgets[name] = builder.get_object(name)
assert self.sensitive_widgets[name] is not None
self.set_widget_initial_values(builder)
self.set_sensitive()
tip = [_('Available patterns:')]
for k in sorted(locale_patterns_dict.values()):
tip.append(k)
self.custom_filename.set_tooltip_text('\n'.join(tip))
#self.resample_rate.connect('changed', self._on_resample_rate_changed)
def convert_setting_from_old_version(self):
""" try to convert previous settings"""
# vorbis quality was once stored as an int enum
try:
self.get_float('vorbis-quality')
except gobject.GError:
log('deleting old settings...')
[self.gconf.unset(self.path(k)) for k in self.defaults.keys()]
self.gconf.clear_cache()
def set_widget_initial_values(self, builder):
self.quality_tabs.set_show_tabs(False)
if self.get_int('same-folder-as-input'):
w = self.same_folder_as_input
else:
w = self.into_selected_folder
w.set_active(True)
uri = filename_to_uri(self.get_string('selected-folder'))
self.target_folder_chooser.set_uri(uri)
self.update_selected_folder()
w = self.create_subfolders
w.set_active(self.get_int('create-subfolders'))
w = self.subfolder_pattern
active = self.get_int('subfolder-pattern-index')
model = w.get_model()
model.clear()
for pattern, desc in self.subfolder_patterns:
i = model.append()
model.set(i, 0, desc)
w.set_active(active)
if self.get_int('replace-messy-chars'):
w = self.replace_messy_chars
w.set_active(True)
if self.get_int('delete-original'):
self.delete_original.set_active(True)
mime_type = self.get_string('output-mime-type')
widgets = ( ('audio/x-vorbis', 'vorbisenc'),
('audio/mpeg' , 'lame'),
('audio/x-flac' , 'flacenc'),
('audio/x-wav' , 'wavenc'),
('audio/x-m4a' , 'faac'),
('audio/ogg; codecs=opus' , 'opusenc'),
('gst-profile' , None),
) # must be in same order in output_mime_type
# desactivate output if encoder plugin is not present
widget = self.output_mime_type
model = widget.get_model()
assert len(model) == len(widgets), 'model:%d widgets:%d' % (len(model),
len(widgets))
if not self.gstprofile.get_model().get_n_columns():
self.gstprofile.set_model(gtk.ListStore(str))
cell = gtk.CellRendererText()
self.gstprofile.pack_start(cell)
self.gstprofile.add_attribute(cell,'text',0)
self.gstprofile.set_active(0)
# check if we can found the stored audio profile
found_profile = False
stored_profile = self.get_string('audio-profile')
for i, profile in enumerate(audio_profiles_list):
description, extension, pipeline = profile
self.gstprofile.get_model().append(['%s (.%s)' % (description, extension)])
if description == stored_profile:
self.gstprofile.set_active(i)
found_profile = True
if not found_profile and stored_profile:
# reset default output
log('Cannot find audio profile "%s", resetting to default output.'
% stored_profile)
self.set_string('audio-profile', '')
self.gstprofile.set_active(0)
mime_type = self.defaults['output-mime-type']
self.present_mime_types = []
i = 0
model = self.output_mime_type.get_model()
for b in widgets:
mime, encoder_name = b
# valid encoder?
encoder_present = encoder_name and encoder_name in available_elements
# valid profile?
profile_present = mime == 'gst-profile' and audio_profiles_list
if encoder_present or profile_present:
# add to supported outputs
self.present_mime_types.append(mime)
i += 1
else:
# remove it.
del model[i]
if mime_type == mime:
mime_type = self.defaults['output-mime-type']
for i, mime in enumerate(self.present_mime_types):
if mime_type == mime:
widget.set_active(i)
self.change_mime_type(mime_type)
# display information about mp3 encoding
if 'lame' not in available_elements:
w = self.lame_absent
w.show()
w = self.vorbis_quality
quality = self.get_float('vorbis-quality')
quality_setting = {0: 0, 0.2: 1, 0.4: 2, 0.6: 3, 0.8: 4, 1.0: 5}
w.set_active(-1)
for k, v in quality_setting.iteritems():
if abs(quality - k) < 0.01:
self.vorbis_quality.set_active(v)
if self.get_int('vorbis-oga-extension'):
self.vorbis_oga_extension.set_active(True)
w = self.aac_quality
quality = self.get_int('aac-quality')
quality_setting = {64: 0, 96: 1, 128: 2, 192: 3, 256: 4, 320: 5}
w.set_active(quality_setting.get(quality, -1))
w = self.opus_quality
quality = self.get_int('opus-bitrate')
quality_setting = {48: 0, 64: 1, 96: 2, 128: 3, 160: 4, 192: 5}
w.set_active(quality_setting.get(quality, -1))
w = self.flac_compression
quality = self.get_int('flac-compression')
quality_setting = {0: 0, 5: 1, 8: 2}
w.set_active(quality_setting.get(quality, -1))
w = self.wav_sample_width
quality = self.get_int('wav-sample-width')
quality_setting = {8: 0, 16: 1, 32: 2}
w.set_active(quality_setting.get(quality, -1))
self.mp3_quality = self.mp3_quality
self.mp3_mode = self.mp3_mode
mode = self.get_string('mp3-mode')
self.change_mp3_mode(mode)
w = self.basename_pattern
active = self.get_int('name-pattern-index')
model = w.get_model()
model.clear()
for pattern, desc in self.basename_patterns:
iter = model.append()
model.set(iter, 0, desc)
w.set_active(active)
self.custom_filename.set_text(self.get_string(
'custom-filename-pattern'))
if self.basename_pattern.get_active() == len(self.basename_patterns)-1:
self.custom_filename_box.set_sensitive(True)
else:
self.custom_filename_box.set_sensitive(False)
self.resample_toggle.set_active(self.get_int('output-resample'))
cell = gtk.CellRendererText()
self.resample_rate.pack_start(cell, True)
self.resample_rate.add_attribute(cell, 'text', 0)
rates = [8000, 11025, 22050, 44100, 48000, 96000]
rate = self.get_int('resample-rate')
try:
idx = rates.index(rate)
except ValueError:
idx = -1
self.resample_rate.set_active(idx)
self.force_mono.set_active(self.get_int('force-mono'))
self.jobs.set_active(self.get_int('limit-jobs'))
self.jobs_spinbutton.set_value(self.get_int('number-of-jobs'))
self.update_jobs()
self.update_example()
def update_selected_folder(self):
self.into_selected_folder.set_use_underline(False)
self.into_selected_folder.set_label(_('Into folder %s') %
beautify_uri(self.get_string('selected-folder')))
def get_bitrate_from_settings(self):
bitrate = 0
aprox = True
mode = self.get_string('mp3-mode')
mime_type = self.get_string('output-mime-type')
if mime_type == 'audio/x-vorbis':
quality = self.get_float('vorbis-quality')*10
quality = int(quality)
bitrates = (64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 500)
bitrate = bitrates[quality]
elif mime_type == 'audio/x-m4a':
bitrate = self.get_int('aac-quality')
elif mime_type == 'audio/ogg; codecs=opus':
bitrate = self.get_int('opus-bitrate')
elif mime_type == 'audio/mpeg':
quality = {
'cbr': 'mp3-cbr-quality',
'abr': 'mp3-abr-quality',
'vbr': 'mp3-vbr-quality'
}
bitrate = self.get_int(quality[mode])
if mode == 'vbr':
# hum, not really, but who cares? :)
bitrates = (320, 256, 224, 192, 160, 128, 112, 96, 80, 64)
bitrate = bitrates[bitrate]
if mode == 'cbr':
aprox = False
if bitrate:
if aprox:
return '~%d kbps' % bitrate
else:
return '%d kbps' % bitrate
else:
return 'N/A'
def update_example(self):
sound_file = SoundFile('foo/bar.flac')
sound_file.tags.update({'track-number': 1, 'track-count': 99})
sound_file.tags.update({'disc-number': 2, 'disc-count': 9})
sound_file.tags.update(locale_patterns_dict)
s = gobject.markup_escape_text(beautify_uri(
self.generate_filename(sound_file, for_display=True)))
p = 0
replaces = []
while 1:
b = s.find('{', p)
if b == -1:
break
e = s.find('}', b)
tag = s[b:e+1]
if tag.lower() in [
v.lower() for v in locale_patterns_dict.values()]:
k = tag
l = k.replace('{', '{')
l = l.replace('}', '}')
replaces.append([k, l])
else:
k = tag
l = k.replace('{', '{')
l = l.replace('}', '}')
replaces.append([k, l])
p = b+1
for k, l in replaces:
s = s.replace(k, l)
self.example.set_markup(s)
markup = '%s' % (_('Target bitrate: %s') %
self.get_bitrate_from_settings())
self.aprox_bitrate.set_markup(markup)
def get_output_suffix(self):
self.gconf.clear_cache()
output_type = self.get_string('output-mime-type')
profile = self.get_string('audio-profile')
profile_ext = audio_profiles_dict[profile][1] if profile else ''
output_suffix = {
'audio/x-vorbis': '.ogg',
'audio/x-flac': '.flac',
'audio/x-wav': '.wav',
'audio/mpeg': '.mp3',
'audio/x-m4a': '.m4a',
'audio/ogg; codecs=opus': '.opus',
'gst-profile': '.' + profile_ext,
}.get(output_type, '.?')
if output_suffix == '.ogg' and self.get_int('vorbis-oga-extension'):
output_suffix = '.oga'
return output_suffix
def generate_filename(self, sound_file, for_display=False):
generator = TargetNameGenerator()
generator.suffix = self.get_output_suffix()
if not self.get_int('same-folder-as-input'):
folder = self.get_string('selected-folder')
folder = filename_to_uri(folder)
generator.folder = folder
if self.get_int('create-subfolders'):
generator.subfolders = self.get_subfolder_pattern()
generator.basename = self.get_basename_pattern()
if for_display:
generator.replace_messy_chars = False
return unquote_filename(generator.get_target_name(sound_file))
else:
generator.replace_messy_chars = self.get_int('replace-messy-chars')
return generator.get_target_name(sound_file)
def generate_temp_filename(self, soundfile):
folder = dirname(soundfile.uri)
if not self.get_int('same-folder-as-input'):
folder = self.get_string('selected-folder')
folder = filename_to_uri(folder)
return folder + '/' + basename(soundfile.filename)
def process_custom_pattern(self, pattern):
for k in custom_patterns:
pattern = pattern.replace(k, custom_patterns[k])
return pattern
def set_sensitive(self):
for widget in self.sensitive_widgets.values():
widget.set_sensitive(False)
x = self.get_int('same-folder-as-input')
for name in ['choose_folder', 'create_subfolders',
'subfolder_pattern']:
self.sensitive_widgets[name].set_sensitive(not x)
self.sensitive_widgets['vorbis_quality'].set_sensitive(
self.get_string('output-mime-type') == 'audio/x-vorbis')
self.sensitive_widgets['jobs_spinbutton'].set_sensitive(
self.get_int('limit-jobs'))
if self.get_string('output-mime-type') == 'gst-profile':
self.sensitive_widgets['resample_hbox'].set_sensitive(False)
self.sensitive_widgets['force_mono'].set_sensitive(False)
else:
self.sensitive_widgets['resample_hbox'].set_sensitive(True)
self.sensitive_widgets['force_mono'].set_sensitive(True)
def run(self):
self.dialog.run()
self.dialog.hide()
def on_delete_original_toggled(self, button):
if button.get_active():
self.set_int('delete-original', 1)
else:
self.set_int('delete-original', 0)
def on_same_folder_as_input_toggled(self, button):
if button.get_active():
self.set_int('same-folder-as-input', 1)
self.set_sensitive()
self.update_example()
def on_into_selected_folder_toggled(self, button):
if button.get_active():
self.set_int('same-folder-as-input', 0)
self.set_sensitive()
self.update_example()
def on_choose_folder_clicked(self, button):
ret = self.target_folder_chooser.run()
folder = self.target_folder_chooser.get_uri()
self.target_folder_chooser.hide()
if ret == gtk.RESPONSE_OK:
if folder:
self.set_string('selected-folder', urllib.unquote(folder))
self.update_selected_folder()
self.update_example()
def on_create_subfolders_toggled(self, button):
if button.get_active():
self.set_int('create-subfolders', 1)
else:
self.set_int('create-subfolders', 0)
self.update_example()
def on_subfolder_pattern_changed(self, combobox):
self.set_int('subfolder-pattern-index', combobox.get_active())
self.update_example()
def get_subfolder_pattern(self):
index = self.get_int('subfolder-pattern-index')
if index < 0 or index >= len(self.subfolder_patterns):
index = 0
return self.subfolder_patterns[index][0]
def on_basename_pattern_changed(self, combobox):
self.set_int('name-pattern-index', combobox.get_active())
if combobox.get_active() == len(self.basename_patterns)-1:
self.custom_filename_box.set_sensitive(True)
else:
self.custom_filename_box.set_sensitive(False)
self.update_example()
def get_basename_pattern(self):
index = self.get_int('name-pattern-index')
if index < 0 or index >= len(self.basename_patterns):
index = 0
if self.basename_pattern.get_active() == len(self.basename_patterns)-1:
return self.process_custom_pattern(self.custom_filename.get_text())
else:
return self.basename_patterns[index][0]
def on_custom_filename_changed(self, entry):
self.set_string('custom-filename-pattern', entry.get_text())
self.update_example()
def on_replace_messy_chars_toggled(self, button):
if button.get_active():
self.set_int('replace-messy-chars', 1)
else:
self.set_int('replace-messy-chars', 0)
self.update_example()
def change_mime_type(self, mime_type):
self.set_string('output-mime-type', mime_type)
self.set_sensitive()
self.update_example()
tabs = {
'audio/x-vorbis': 0,
'audio/mpeg': 1,
'audio/x-flac': 2,
'audio/x-wav': 3,
'audio/x-m4a': 4,
'audio/ogg; codecs=opus': 5,
'gst-profile': 6,
}
self.quality_tabs.set_current_page(tabs[mime_type])
def on_output_mime_type_changed(self, combo):
self.change_mime_type(
self.present_mime_types[combo.get_active()]
)
def on_output_mime_type_ogg_vorbis_toggled(self, button):
if button.get_active():
self.change_mime_type('audio/x-vorbis')
def on_output_mime_type_flac_toggled(self, button):
if button.get_active():
self.change_mime_type('audio/x-flac')
def on_output_mime_type_wav_toggled(self, button):
if button.get_active():
self.change_mime_type('audio/x-wav')
def on_output_mime_type_mp3_toggled(self, button):
if button.get_active():
self.change_mime_type('audio/mpeg')
def on_output_mime_type_aac_toggled(self, button):
if button.get_active():
self.change_mime_type('audio/x-m4a')
def on_output_mime_type_opus_toggled(self, button):
if button.get_active():
self.change_mime_type('audio/ogg; codecs=opus')
def on_vorbis_quality_changed(self, combobox):
if combobox.get_active() == -1:
return # just de-selectionning
quality = (0, 0.2, 0.4, 0.6, 0.8, 1.0)
fquality = quality[combobox.get_active()]
self.set_float('vorbis-quality', fquality)
self.hscale_vorbis_quality.set_value(fquality*10)
self.update_example()
def on_hscale_vorbis_quality_value_changed(self, hscale):
fquality = hscale.get_value()
if abs(self.get_float('vorbis-quality') - fquality/10.0) < 0.001:
return # already at right value
self.set_float('vorbis-quality', fquality/10.0)
self.vorbis_quality.set_active(-1)
self.update_example()
def on_vorbis_oga_extension_toggled(self, toggle):
self.set_int('vorbis-oga-extension', toggle.get_active())
self.update_example()
def on_aac_quality_changed(self, combobox):
quality = (64, 96, 128, 192, 256, 320)
self.set_int('aac-quality', quality[combobox.get_active()])
self.update_example()
def on_opus_quality_changed(self, combobox):
quality = (48, 64, 96, 128, 160, 192)
self.set_int('opus-bitrate', quality[combobox.get_active()])
self.update_example()
def on_wav_sample_width_changed(self, combobox):
quality = (8, 16, 32)
self.set_int('wav-sample-width', quality[combobox.get_active()])
self.update_example()
def on_flac_compression_changed(self, combobox):
quality = (0, 5, 8)
self.set_int('flac-compression', quality[combobox.get_active()])
self.update_example()
def on_gstprofile_changed(self, combobox):
profile = audio_profiles_list[combobox.get_active()]
description, extension, pipeline = profile
self.set_string('audio-profile', description)
self.update_example()
def on_force_mono_toggle(self, button):
if button.get_active():
self.set_int('force-mono', 1)
else:
self.set_int('force-mono', 0)
self.update_example()
def change_mp3_mode(self, mode):
keys = {'cbr': 0, 'abr': 1, 'vbr': 2}
self.mp3_mode.set_active(keys[mode])
keys = {
'cbr': 'mp3-cbr-quality',
'abr': 'mp3-abr-quality',
'vbr': 'mp3-vbr-quality',
}
quality = self.get_int(keys[mode])
quality_to_preset = {
'cbr': {64: 0, 96: 1, 128: 2, 192: 3, 256: 4, 320: 5},
'abr': {64: 0, 96: 1, 128: 2, 192: 3, 256: 4, 320: 5},
'vbr': {9: 0, 7: 1, 5: 2, 3: 3, 1: 4, 0: 5}, # inverted !
}
range_ = {
'cbr': 14,
'abr': 14,
'vbr': 10,
}
self.hscale_mp3.set_range(0, range_[mode])
if quality in quality_to_preset[mode]:
self.mp3_quality.set_active(quality_to_preset[mode][quality])
self.update_example()
def on_mp3_mode_changed(self, combobox):
mode = ('cbr', 'abr', 'vbr')[combobox.get_active()]
self.set_string('mp3-mode', mode)
self.change_mp3_mode(mode)
def on_mp3_quality_changed(self, combobox):
keys = {
'cbr': 'mp3-cbr-quality',
'abr': 'mp3-abr-quality',
'vbr': 'mp3-vbr-quality'
}
quality = {
'cbr': (64, 96, 128, 192, 256, 320),
'abr': (64, 96, 128, 192, 256, 320),
'vbr': (9, 7, 5, 3, 1, 0),
}
mode = self.get_string('mp3-mode')
self.set_int(keys[mode], quality[mode][combobox.get_active()])
self.update_example()
def on_hscale_mp3_value_changed(self, widget):
mode = self.get_string('mp3-mode')
keys = {
'cbr': 'mp3-cbr-quality',
'abr': 'mp3-abr-quality',
'vbr': 'mp3-vbr-quality'
}
quality = {
'cbr': (32, 40, 48, 56, 64, 80, 96, 112,
128, 160, 192, 224, 256, 320),
'abr': (32, 40, 48, 56, 64, 80, 96, 112,
128, 160, 192, 224, 256, 320),
'vbr': (9, 8, 7, 6, 5, 4, 3, 2, 1, 0),
}
self.set_int(keys[mode], quality[mode][int(widget.get_value())])
self.mp3_quality.set_active(-1)
self.update_example()
def on_resample_rate_changed(self, combobox):
model = combobox.get_model()
iter = combobox.get_active_iter()
changeto = model.get_value(iter, 0)
self.set_int('resample-rate', int(changeto))
def on_resample_toggle(self, rstoggle):
self.set_int('output-resample', rstoggle.get_active())
self.resample_rate.set_sensitive(rstoggle.get_active())
def on_jobs_toggled(self, jtoggle):
self.set_int('limit-jobs', jtoggle.get_active())
self.jobs_spinbutton.set_sensitive(jtoggle.get_active())
self.update_jobs()
def on_jobs_spinbutton_value_changed(self, jspinbutton):
self.set_int('number-of-jobs', int(jspinbutton.get_value()))
self.update_jobs()
def update_jobs(self):
if self.get_int('limit-jobs'):
settings['jobs'] = self.get_int('number-of-jobs')
else:
settings['jobs'] = settings['max-jobs']
self.set_sensitive()
class CustomFileChooser:
"""
Custom file chooser.\n
"""
def __init__(self, builder, parent):
"""
Constructor
Load glade object, create a combobox
"""
self.dlg = builder.get_object('custom_file_chooser')
self.dlg.set_title(_('Open a file'))
self.dlg.set_transient_for(parent)
# setup
self.fcw = builder.get_object('filechooserwidget')
self.fcw.set_local_only(not use_gnomevfs)
self.fcw.set_select_multiple(True)
self.pattern = []
# Create combobox model
self.combo = builder.get_object('filtercombo')
self.combo.connect('changed', self.on_combo_changed)
self.store = gtk.ListStore(str)
self.combo.set_model(self.store)
combo_rend = gtk.CellRendererText()
self.combo.pack_start(combo_rend, True)
self.combo.add_attribute(combo_rend, 'text', 0)
# TODO: get all (gstreamer) knew files
for name, pattern in filepattern:
self.add_pattern(name, pattern)
self.combo.set_active(0)
def add_pattern(self, name, pat):
"""
Add a new pattern to the combobox.
@param name: The pattern name.
@type name: string
@param pat: the pattern
@type pat: string
"""
self.pattern.append(pat)
self.store.append(['%s (%s)' % (name, pat)])
def filter_cb(self, info, pattern):
filename = info[2]
return filename.lower().endswith(pattern[1:])
def on_combo_changed(self, w):
"""
Callback for combobox 'changed' signal\n
Set a new filter for the filechooserwidget
"""
filter = gtk.FileFilter()
active = self.combo.get_active()
if active:
filter.add_custom(gtk.FILE_FILTER_DISPLAY_NAME, self.filter_cb,
self.pattern[self.combo.get_active()])
else:
filter.add_pattern('*.*')
self.fcw.set_filter(filter)
def __getattr__(self, attr):
"""
Redirect all missing attributes/methods
to dialog.
"""
try:
# defaut to dialog attributes
return getattr(self.dlg, attr)
except AttributeError:
# fail back to inner file chooser widget
return getattr(self.fcw, attr)
_old_progress = 0
_old_total = 0
class SoundConverterWindow(GladeWindow):
"""Main application class."""
sensitive_names = ['remove', 'clearlist',
'toolbutton_clearlist', 'convert_button']
unsensitive_when_converting = ['remove', 'clearlist', 'prefs_button',
'toolbutton_addfile', 'toolbutton_addfolder', 'convert_button',
'toolbutton_clearlist', 'filelist', 'menubar']
def __init__(self, builder):
self.paused_time = 0
GladeWindow.__init__(self, builder)
self.widget = builder.get_object('window')
self.prefs = PreferencesDialog(builder, self.widget)
self.addchooser = CustomFileChooser(builder, self.widget)
GladeWindow.connect_signals()
self.filelist = FileList(self, builder)
self.filelist_selection = self.filelist.widget.get_selection()
self.filelist_selection.connect('changed', self.selection_changed)
self.existsdialog = builder.get_object('existsdialog')
self.existsdialog.message = builder.get_object('exists_message')
self.existsdialog.apply_to_all = builder.get_object('apply_to_all')
self.addfolderchooser = gtk.FileChooserDialog(_('Add Folder...'),
self.widget, gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN,
gtk.RESPONSE_OK))
self.addfolderchooser.set_select_multiple(True)
self.addfolderchooser.set_local_only(not use_gnomevfs)
self.combo = gtk.ComboBox()
self.store = gtk.ListStore(str)
self.combo.set_model(self.store)
combo_rend = gtk.CellRendererText()
self.combo.pack_start(combo_rend, True)
self.combo.add_attribute(combo_rend, 'text', 0)
# TODO: get all (gstreamer) knew files
for files in filepattern:
self.store.append(['%s (%s)' % (files[0], files[1])])
self.combo.set_active(0)
self.addfolderchooser.set_extra_widget(self.combo)
self.aboutdialog.set_property('name', NAME)
self.aboutdialog.set_property('version', VERSION)
self.aboutdialog.set_transient_for(self.widget)
self.converter = ConverterQueue(self)
self.sensitive_widgets = {}
for name in self.sensitive_names:
self.sensitive_widgets[name] = builder.get_object(name)
for name in self.unsensitive_when_converting:
self.sensitive_widgets[name] = builder.get_object(name)
self.set_sensitive()
self.set_status()
#msg = _('The output file %s\n exists already.\n '\
# 'Do you want to skip the file, overwrite it or'\
# ' cancel the conversion?\n') % '/foo/bar/baz'
vbox = self.vbox_status
self.msg_area = msg_area = MessageArea()
#msg_area.add_button('_Overwrite', 1)
#msg_area.add_button('_Skip', 2)
msg_area.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CLOSE)
#checkbox = gtk.CheckButton('Apply to _all queue')
#checkbox.show()
#msg_area.set_text_and_icon(gtk.STOCK_DIALOG_ERROR, 'Access Denied', msg, checkbox)
#msg_area.connect("response", self.OnMessageAreaReponse, msg_area)
#msg_area.connect("close", self.OnMessageAreaClose, msg_area)
vbox.pack_start(msg_area, False, False)
#msg_area.show()
# This bit of code constructs a list of methods for binding to Gtk+
# signals. This way, we don't have to maintain a list manually,
# saving editing effort. It's enough to add a method to the suitable
# class and give the same name in the .glade file.
def __getattr__(self, attribute):
"""Allow direct use of window widget."""
widget = self.builder.get_object(attribute)
if widget is None:
raise AttributeError('Widget \'%s\' not found' % attribute)
self.__dict__[attribute] = widget # cache result
return widget
def close(self, *args):
debug('closing...')
self.filelist.abort()
self.converter.abort()
self.widget.hide_all()
self.widget.destroy()
# wait one second...
# yes, this sucks badly, but signals can still be called by gstreamer
# so wait a bit for things to calm down, and quit.
gtk_sleep(1)
gtk.main_quit()
return True
on_window_delete_event = close
on_quit_activate = close
on_quit_button_clicked = close
def on_add_activate(self, *args):
last_folder = self.prefs.get_string('last-used-folder')
if last_folder:
self.addchooser.set_current_folder_uri(last_folder)
ret = self.addchooser.run()
folder = self.addchooser.get_current_folder_uri()
self.addchooser.hide()
if ret == gtk.RESPONSE_OK and folder:
self.filelist.add_uris(self.addchooser.get_uris())
self.prefs.set_string('last-used-folder', folder)
self.set_sensitive()
def on_addfolder_activate(self, *args):
last_folder = self.prefs.get_string('last-used-folder')
if last_folder:
self.addfolderchooser.set_current_folder_uri(last_folder)
ret = self.addfolderchooser.run()
folders = self.addfolderchooser.get_uris()
folder = self.addfolderchooser.get_current_folder_uri()
self.addfolderchooser.hide()
if ret == gtk.RESPONSE_OK:
extensions = None
if self.combo.get_active():
patterns = filepattern[self.combo.get_active()][1].split(';')
extensions = [os.path.splitext(p)[1] for p in patterns]
self.filelist.add_uris(folders, extensions=extensions)
if folder:
self.prefs.set_string('last-used-folder', folder)
self.set_sensitive()
def on_remove_activate(self, *args):
model, paths = self.filelist_selection.get_selected_rows()
while paths:
# Remove files
childpath = model.convert_path_to_child_path(paths[0])
i = self.filelist.model.get_iter(childpath)
self.filelist.remove(i)
model, paths = self.filelist_selection.get_selected_rows()
# re-assign row numbers
files = self.filelist.get_files()
for i, sound_file in enumerate(files):
sound_file.filelist_row = i
self.set_sensitive()
def on_clearlist_activate(self, *args):
self.filelist.model.clear()
self.filelist.filelist.clear()
self.set_sensitive()
self.set_status()
def on_progress(self):
if self.pulse_progress > 0: # still waiting for tags
self.set_progress(self.pulse_progress, display_time=False)
return True
if self.pulse_progress == -1: # still waiting for add
self.set_progress()
return True
if self.pulse_progress == False: # conversion ended
return False
perfile = {}
for s in self.filelist.get_files():
perfile[s] = None
running, progress = self.converter.get_progress(perfile)
if running:
self.set_progress(progress)
for sound_file, taskprogress in perfile.iteritems():
if taskprogress > 0.0:
sound_file.progress = taskprogress
self.set_file_progress(sound_file, taskprogress)
if taskprogress is None and sound_file.progress:
self.set_file_progress(sound_file, 1.0)
sound_file.progress = None
return running
def do_convert(self):
self.pulse_progress = -1
gobject.timeout_add(100, self.on_progress)
self.progressbar.set_text(_('Preparing conversion...'))
files = self.filelist.get_files()
total = len(files)
for i, sound_file in enumerate(files):
gtk_iteration()
self.pulse_progress = float(i)/total # TODO: still needed?
sound_file.progress = None
self.converter.add(sound_file)
# all was OK
self.set_status('')
self.pulse_progress = None
self.converter.start()
self.set_sensitive()
def on_convert_button_clicked(self, *args):
# reset and show progress bar
self.set_progress(0)
self.progress_frame.show()
self.status_frame.hide()
self.progress_time = time.time()
self.set_progress()
self.set_status(_('Converting'))
for soundfile in self.filelist.get_files():
self.set_file_progress(soundfile, 0.0)
# start conversion
self.do_convert()
# update ui
self.set_sensitive()
def on_button_pause_clicked(self, *args):
self.converter.toggle_pause(not self.converter.paused)
if self.converter.paused:
self.current_pause_start = time.time()
else:
self.paused_time += time.time() - self.current_pause_start
def on_button_cancel_clicked(self, *args):
self.converter.abort()
self.set_status(_('Canceled'))
self.set_sensitive()
self.conversion_ended()
def on_select_all_activate(self, *args):
self.filelist.widget.get_selection().select_all()
def on_clear_activate(self, *args):
self.filelist.widget.get_selection().unselect_all()
def on_preferences_activate(self, *args):
self.prefs.run()
on_prefs_button_clicked = on_preferences_activate
def on_about_activate(self, *args):
about = self.aboutdialog
about.set_property('name', NAME)
about.set_property('version', VERSION)
about.set_transient_for(self.widget)
#TODO: about.set_property('translator_credits', TRANSLATORS)
about.show()
def on_aboutdialog_response(self, *args):
self.aboutdialog.hide()
def selection_changed(self, *args):
self.set_sensitive()
def conversion_ended(self):
self.pulse_progress = False
self.progress_frame.hide()
self.filelist.hide_row_progress()
self.status_frame.show()
self.widget.set_sensitive(True)
try:
from gi.repository import Unity
launcher = Unity.LauncherEntry.get_for_desktop_id ("soundconverter.desktop")
launcher.set_property("progress_visible", False)
except ImportError:
pass
def set_widget_sensitive(self, name, sensitivity):
self.sensitive_widgets[name].set_sensitive(sensitivity)
def set_sensitive(self):
"""update the sensitive state of UI for the current state"""
for w in self.unsensitive_when_converting:
self.set_widget_sensitive(w, not self.converter.running)
if not self.converter.running:
self.set_widget_sensitive('remove',
self.filelist_selection.count_selected_rows() > 0)
self.set_widget_sensitive('convert_button',
self.filelist.is_nonempty())
def set_file_progress(self, sound_file, progress):
row = sound_file.filelist_row
self.filelist.set_row_progress(row, progress)
def set_progress(self, fraction=None, display_time=True):
if not fraction:
if fraction is None:
self.progressbar.pulse()
else:
self.progressbar.set_fraction(0)
self.progressbar.set_text('')
self.progressfile.set_markup('')
self.filelist.hide_row_progress()
return
if self.converter.paused:
self.progressbar.set_text(_('Paused'))
return
fraction = min(max(fraction, 0.0), 1.0)
self.progressbar.set_fraction(fraction)
if display_time:
t = time.time() - self.converter.run_start_time - \
self.paused_time
if (t < 1):
# wait a bit not to display crap
self.progressbar.pulse()
return
r = (t / fraction - t)
s = max(r % 60, 1)
m = r / 60
remaining = _('%d:%02d left') % (m, s)
self.progressbar.set_text(remaining)
self.progress_time = time.time()
def set_status(self, text=None):
if not text:
text = _('Ready')
self.statustext.set_markup(text)
gtk_iteration()
def is_active(self):
return self.widget.is_active()
NAME = VERSION = None
win = None
def gui_main(name, version, gladefile, input_files):
global NAME, VERSION
NAME, VERSION = name, version
gnome.init(name, version)
builder = gtk.Builder()
builder.set_translation_domain(name.lower())
builder.add_from_file(gladefile)
global win
win = SoundConverterWindow(builder)
import error
error.set_error_handler(ErrorDialog(builder))
#error_dialog = MsgAreaErrorDialog(builder)
#error_dialog.msg_area = win.msg_area
#error.set_error_handler(error_dialog)
gobject.idle_add(win.filelist.add_uris, input_files)
win.set_sensitive()
gtk.main()