class Progress(QDialog): file_converted_signal = pyqtSignal() refr_bars_signal = pyqtSignal(int) update_text_edit_signal = pyqtSignal(str) def __init__(self, files, tab, delete, parent, test=False): """ Keyword arguments: files -- list with dicts containing file names tab -- instanseof AudioVideoTab, ImageTab or DocumentTab indicating currently active tab delete -- boolean that shows if files must removed after conversion parent -- parent widget files: Each dict have only one key and one corresponding value. Key is a file to be converted and it's value is the name of the new file that will be converted. Example list: [{"/foo/bar.png" : "/foo/bar.bmp"}, {"/f/bar2.png" : "/f/bar2.bmp"}] """ super(Progress, self).__init__(parent) self.parent = parent self.files = files self.num_total_files = len(self.files) self.tab = tab self.delete = delete if not test: self._type = tab.name self.step = int(100 / len(files)) self.ok = 0 self.error = 0 self.running = True self.nowQL = QLabel(self.tr('In progress: ')) totalQL = QLabel(self.tr('Total:')) self.nowQPBar = QProgressBar() self.nowQPBar.setValue(0) self.totalQPBar = QProgressBar() self.totalQPBar.setValue(0) self.cancelQPB = QPushButton(self.tr('Cancel')) detailsQPB = QCommandLinkButton(self.tr('Details')) detailsQPB.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) detailsQPB.setCheckable(True) detailsQPB.setMaximumWidth(113) line = QFrame() line.setFrameShape(QFrame.HLine) line.setFrameShadow(QFrame.Sunken) self.outputQTE = QTextEdit() self.outputQTE.setReadOnly(True) self.frame = QFrame() frame_layout = utils.add_to_layout('h', self.outputQTE) self.frame.setLayout(frame_layout) self.frame.hide() hlayout = utils.add_to_layout('h', None, self.nowQL, None) hlayout2 = utils.add_to_layout('h', None, totalQL, None) hlayout3 = utils.add_to_layout('h', detailsQPB, line) hlayout4 = utils.add_to_layout('h', self.frame) hlayout5 = utils.add_to_layout('h', None, self.cancelQPB) vlayout = utils.add_to_layout( 'v', hlayout, self.nowQPBar, hlayout2, self.totalQPBar, None, hlayout3, hlayout4, hlayout5 ) self.setLayout(vlayout) detailsQPB.toggled.connect(self.resize_dialog) detailsQPB.toggled.connect(self.frame.setVisible) self.cancelQPB.clicked.connect(self.reject) self.file_converted_signal.connect(self.next_file) self.refr_bars_signal.connect(self.refresh_progress_bars) self.update_text_edit_signal.connect(self.update_text_edit) self.resize(484, 200) self.setWindowTitle('FF Multi Converter - ' + self.tr('Conversion')) if not test: self.get_data() # should be first and not in QTimer.singleShot() QTimer.singleShot(0, self.manage_conversions) def get_data(self): """Collect conversion data from parents' widgets.""" if self._type == 'AudioVideo': self.cmd = self.tab.commandQLE.text() elif self._type == 'Images': width = self.tab.widthQLE.text() self.size = '' self.mntaspect = False if width: height = self.tab.heightQLE.text() self.size = '{0}x{1}'.format(width, height) self.mntaspect = self.tab.imgaspectQChB.isChecked() self.imgcmd = self.tab.commandQLE.text() if self.tab.autocropQChB.isChecked(): self.imgcmd += ' -trim +repage' rotate = self.tab.rotateQLE.text().strip() if rotate: self.imgcmd += ' -rotate {0}'.format(rotate) if self.tab.vflipQChB.isChecked(): self.imgcmd += ' -flip' if self.tab.hflipQChB.isChecked(): self.imgcmd += ' -flop' def resize_dialog(self): """Resize dialog.""" height = 200 if self.frame.isVisible() else 366 self.setMinimumSize(484, height) self.resize(484, height) def update_text_edit(self, txt): """Append txt to the end of current self.outputQTE's text.""" current = self.outputQTE.toPlainText() self.outputQTE.setText(current+txt) self.outputQTE.moveCursor(QTextCursor.End) def refresh_progress_bars(self, now_percent): """Refresh the values of self.nowQPBar and self.totalQPBar.""" total_percent = int(((now_percent * self.step) / 100) + self.min_value) if now_percent > self.nowQPBar.value() and not (now_percent > 100): self.nowQPBar.setValue(now_percent) if (total_percent > self.totalQPBar.value() and not (total_percent > self.max_value)): self.totalQPBar.setValue(total_percent) def manage_conversions(self): """ Check whether all files have been converted. If not, it will allow convert_a_file() to convert the next file. """ if not self.running: return if not self.files: self.totalQPBar.setValue(100) if self.totalQPBar.value() >= 100: sum_files = self.ok + self.error msg = QMessageBox(self) msg.setStandardButtons(QMessageBox.Ok) msg.setWindowTitle(self.tr("Report")) msg.setText(self.tr("Converted: {0}/{1}".format(self.ok,sum_files))) msg.setModal(False) msg.show() self.cancelQPB.setText(self.tr("Close")) else: self.convert_a_file() def next_file(self): """ Update progress bars values, remove converted file from self.files and call manage_conversions() to continue the process. """ self.totalQPBar.setValue(self.max_value) self.nowQPBar.setValue(100) QApplication.processEvents() self.files.pop(0) self.manage_conversions() def reject(self): """ Use standard dialog to ask whether procedure must stop or not. Use the SIGSTOP to stop the conversion process while waiting for user to respond and SIGCONT or kill depending on user's answer. """ if not self.files: QDialog.accept(self) return if self._type == 'AudioVideo': self.process.send_signal(signal.SIGSTOP) self.running = False reply = QMessageBox.question( self, 'FF Multi Converter - ' + self.tr('Cancel Conversion'), self.tr('Are you sure you want to cancel conversion?'), QMessageBox.Yes|QMessageBox.Cancel ) if reply == QMessageBox.Yes: if self._type == 'AudioVideo': self.process.kill() self.running = False self.thread.join() QDialog.reject(self) if reply == QMessageBox.Cancel: self.running = True if self._type == 'AudioVideo': self.process.send_signal(signal.SIGCONT) else: self.manage_conversions() def convert_a_file(self): """ Update self.nowQL's text with current file's name, set self.nowQPBar value to zero and start the conversion procedure in a second thread using threading module. """ if not self.files: return from_file = list(self.files[0].keys())[0] to_file = list(self.files[0].values())[0] text = os.path.basename(from_file[1:-1]) num_file = self.num_total_files - len(self.files) + 1 text += ' ({0}/{1})'.format(num_file, self.num_total_files) self.nowQL.setText(self.tr('In progress:') + ' ' + text) self.nowQPBar.setValue(0) self.min_value = self.totalQPBar.value() self.max_value = self.min_value + self.step if not os.path.exists(from_file[1:-1]): self.error += 1 self.file_converted_signal.emit() return def convert(): if self._type == 'AudioVideo': conv_func = self.convert_video params = (from_file, to_file, self.cmd) elif self._type == 'Images': conv_func = self.convert_image params = (from_file, to_file, self.size, self.mntaspect, self.imgcmd) else: conv_func = self.convert_document params = (from_file, to_file) if conv_func(*params): self.ok += 1 if self.delete and not from_file == to_file: try: os.remove(from_file[1:-1]) except OSError: pass else: self.error += 1 self.file_converted_signal.emit() self.thread = threading.Thread(target=convert) self.thread.start() def convert_video(self, from_file, to_file, command): """ Create the ffmpeg command and execute it in a new process using the subprocess module. While the process is alive, parse ffmpeg output, estimate conversion progress using video's duration. With the result, emit the corresponding signal in order progressbars to be updated. Also emit regularly the corresponding signal in order an outputQTE to be updated with ffmpeg's output. Finally, save log information. Return True if conversion succeed, else False. """ # note: from_file and to_file names are inside quotation marks convert_cmd = '{0} -y -i {1} {2} {3}'.format( self.parent.vidconverter, from_file, command, to_file) self.update_text_edit_signal.emit(convert_cmd + '\n') self.process = subprocess.Popen( shlex.split(convert_cmd), stderr=subprocess.STDOUT, stdout=subprocess.PIPE ) final_output = myline = '' reader = io.TextIOWrapper(self.process.stdout, encoding='utf8') while True: out = reader.read(1) if out == '' and self.process.poll() is not None: break myline += out if out in ('\r', '\n'): m = re.search("Duration: ([0-9:.]+)", myline) if m: total = utils.duration_in_seconds(m.group(1)) n = re.search("time=([0-9:]+)", myline) # time can be of format 'time=hh:mm:ss.ts' or 'time=ss.ts' # depending on ffmpeg version if n: time = n.group(1) if ':' in time: time = utils.duration_in_seconds(time) now_sec = int(float(time)) try: self.refr_bars_signal.emit(100 * now_sec / total) except (UnboundLocalError, ZeroDivisionError): pass self.update_text_edit_signal.emit(myline) final_output += myline myline = '' self.update_text_edit_signal.emit('\n\n') return_code = self.process.poll() log_data = { 'command' : convert_cmd, 'returncode' : return_code, 'type' : 'VIDEO' } log_lvl = logging.info if return_code == 0 else logging.error log_lvl(final_output, extra=log_data) return return_code == 0 def convert_image(self, from_file, to_file, size, mntaspect, imgcmd): """ Convert an image using ImageMagick. Create conversion info ("cmd") and emit the corresponding signal in order an outputQTE to be updated with that info. Finally, save log information. Return True if conversion succeed, else False. """ # note: from_file and to_file names are inside quotation marks resize = '' if size: resize = '-resize {0}'.format(size) if not mntaspect: resize += '\!' imgcmd = ' ' + imgcmd.strip() + ' ' cmd = 'convert {0} {1}{2}{3}'.format(from_file, resize, imgcmd, to_file) self.update_text_edit_signal.emit(cmd + '\n') child = subprocess.Popen( shlex.split(cmd), stderr=subprocess.STDOUT, stdout=subprocess.PIPE ) child.wait() reader = io.TextIOWrapper(child.stdout, encoding='utf8') final_output = reader.read() self.update_text_edit_signal.emit(final_output+'\n\n') return_code = child.poll() log_data = { 'command' : cmd, 'returncode' : return_code, 'type' : 'IMAGE' } log_lvl = logging.info if return_code == 0 else logging.error log_lvl(final_output, extra=log_data) return return_code == 0 def convert_document(self, from_file, to_file): """ Create the unoconv command and execute it using the subprocess module. Emit the corresponding signal in order an outputQTE to be updated with unoconv's output. Finally, save log information. Return True if conversion succeed, else False. """ # note: from_file and to_file names are inside quotation marks to_base, to_ext = os.path.splitext(to_file[1:-1]) cmd = 'unoconv -f {0} -o {1} {2}'.format(to_ext[1:], to_file, from_file) self.update_text_edit_signal.emit(cmd + '\n') child = subprocess.Popen( shlex.split(cmd), stderr=subprocess.STDOUT, stdout=subprocess.PIPE ) child.wait() reader = io.TextIOWrapper(child.stdout, encoding='utf8') final_output = reader.read() self.update_text_edit_signal.emit(final_output+'\n\n') return_code = child.poll() log_data = { 'command' : cmd, 'returncode' : return_code, 'type' : 'DOCUMENT' } log_lvl = logging.info if return_code == 0 else logging.error log_lvl(final_output, extra=log_data) return return_code == 0
class Progress(QDialog): file_converted_signal = pyqtSignal() refr_bars_signal = pyqtSignal(int) update_text_edit_signal = pyqtSignal(str) def __init__(self, files, _type, cmd, ffmpeg, size, mntaspect, delete, parent, test=False): """ Keyword arguments: files -- list with dicts containing file names _type -- 'AudioVideo', 'Images' or 'Documents' depending files type cmd -- ffmpeg command, for audio/video conversions ffmpeg -- if True ffmpeg will be used, else avconv for audio/video conversions size -- new image size string of type 'widthxheight' eg. '300x300' for image conversions mntaspect -- boolean indicating whether aspect ratio must be maintained for image conversions delete -- boolean that shows if files must removed after conversion files: Each dict have only one key and one corresponding value. Key is a file to be converted and it's value is the name of the new file that will be converted. Example list: [{"/foo/bar.png" : "/foo/bar.bmp"}, {"/f/bar2.png" : "/f/bar2.bmp"}] """ super(Progress, self).__init__(parent) self.parent = parent self._type = _type self.cmd = cmd self.ffmpeg = ffmpeg self.size = size self.mntaspect = mntaspect self.files = files self.delete = delete if not test: self.step = int(100 / len(files)) self.ok = 0 self.error = 0 self.running = True self.nowLabel = QLabel(self.tr('In progress: ')) totalLabel = QLabel(self.tr('Total:')) self.nowBar = QProgressBar() self.nowBar.setValue(0) self.totalBar = QProgressBar() self.totalBar.setValue(0) self.cancelButton = QPushButton(self.tr('Cancel')) detailsButton = QCommandLinkButton(self.tr('Details')) detailsButton.setSizePolicy(QSizePolicy(QSizePolicy.Fixed)) detailsButton.setCheckable(True) detailsButton.setMaximumWidth(113) line = QFrame() line.setFrameShape(QFrame.HLine) line.setFrameShadow(QFrame.Sunken) self.textEdit = QTextEdit() self.textEdit.setReadOnly(True) self.frame = QFrame() frame_layout = pyqttools.add_to_layout(QHBoxLayout(), self.textEdit) self.frame.setLayout(frame_layout) self.frame.hide() hlayout = pyqttools.add_to_layout(QHBoxLayout(), None, self.nowLabel, None) hlayout2 = pyqttools.add_to_layout(QHBoxLayout(), None, totalLabel, None) hlayout3 = pyqttools.add_to_layout(QHBoxLayout(), detailsButton, line) hlayout4 = pyqttools.add_to_layout(QHBoxLayout(), self.frame) hlayout5 = pyqttools.add_to_layout(QHBoxLayout(), None, self.cancelButton) vlayout = pyqttools.add_to_layout(QVBoxLayout(), hlayout, self.nowBar, hlayout2, self.totalBar, None, hlayout3, hlayout4, hlayout5) self.setLayout(vlayout) detailsButton.toggled.connect(self.resize_dialog) detailsButton.toggled.connect(self.frame.setVisible) self.cancelButton.clicked.connect(self.reject) self.file_converted_signal.connect(self.file_converted) self.refr_bars_signal.connect(self.refresh_progress_bars) self.update_text_edit_signal.connect(self.update_text_edit) self.resize(484, 200) self.setWindowTitle('FF Multi Converter - ' + self.tr('Conversion')) if not test: QTimer.singleShot(0, self.manage_conversions) def resize_dialog(self): """Resize dialog.""" height = 200 if self.frame.isVisible() else 366 self.setMinimumSize(484, height) self.resize(484, height) def update_text_edit(self, txt): """Append txt to the end of current self.textEdit's text.""" current = self.textEdit.toPlainText() self.textEdit.setText(current+txt) self.textEdit.moveCursor(QTextCursor.End) def refresh_progress_bars(self, now_percent): """Refresh the values of self.nowBar and self.totalBar.""" total_percent = int(((now_percent * self.step) / 100) + self.min_value) if now_percent > self.nowBar.value() and not (now_percent > 100): self.nowBar.setValue(now_percent) if (total_percent > self.totalBar.value() and not (total_percent > self.max_value)): self.totalBar.setValue(total_percent) def manage_conversions(self): """ Check whether all files have been converted. If not, it will allow convert_a_file() to convert the next file. """ if not self.running: return if not self.files: self.totalBar.setValue(100) if self.totalBar.value() >= 100: sum_files = self.ok + self.error msg = QMessageBox(self) msg.setStandardButtons(QMessageBox.Ok) msg.setWindowTitle(self.tr("Report")) msg.setText(self.tr("Converted: %1/%2").arg(self.ok).arg(sum_files)) msg.setModal(False) msg.show() self.cancelButton.setText(self.tr("Close")) if self._type == 'Documents': self.parent.docconv = False # doc conversion end else: self.convert_a_file() def file_converted(self): """ Update progress bars values, remove converted file from self.files and call manage_conversions() to continue the process. """ self.totalBar.setValue(self.max_value) self.nowBar.setValue(100) QApplication.processEvents() self.files.pop(0) self.manage_conversions() def reject(self): """ Use standard dialog to ask whether procedure must stop or not. Use the SIGSTOP to stop the conversion process while waiting for user to respond and SIGCONT or kill depending on user's answer. """ if not self.files: QDialog.accept(self) return if self._type == 'AudioVideo': self.process.send_signal(signal.SIGSTOP) self.running = False reply = QMessageBox.question(self, 'FF Multi Converter - ' + self.tr('Cancel Conversion'), self.tr('Are you sure you want to cancel conversion?'), QMessageBox.Yes|QMessageBox.Cancel) if reply == QMessageBox.Yes: if self._type == 'AudioVideo': self.process.kill() if self._type == 'Documents': self.parent.docconv = False self.running = False self.thread.join() QDialog.reject(self) if reply == QMessageBox.Cancel: self.running = True if self._type == 'AudioVideo': self.process.send_signal(signal.SIGCONT) else: self.manage_conversions() def convert_a_file(self): """ Update self.nowLabel's text with current file's name, set self.nowBar value to zero and start the conversion procedure in a second thread using threading module. """ if not self.files: return from_file = self.files[0].keys()[0] to_file = self.files[0].values()[0] if len(from_file) > 40: # split file name if it is too long in order to display it properly text = '.../' + from_file.split('/')[-1] else: text = from_file self.nowLabel.setText(self.tr('In progress:') + ' ' + text) self.nowBar.setValue(0) self.min_value = self.totalBar.value() self.max_value = self.min_value + self.step if not os.path.exists(from_file[1:-1]): self.error += 1 self.file_converted_signal.emit() return def convert(): if self._type == 'AudioVideo': conv_func = self.convert_video params = (from_file, to_file, self.cmd, self.ffmpeg) elif self._type == 'Images': conv_func = self.convert_image params = (from_file, to_file, self.size, self.mntaspect) else: conv_func = self.convert_doc params = (from_file, to_file) if conv_func(*params): self.ok += 1 if self.delete: try: os.remove(from_file[1:-1]) except OSError: pass else: self.error += 1 self.file_converted_signal.emit() self.thread = threading.Thread(target=convert) self.thread.start() def duration_in_seconds(self, duration): """ Return the number of seconds of duration, an integer. Duration is a strinf of type hh:mm:ss.ts """ duration = duration.split('.')[0] hours, mins, secs = duration.split(':') seconds = int(secs) seconds += (int(hours) * 3600) + (int(mins) * 60) return seconds def convert_video(self, from_file, to_file, command, ffmpeg): """ Create the ffmpeg command and execute it in a new process using the subprocess module. While the process is alive, parse ffmpeg output, estimate conversion progress using video's duration. With the result, emit the corresponding signal in order progressbars to be updated. Also emit regularly the corresponding signal in order an textEdit to be updated with ffmpeg's output. Finally, save log information. Return True if conversion succeed, else False. """ assert isinstance(from_file, unicode) and isinstance(to_file, unicode) assert from_file.startswith('"') and from_file.endswith('"') assert to_file.startswith('"') and to_file.endswith('"') converter = 'ffmpeg' if ffmpeg else 'avconv' convert_cmd = '{0} -y -i {1} {2} {3}'.format(converter, from_file, command, to_file) convert_cmd = str(QString(convert_cmd).toUtf8()) self.update_text_edit_signal.emit(unicode(convert_cmd, 'utf-8')+'\n') self.process = subprocess.Popen(shlex.split(convert_cmd), stderr=subprocess.STDOUT, stdout=subprocess.PIPE) final_output = myline = str('') while True: out = str(QString(self.process.stdout.read(1)).toUtf8()) if out == str('') and self.process.poll() is not None: break myline += out if out in (str('\r'), str('\n')): m = re.search("Duration: ([0-9:.]+)", myline) if m: total = self.duration_in_seconds(m.group(1)) n = re.search("time=([0-9:]+)", myline) # time can be of format 'time=hh:mm:ss.ts' or 'time=ss.ts' # depending on ffmpeg version if n: time = n.group(1) if ':' in time: time = self.duration_in_seconds(time) now_sec = int(float(time)) try: self.refr_bars_signal.emit(100 * now_sec / total) except UnboundLocalError, ZeroDivisionError: pass self.update_text_edit_signal.emit(myline) final_output += myline myline = str('') self.update_text_edit_signal.emit('\n\n') return_code = self.process.poll() log_data = {'command' : unicode(convert_cmd, 'utf-8'), 'returncode' : return_code, 'type' : 'VIDEO'} log_lvl = logging.info if return_code == 0 else logging.error log_lvl(unicode(final_output, 'utf-8'), extra=log_data) return return_code == 0
class AudioVideoTab(QWidget): def __init__(self, parent): super(AudioVideoTab, self).__init__(parent) self.parent = parent self.name = 'AudioVideo' self.formats = [ '3gp', 'aac', 'ac3', 'afc', 'aiff', 'amr', 'asf', 'au', 'avi', 'dvd', 'flac', 'flv', 'mka', 'mkv', 'mmf', 'mov', 'mp3', 'mp4', 'mpg', 'ogg', 'ogv', 'psp', 'rm', 'spx', 'vob', 'wav', 'webm', 'wma', 'wmv' ] self.extra_formats = [ 'aifc', 'm2t', 'm4a', 'm4v', 'mp2', 'mpeg', 'ra', 'ts' ] nochange = self.tr('No Change') frequency_values = [nochange, '22050', '44100', '48000'] bitrate_values = [ nochange, '32', '96', '112', '128', '160', '192', '256', '320' ] pattern = QRegExp(r'^[1-9]\d*') validator = QRegExpValidator(pattern, self) converttoLabel = QLabel(self.tr('Convert to:')) self.extComboBox = QComboBox() self.extComboBox.addItems(self.formats + [self.tr('Other')]) self.extComboBox.setMinimumWidth(130) self.extLineEdit = QLineEdit() self.extLineEdit.setMaximumWidth(85) self.extLineEdit.setEnabled(False) hlayout1 = pyqttools.add_to_layout(QHBoxLayout(), converttoLabel, None, self.extComboBox, self.extLineEdit) commandLabel = QLabel(self.tr('Command:')) self.commandLineEdit = QLineEdit() self.presetButton = QPushButton(self.tr('Preset')) self.defaultButton = QPushButton(self.tr('Default')) hlayout2 = pyqttools.add_to_layout(QHBoxLayout(), commandLabel, self.commandLineEdit, self.presetButton, self.defaultButton) sizeLabel = QLabel(self.tr('Video Size:')) aspectLabel = QLabel(self.tr('Aspect:')) frameLabel = QLabel(self.tr('Frame Rate (fps):')) bitrateLabel = QLabel(self.tr('Video Bitrate (kbps):')) self.widthLineEdit = pyqttools.create_LineEdit((50, 16777215), validator, 4) self.heightLineEdit = pyqttools.create_LineEdit((50, 16777215), validator, 4) label = QLabel('x') layout1 = pyqttools.add_to_layout(QHBoxLayout(), self.widthLineEdit, label, self.heightLineEdit) self.aspect1LineEdit = pyqttools.create_LineEdit((35, 16777215), validator, 2) self.aspect2LineEdit = pyqttools.create_LineEdit((35, 16777215), validator, 2) label = QLabel(':') layout2 = pyqttools.add_to_layout(QHBoxLayout(), self.aspect1LineEdit, label, self.aspect2LineEdit) self.frameLineEdit = pyqttools.create_LineEdit(None, validator, 4) self.bitrateLineEdit = pyqttools.create_LineEdit(None, validator, 6) labels = [sizeLabel, aspectLabel, frameLabel, bitrateLabel] widgets = [layout1, layout2, self.frameLineEdit, self.bitrateLineEdit] videosettings_layout = QHBoxLayout() for a, b in zip(labels, widgets): text = a.text() a.setText('<html><p align="center">{0}</p></html>'.format(text)) layout = pyqttools.add_to_layout(QVBoxLayout(), a, b) videosettings_layout.addLayout(layout) freqLabel = QLabel(self.tr('Frequency (Hz):')) chanLabel = QLabel(self.tr('Channels:')) bitrateLabel = QLabel(self.tr('Audio Bitrate (kbps):')) self.freqComboBox = QComboBox() self.freqComboBox.addItems(frequency_values) self.chan1RadioButton = QRadioButton('1') self.chan1RadioButton.setMaximumSize(QSize(51, 16777215)) self.chan2RadioButton = QRadioButton('2') self.chan2RadioButton.setMaximumSize(QSize(51, 16777215)) self.group = QButtonGroup() self.group.addButton(self.chan1RadioButton) self.group.addButton(self.chan2RadioButton) spcr1 = QSpacerItem(40, 20, QSizePolicy.Preferred, QSizePolicy.Minimum) spcr2 = QSpacerItem(40, 20, QSizePolicy.Preferred, QSizePolicy.Minimum) chanlayout = pyqttools.add_to_layout(QHBoxLayout(), spcr1, self.chan1RadioButton, self.chan2RadioButton, spcr2) self.audio_bitrateComboBox = QComboBox() self.audio_bitrateComboBox.addItems(bitrate_values) labels = [freqLabel, chanLabel, bitrateLabel] widgets = [self.freqComboBox, chanlayout, self.audio_bitrateComboBox] audiosettings_layout = QHBoxLayout() for a, b in zip(labels, widgets): text = a.text() a.setText('<html><p align="center">{0}</p></html>'.format(text)) layout = pyqttools.add_to_layout(QVBoxLayout(), a, b) audiosettings_layout.addLayout(layout) hidden_layout = pyqttools.add_to_layout(QVBoxLayout(), videosettings_layout, audiosettings_layout) line = QFrame() line.setFrameShape(QFrame.HLine) line.setFrameShadow(QFrame.Sunken) self.moreButton = QPushButton(QApplication.translate('Tab', 'More')) self.moreButton.setSizePolicy(QSizePolicy(QSizePolicy.Fixed)) self.moreButton.setCheckable(True) hlayout3 = pyqttools.add_to_layout(QHBoxLayout(), line, self.moreButton) self.frame = QFrame() self.frame.setLayout(hidden_layout) self.frame.hide() final_layout = pyqttools.add_to_layout(QVBoxLayout(), hlayout1, hlayout2, hlayout3, self.frame) self.setLayout(final_layout) self.presetButton.clicked.connect(self.choose_preset) self.defaultButton.clicked.connect(self.set_default_command) self.moreButton.toggled.connect(self.frame.setVisible) self.moreButton.toggled.connect(self.resize_parent) self.extComboBox.currentIndexChanged.connect( lambda: self.extLineEdit.setEnabled(self.extComboBox.currentIndex( ) == len(self.formats))) self.widthLineEdit.textChanged.connect( lambda: self.command_elements_change('size')) self.heightLineEdit.textChanged.connect( lambda: self.command_elements_change('size')) self.aspect1LineEdit.textChanged.connect( lambda: self.command_elements_change('aspect')) self.aspect2LineEdit.textChanged.connect( lambda: self.command_elements_change('aspect')) self.frameLineEdit.textChanged.connect( lambda: self.command_elements_change('frames')) self.bitrateLineEdit.textChanged.connect( lambda: self.command_elements_change('video_bitrate')) self.freqComboBox.currentIndexChanged.connect( lambda: self.command_elements_change('frequency')) self.audio_bitrateComboBox.currentIndexChanged.connect( lambda: self.command_elements_change('audio_bitrate')) self.chan1RadioButton.clicked.connect( lambda: self.command_elements_change('channels1')) self.chan2RadioButton.clicked.connect( lambda: self.command_elements_change('channels2')) def resize_parent(self): """Resize MainWindow.""" height = MAIN_FIXED_HEIGHT if self.frame.isVisible() else MAIN_HEIGHT self.parent.setMinimumSize(MAIN_WIDTH, height) self.parent.resize(MAIN_WIDTH, height) def clear(self): """Clear all values of graphical widgets.""" lines = [ self.commandLineEdit, self.widthLineEdit, self.heightLineEdit, self.aspect1LineEdit, self.aspect2LineEdit, self.frameLineEdit, self.bitrateLineEdit, self.extLineEdit ] for i in lines: i.clear() self.freqComboBox.setCurrentIndex(0) self.audio_bitrateComboBox.setCurrentIndex(0) self.group.setExclusive(False) self.chan1RadioButton.setChecked(False) self.chan2RadioButton.setChecked(False) self.group.setExclusive(True) # setExclusive(False) in order to be able to uncheck checkboxes and # then setExclusive(True) so only one radio button can be set def ok_to_continue(self): """ Check if everything is ok with audiovideotab to continue conversion. Check if: - Either ffmpeg or avconv are installed. - Desired extension is valid. - self.commandLineEdit is empty. Return True if all tests pass, else False. """ if not self.parent.ffmpeg and not self.parent.avconv: QMessageBox.warning( self, 'FF Multi Converter - ' + self.tr('Error!'), self. tr('Neither ffmpeg nor avconv are installed.' '\nYou will not be able to convert audio/video files until you' ' install one of them.')) return False if self.extLineEdit.isEnabled(): text = str(self.extLineEdit.text()).strip() if len(text.split()) != 1 or text[0] == '.': QMessageBox.warning( self, 'FF Multi Converter - ' + self.tr('Error!'), self.tr('Extension must be one word and must ' 'not start with a dot.')) self.extLineEdit.selectAll() self.extLineEdit.setFocus() return False if not self.commandLineEdit.text(): QMessageBox.warning( self, 'FF Multi Converter - ' + self.tr('Error!'), self.tr('The command LineEdit may not be empty.')) self.commandLineEdit.setFocus() return False return True def set_default_command(self): """Set the default value to self.commandLineEdit.""" self.clear() self.commandLineEdit.setText(self.parent.default_command) def choose_preset(self): """ Open the presets dialog and update self.commandLineEdit, self.extComboBox and self.extLineEdit with the appropriate values. """ dialog = presets_dlgs.ShowPresets() if dialog.exec_() and dialog.the_command is not None: self.commandLineEdit.setText(dialog.the_command) self.commandLineEdit.home(False) find = self.extComboBox.findText(dialog.the_extension) if find >= 0: self.extComboBox.setCurrentIndex(find) else: self.extComboBox.setCurrentIndex(len(self.formats)) self.extLineEdit.setText(dialog.the_extension) def remove_consecutive_spaces(self, string): """Remove any consecutive spaces from a string and return it.""" temp = string string = '' for i in temp.split(): if i: string += i + ' ' return string[:-1] def command_elements_change(self, widget): """Fill self.commandLineEdit with the appropriate command parameters.""" command = str(self.commandLineEdit.text()) if widget == 'size': text1 = self.widthLineEdit.text() text2 = self.heightLineEdit.text() if (text1 or text2) and not (text1 and text2): return f = re.sub(r'^.*(-s\s+\d+x\d+).*$', r'\1', command) if re.match(r'^.*(-s\s+\d+x\d+).*$', f): command = command.replace(f, '').strip() if text1 and text2: command += ' -s {0}x{1}'.format(text1, text2) elif widget == 'aspect': text1 = self.aspect1LineEdit.text() text2 = self.aspect2LineEdit.text() if (text1 or text2) and not (text1 and text2): return f = re.sub(r'^.*(-aspect\s+\d+:\d+).*$', r'\1', command) if re.match(r'^.*(-aspect\s+\d+:\d+).*$', f): command = command.replace(f, '').strip() if text1 and text2: command += ' -aspect {0}:{1}'.format(text1, text2) elif widget == 'frames': text = self.frameLineEdit.text() f = re.sub(r'^.*(-r\s+\d+).*$', r'\1', command) if re.match(r'^.*(-r\s+\d+).*$', f): command = command.replace(f, '').strip() if text: command += ' -r {0}'.format(text) elif widget == 'video_bitrate': text = self.bitrateLineEdit.text() f = re.sub(r'^.*(-b\s+\d+k).*$', r'\1', command) if re.match(r'^.*(-b\s+\d+k).*$', f): command = command.replace(f, '') if text: command += ' -b {0}k'.format(text) command = command.replace('-sameq', '').strip() elif widget == 'frequency': text = self.freqComboBox.currentText() f = re.sub(r'^.*(-ar\s+\d+).*$', r'\1', command) if re.match(r'^.*(-ar\s+\d+).*$', f): command = command.replace(f, '').strip() if text != 'No Change': command += ' -ar {0}'.format(text) elif widget == 'audio_bitrate': text = self.audio_bitrateComboBox.currentText() f = re.sub(r'^.*(-ab\s+\d+k).*$', r'\1', command) if re.match(r'^.*(-ab\s+\d+k).*$', f): command = command.replace(f, '').strip() if text != 'No Change': command += ' -ab {0}k'.format(text) elif widget in ('channels1', 'channels2'): text = self.chan1RadioButton.text() if widget == 'channels1' \ else self.chan2RadioButton.text() f = re.sub(r'^.*(-ac\s+\d+).*$', r'\1', command) if re.match(r'^.*(-ac\s+\d+).*$', f): command = command.replace(f, '').strip() command += ' -ac {0}'.format(text) self.commandLineEdit.clear() self.commandLineEdit.setText(self.remove_consecutive_spaces(command))
class AudioVideoTab(QWidget): def __init__(self, parent): super(AudioVideoTab, self).__init__(parent) self.parent = parent self.name = 'AudioVideo' self.formats = ['3gp', 'aac', 'ac3', 'afc', 'aiff', 'amr', 'asf', 'au', 'avi', 'dvd', 'flac', 'flv', 'mka', 'mkv', 'mmf', 'mov', 'mp3', 'mp4', 'mpg', 'ogg', 'ogv', 'psp', 'rm', 'spx', 'vob', 'wav', 'webm', 'wma', 'wmv'] self.extra_formats = ['aifc', 'm2t', 'm4a', 'm4v', 'mp2', 'mpeg', 'ra', 'ts'] nochange = self.tr('No Change') frequency_values = [nochange, '22050', '44100', '48000'] bitrate_values = [nochange, '32', '96', '112', '128', '160', '192', '256', '320'] pattern = QRegExp(r'^[1-9]\d*') validator = QRegExpValidator(pattern, self) converttoLabel = QLabel(self.tr('Convert to:')) self.extComboBox = QComboBox() self.extComboBox.addItems(self.formats + [self.tr('Other')]) self.extComboBox.setMinimumWidth(130) self.extLineEdit = QLineEdit() self.extLineEdit.setMaximumWidth(85) self.extLineEdit.setEnabled(False) hlayout1 = pyqttools.add_to_layout(QHBoxLayout(), converttoLabel, None, self.extComboBox, self.extLineEdit) commandLabel = QLabel(self.tr('Command:')) self.commandLineEdit = QLineEdit() self.presetButton = QPushButton(self.tr('Preset')) self.defaultButton = QPushButton(self.tr('Default')) hlayout2 = pyqttools.add_to_layout(QHBoxLayout(), commandLabel, self.commandLineEdit, self.presetButton, self.defaultButton) sizeLabel = QLabel(self.tr('Video Size:')) aspectLabel = QLabel(self.tr('Aspect:')) frameLabel = QLabel(self.tr('Frame Rate (fps):')) bitrateLabel = QLabel(self.tr('Video Bitrate (kbps):')) self.widthLineEdit = pyqttools.create_LineEdit((50, 16777215), validator, 4) self.heightLineEdit = pyqttools.create_LineEdit((50, 16777215), validator,4) label = QLabel('x') layout1 = pyqttools.add_to_layout(QHBoxLayout(), self.widthLineEdit, label, self.heightLineEdit) self.aspect1LineEdit = pyqttools.create_LineEdit((35, 16777215), validator,2) self.aspect2LineEdit = pyqttools.create_LineEdit((35, 16777215), validator,2) label = QLabel(':') layout2 = pyqttools.add_to_layout(QHBoxLayout(), self.aspect1LineEdit, label, self.aspect2LineEdit) self.frameLineEdit = pyqttools.create_LineEdit(None, validator, 4) self.bitrateLineEdit = pyqttools.create_LineEdit(None, validator, 6) labels = [sizeLabel, aspectLabel, frameLabel, bitrateLabel] widgets = [layout1, layout2, self.frameLineEdit, self.bitrateLineEdit] videosettings_layout = QHBoxLayout() for a, b in zip(labels, widgets): text = a.text() a.setText('<html><p align="center">{0}</p></html>'.format(text)) layout = pyqttools.add_to_layout(QVBoxLayout(), a, b) videosettings_layout.addLayout(layout) freqLabel = QLabel(self.tr('Frequency (Hz):')) chanLabel = QLabel(self.tr('Channels:')) bitrateLabel = QLabel(self.tr('Audio Bitrate (kbps):')) self.freqComboBox = QComboBox() self.freqComboBox.addItems(frequency_values) self.chan1RadioButton = QRadioButton('1') self.chan1RadioButton.setMaximumSize(QSize(51, 16777215)) self.chan2RadioButton = QRadioButton('2') self.chan2RadioButton.setMaximumSize(QSize(51, 16777215)) self.group = QButtonGroup() self.group.addButton(self.chan1RadioButton) self.group.addButton(self.chan2RadioButton) spcr1 = QSpacerItem(40, 20, QSizePolicy.Preferred, QSizePolicy.Minimum) spcr2 = QSpacerItem(40, 20, QSizePolicy.Preferred, QSizePolicy.Minimum) chanlayout = pyqttools.add_to_layout(QHBoxLayout(), spcr1, self.chan1RadioButton, self.chan2RadioButton, spcr2) self.audio_bitrateComboBox = QComboBox() self.audio_bitrateComboBox.addItems(bitrate_values) labels = [freqLabel, chanLabel, bitrateLabel] widgets = [self.freqComboBox, chanlayout, self.audio_bitrateComboBox] audiosettings_layout = QHBoxLayout() for a, b in zip(labels, widgets): text = a.text() a.setText('<html><p align="center">{0}</p></html>'.format(text)) layout = pyqttools.add_to_layout(QVBoxLayout(), a, b) audiosettings_layout.addLayout(layout) hidden_layout = pyqttools.add_to_layout(QVBoxLayout(), videosettings_layout, audiosettings_layout) line = QFrame() line.setFrameShape(QFrame.HLine) line.setFrameShadow(QFrame.Sunken) self.moreButton = QPushButton(QApplication.translate('Tab', 'More')) self.moreButton.setSizePolicy(QSizePolicy(QSizePolicy.Fixed)) self.moreButton.setCheckable(True) hlayout3 = pyqttools.add_to_layout(QHBoxLayout(), line, self.moreButton) self.frame = QFrame() self.frame.setLayout(hidden_layout) self.frame.hide() final_layout = pyqttools.add_to_layout(QVBoxLayout(), hlayout1, hlayout2, hlayout3, self.frame) self.setLayout(final_layout) self.presetButton.clicked.connect(self.choose_preset) self.defaultButton.clicked.connect(self.set_default_command) self.moreButton.toggled.connect(self.frame.setVisible) self.moreButton.toggled.connect(self.resize_parent) self.extComboBox.currentIndexChanged.connect( lambda: self.extLineEdit.setEnabled( self.extComboBox.currentIndex() == len(self.formats))) self.widthLineEdit.textChanged.connect( lambda: self.command_elements_change('size')) self.heightLineEdit.textChanged.connect( lambda: self.command_elements_change('size')) self.aspect1LineEdit.textChanged.connect( lambda: self.command_elements_change('aspect')) self.aspect2LineEdit.textChanged.connect( lambda: self.command_elements_change('aspect')) self.frameLineEdit.textChanged.connect( lambda: self.command_elements_change('frames')) self.bitrateLineEdit.textChanged.connect( lambda: self.command_elements_change('video_bitrate')) self.freqComboBox.currentIndexChanged.connect( lambda: self.command_elements_change('frequency')) self.audio_bitrateComboBox.currentIndexChanged.connect( lambda: self.command_elements_change('audio_bitrate')) self.chan1RadioButton.clicked.connect( lambda: self.command_elements_change('channels1')) self.chan2RadioButton.clicked.connect( lambda: self.command_elements_change('channels2')) def resize_parent(self): """Resize MainWindow.""" height = MAIN_FIXED_HEIGHT if self.frame.isVisible() else MAIN_HEIGHT self.parent.setMinimumSize(MAIN_WIDTH, height) self.parent.resize(MAIN_WIDTH, height) def clear(self): """Clear all values of graphical widgets.""" lines = [self.commandLineEdit, self.widthLineEdit, self.heightLineEdit, self.aspect1LineEdit, self.aspect2LineEdit, self.frameLineEdit, self.bitrateLineEdit, self.extLineEdit] for i in lines: i.clear() self.freqComboBox.setCurrentIndex(0) self.audio_bitrateComboBox.setCurrentIndex(0) self.group.setExclusive(False) self.chan1RadioButton.setChecked(False) self.chan2RadioButton.setChecked(False) self.group.setExclusive(True) # setExclusive(False) in order to be able to uncheck checkboxes and # then setExclusive(True) so only one radio button can be set def ok_to_continue(self): """ Check if everything is ok with audiovideotab to continue conversion. Check if: - Either ffmpeg or avconv are installed. - Desired extension is valid. - self.commandLineEdit is empty. Return True if all tests pass, else False. """ if not self.parent.ffmpeg and not self.parent.avconv: QMessageBox.warning(self, 'FF Multi Converter - ' + self.tr( 'Error!'), self.tr('Neither ffmpeg nor avconv are installed.' '\nYou will not be able to convert audio/video files until you' ' install one of them.')) return False if self.extLineEdit.isEnabled(): text = str(self.extLineEdit.text()).strip() if len(text.split()) != 1 or text[0] == '.': QMessageBox.warning(self, 'FF Multi Converter - ' + self.tr( 'Error!'), self.tr('Extension must be one word and must ' 'not start with a dot.')) self.extLineEdit.selectAll() self.extLineEdit.setFocus() return False if not self.commandLineEdit.text(): QMessageBox.warning(self, 'FF Multi Converter - ' + self.tr( 'Error!'), self.tr('The command LineEdit may not be empty.')) self.commandLineEdit.setFocus() return False return True def set_default_command(self): """Set the default value to self.commandLineEdit.""" self.clear() self.commandLineEdit.setText(self.parent.default_command) def choose_preset(self): """ Open the presets dialog and update self.commandLineEdit, self.extComboBox and self.extLineEdit with the appropriate values. """ dialog = presets_dlgs.ShowPresets() if dialog.exec_() and dialog.the_command is not None: self.commandLineEdit.setText(dialog.the_command) self.commandLineEdit.home(False) find = self.extComboBox.findText(dialog.the_extension) if find >= 0: self.extComboBox.setCurrentIndex(find) else: self.extComboBox.setCurrentIndex(len(self.formats)) self.extLineEdit.setText(dialog.the_extension) def remove_consecutive_spaces(self, string): """Remove any consecutive spaces from a string and return it.""" temp = string string = '' for i in temp.split(): if i: string += i + ' ' return string[:-1] def command_elements_change(self, widget): """Fill self.commandLineEdit with the appropriate command parameters.""" command = str(self.commandLineEdit.text()) if widget == 'size': text1 = self.widthLineEdit.text() text2 = self.heightLineEdit.text() if (text1 or text2) and not (text1 and text2): return f = re.sub(r'^.*(-s\s+\d+x\d+).*$', r'\1', command) if re.match(r'^.*(-s\s+\d+x\d+).*$', f): command = command.replace(f, '').strip() if text1 and text2: command += ' -s {0}x{1}'.format(text1, text2) elif widget == 'aspect': text1 = self.aspect1LineEdit.text() text2 = self.aspect2LineEdit.text() if (text1 or text2) and not (text1 and text2): return f = re.sub(r'^.*(-aspect\s+\d+:\d+).*$', r'\1', command) if re.match(r'^.*(-aspect\s+\d+:\d+).*$', f): command = command.replace(f, '').strip() if text1 and text2: command += ' -aspect {0}:{1}'.format(text1, text2) elif widget == 'frames': text = self.frameLineEdit.text() f = re.sub(r'^.*(-r\s+\d+).*$', r'\1', command) if re.match(r'^.*(-r\s+\d+).*$', f): command = command.replace(f, '').strip() if text: command += ' -r {0}'.format(text) elif widget == 'video_bitrate': text = self.bitrateLineEdit.text() f = re.sub(r'^.*(-b\s+\d+k).*$', r'\1', command) if re.match(r'^.*(-b\s+\d+k).*$', f): command = command.replace(f, '') if text: command += ' -b {0}k'.format(text) command = command.replace('-sameq', '').strip() elif widget == 'frequency': text = self.freqComboBox.currentText() f = re.sub(r'^.*(-ar\s+\d+).*$', r'\1', command) if re.match(r'^.*(-ar\s+\d+).*$', f): command = command.replace(f, '').strip() if text != 'No Change': command += ' -ar {0}'.format(text) elif widget == 'audio_bitrate': text = self.audio_bitrateComboBox.currentText() f = re.sub(r'^.*(-ab\s+\d+k).*$', r'\1', command) if re.match(r'^.*(-ab\s+\d+k).*$', f): command = command.replace(f, '').strip() if text != 'No Change': command += ' -ab {0}k'.format(text) elif widget in ('channels1', 'channels2'): text = self.chan1RadioButton.text() if widget == 'channels1' \ else self.chan2RadioButton.text() f = re.sub(r'^.*(-ac\s+\d+).*$', r'\1', command) if re.match(r'^.*(-ac\s+\d+).*$', f): command = command.replace(f, '').strip() command += ' -ac {0}'.format(text) self.commandLineEdit.clear() self.commandLineEdit.setText(self.remove_consecutive_spaces(command))
class ListEdit(QWidget): def __init__(self, parent=None, isChild=False): QWidget.__init__(self, parent) QGridLayout(self) # FIXME: call self.setLayout(layout)? self._readonly = False self.up_down = QFrame() self.up = UpButton() self.down = DownButton() self.add = AddButton() self.rem = RemButton() self.listEditView = ListEditView(isChild, parent=self) self.connect(self.listEditView, SIGNAL('itemAdded'), SIGNAL('itemAdded')) self.connect(self.listEditView, SIGNAL('itemModified'), SIGNAL('itemModified')) self.connect(self.listEditView.model, SIGNAL('dataChanged(QModelIndex,QModelIndex)'), SIGNAL('dataChanged(QModelIndex,QModelIndex)')) self.connect(self.listEditView.model, SIGNAL('rowsRemoved(QModelIndex,int,int)'), SIGNAL('rowsRemoved(QModelIndex,int,int)')) self.connect(self.listEditView.model, SIGNAL('headerDataChanged(Qt::Orientation,int,int)'), SIGNAL('headerDataChanged(Qt::Orientation,int,int)')) self.connect(self.listEditView, SIGNAL('itemDeleted'), SIGNAL('itemDeleted')) self.connect(self.listEditView, SIGNAL('itemSorted'), SIGNAL('itemSorted')) self.connect(self.listEditView.horizontalHeader(), SIGNAL('sectionClicked(int)'), SIGNAL('sectionClicked(int)')) self.connect(self.listEditView, SIGNAL('clicked(QModelIndex)'), SIGNAL('clicked(QModelIndex)')) self.layout().addWidget(self.listEditView, 0, 0) self.buildUpDown() if not isChild: self.buildAddRem() self.setDisplayUpDown(False) self.setReadOnly(False) # Qt Properties ... def getReadOnly(self): return self._readonly def setReadOnly(self, readonly): self._readonly = readonly self.up.setEnabled(not self.readOnly) self.down.setEnabled(not self.readOnly) self.add.setEnabled(not self.readOnly) self.rem.setEnabled(not self.readOnly) self.listEditView.setReadOnly(self.readOnly) def resetReadOnly(self): self._readonly = False readOnly = pyqtProperty('bool', getReadOnly, setReadOnly, resetReadOnly) def getAcceptDrops(self): return self.listEditView.acceptDrops() def setAcceptDrops(self, mode): self.listEditView.setAcceptDrops(mode) flags = self.listEditView.model.getFlags() if mode: flags |= Qt.ItemIsDropEnabled else: flags &= ~Qt.ItemIsDropEnabled self.listEditView.model.setFlags(flags) def resetAcceptDrops(self): self.setAcceptDrops(False) acceptDrops = pyqtProperty('bool', getAcceptDrops, setAcceptDrops, resetAcceptDrops) def getDragDropMode(self): return self.listEditView.dragDropMode() Q_ENUMS('QAbstractItemView.DragDropMode') def setDragDropMode(self, mode): self.listEditView.setDragDropMode(mode) def resetDragDropMode(self): self.listEditView.setDragDropMode(QAbstractItemView.NoDragDrop) dragDropMode = pyqtProperty('QAbstractItemView::DragDropMode', getDragDropMode, setDragDropMode, resetDragDropMode) def getShowDropIndicator(self): return self.listEditView.showDropIndicator() def setShowDropIndicator(self, mode): self.listEditView.setShowDropIndicator(mode) def resetShowDropIndicator(self): self.listEditView.setShowDropIndicator(False) showDropIndicator = pyqtProperty('bool', getShowDropIndicator, setShowDropIndicator, resetShowDropIndicator) def getDisplayUpDown(self): return self.up_down.isVisible() def setDisplayUpDown(self, displayUpDown): self.up_down.setVisible(displayUpDown) def resetDisplayUpDown(self): self.up_down.setVisible(False) displayUpDown = pyqtProperty('bool', getDisplayUpDown, setDisplayUpDown, resetDisplayUpDown) def getEditBoxDescription(self): return self.listEditView.getEditBoxDescription() def setEditBoxDescription(self, description): self.listEditView.setEditBoxDescription(description) def resetEditBoxDescription(self): self.listEditView.resetEditBoxDescription() editBoxDescription = pyqtProperty('QString', getEditBoxDescription, setEditBoxDescription, resetEditBoxDescription) def getHeaders(self): return self.listEditView.getHeaders() def setHeaders(self, headers): self.listEditView.setHeaders(headers) def resetHeaders(self): self.listEditView.resetHeaders() headers = pyqtProperty('QStringList', getHeaders, setHeaders, resetHeaders) def getEditInPopup(self): return self.listEditView.editInPopup def setEditInPopup(self, in_popup): self.listEditView.editInPopup = in_popup def resetEditInPopup(self): self.listEditView.editInPopup = True editInPopup = pyqtProperty('bool', getEditInPopup, setEditInPopup, resetEditInPopup) def getEditInPlace(self): return self.listEditView.editInPlace def setEditInPlace(self, edit_in_place): self.listEditView.editInPlace = edit_in_place def resetEditInPlace(self): self.listEditView.editInPlace = False editInPlace = pyqtProperty('bool', getEditInPlace, setEditInPlace, resetEditInPlace) # ... Qt Properties def setDropMimeData(self, callback): self.listEditView.model.dropMimeData_cb = callback def setEditBox(self, editBox): """allow to customize edit popup""" # box = editBox([row1, row2, row3], listEditOption, windowTitle) # ret = box.exec_() # if QDialog.Accepted == ret: # newData = box.getData() # # newData : [ modifiedRow1, modifiedRow2, modifiedRow3 ] # box must return QDialog.Accepted if data have been modified / created # then data must be returned by box.getData() self.listEditView.editBox = editBox def setColDelegate(self, callback): """callback prototype: createDelegate(view, column)""" self.listEditView.setColDelegate(callback) def buildUpDown(self): up_down_layout = QVBoxLayout(self.up_down) up_down_layout.addWidget(self.up) up_down_layout.addWidget(self.down) up_down_layout.insertStretch(0) up_down_layout.insertStretch(-1) self.layout().addWidget(self.up_down, 0, 1) self.connect(self.up, SIGNAL('clicked()'), self.listEditView.upItem) self.connect(self.down, SIGNAL('clicked()'), self.listEditView.downItem) def buildAddRem(self): buttons = QFrame() buttons_layout = QHBoxLayout(buttons) buttons_layout.insertStretch(1) self.connect(self.add, SIGNAL('clicked()'), self.listEditView.addItem) self.connect(self.rem, SIGNAL('clicked()'), self.listEditView.removeItem) buttons_layout.addWidget(self.add) buttons_layout.addWidget(self.rem) self.layout().addWidget(buttons, 1, 0) def hideRow(self, row): self.listEditView.verticalHeader().setSectionHidden(row, True) def showRow(self, row): self.listEditView.verticalHeader().setSectionHidden(row, False) def hideColumn(self, col): self.listEditView.horizontalHeader().setSectionHidden(col, True) def showColumn(self, col): self.listEditView.horizontalHeader().setSectionHidden(col, False) def reset(self, data): """ TODO call clean & setData & reset """ self.listEditView.model.newData(data) self.listEditView.model.emit(SIGNAL("modelReset()")) def rawData(self): # TODO use model.data(...) return deepcopy(self.listEditView.model._data)
class Progress(QDialog): file_converted_signal = pyqtSignal() refr_bars_signal = pyqtSignal(int) update_text_edit_signal = pyqtSignal(str) def __init__(self, files, tab, delete, parent, test=False): """ Keyword arguments: files -- list with dicts containing file names tab -- instanseof AudioVideoTab, ImageTab or DocumentTab indicating currently active tab delete -- boolean that shows if files must removed after conversion parent -- parent widget files: Each dict have only one key and one corresponding value. Key is a file to be converted and it's value is the name of the new file that will be converted. Example list: [{"/foo/bar.png" : "/foo/bar.bmp"}, {"/f/bar2.png" : "/f/bar2.bmp"}] """ super(Progress, self).__init__(parent) self.parent = parent self.files = files self.num_total_files = len(self.files) self.tab = tab self.delete = delete if not test: self._type = tab.name self.step = int(100 / len(files)) self.ok = 0 self.error = 0 self.running = True self.nowQL = QLabel(self.tr('In progress: ')) totalQL = QLabel(self.tr('Total:')) self.nowQPBar = QProgressBar() self.nowQPBar.setValue(0) self.totalQPBar = QProgressBar() self.totalQPBar.setValue(0) self.cancelQPB = QPushButton(self.tr('Cancel')) detailsQPB = QCommandLinkButton(self.tr('Details')) detailsQPB.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) detailsQPB.setCheckable(True) detailsQPB.setMaximumWidth(113) line = QFrame() line.setFrameShape(QFrame.HLine) line.setFrameShadow(QFrame.Sunken) self.outputQTE = QTextEdit() self.outputQTE.setReadOnly(True) self.frame = QFrame() frame_layout = utils.add_to_layout('h', self.outputQTE) self.frame.setLayout(frame_layout) self.frame.hide() hlayout = utils.add_to_layout('h', None, self.nowQL, None) hlayout2 = utils.add_to_layout('h', None, totalQL, None) hlayout3 = utils.add_to_layout('h', detailsQPB, line) hlayout4 = utils.add_to_layout('h', self.frame) hlayout5 = utils.add_to_layout('h', None, self.cancelQPB) vlayout = utils.add_to_layout('v', hlayout, self.nowQPBar, hlayout2, self.totalQPBar, None, hlayout3, hlayout4, hlayout5) self.setLayout(vlayout) detailsQPB.toggled.connect(self.resize_dialog) detailsQPB.toggled.connect(self.frame.setVisible) self.cancelQPB.clicked.connect(self.reject) self.file_converted_signal.connect(self.next_file) self.refr_bars_signal.connect(self.refresh_progress_bars) self.update_text_edit_signal.connect(self.update_text_edit) self.resize(484, 200) self.setWindowTitle('FF Multi Converter - ' + self.tr('Conversion')) if not test: self.get_data() # should be first and not in QTimer.singleShot() QTimer.singleShot(0, self.manage_conversions) def get_data(self): """Collect conversion data from parents' widgets.""" if self._type == 'AudioVideo': self.cmd = self.tab.commandQLE.text() elif self._type == 'Images': width = self.tab.widthQLE.text() self.size = '' self.mntaspect = False if width: height = self.tab.heightQLE.text() self.size = '{0}x{1}'.format(width, height) self.mntaspect = self.tab.imgaspectQChB.isChecked() self.imgcmd = self.tab.commandQLE.text() if self.tab.autocropQChB.isChecked(): self.imgcmd += ' -trim +repage' rotate = self.tab.rotateQLE.text().strip() if rotate: self.imgcmd += ' -rotate {0}'.format(rotate) if self.tab.vflipQChB.isChecked(): self.imgcmd += ' -flip' if self.tab.hflipQChB.isChecked(): self.imgcmd += ' -flop' def resize_dialog(self): """Resize dialog.""" height = 200 if self.frame.isVisible() else 366 self.setMinimumSize(484, height) self.resize(484, height) def update_text_edit(self, txt): """Append txt to the end of current self.outputQTE's text.""" current = self.outputQTE.toPlainText() self.outputQTE.setText(current + txt) self.outputQTE.moveCursor(QTextCursor.End) def refresh_progress_bars(self, now_percent): """Refresh the values of self.nowQPBar and self.totalQPBar.""" total_percent = int(((now_percent * self.step) / 100) + self.min_value) if now_percent > self.nowQPBar.value() and not (now_percent > 100): self.nowQPBar.setValue(now_percent) if (total_percent > self.totalQPBar.value() and not (total_percent > self.max_value)): self.totalQPBar.setValue(total_percent) def manage_conversions(self): """ Check whether all files have been converted. If not, it will allow convert_a_file() to convert the next file. """ if not self.running: return if not self.files: self.totalQPBar.setValue(100) if self.totalQPBar.value() >= 100: sum_files = self.ok + self.error msg = QMessageBox(self) msg.setStandardButtons(QMessageBox.Ok) msg.setWindowTitle(self.tr("Report")) msg.setText( self.tr("Converted: {0}/{1}".format(self.ok, sum_files))) msg.setModal(False) msg.show() self.cancelQPB.setText(self.tr("Close")) else: self.convert_a_file() def next_file(self): """ Update progress bars values, remove converted file from self.files and call manage_conversions() to continue the process. """ self.totalQPBar.setValue(self.max_value) self.nowQPBar.setValue(100) QApplication.processEvents() self.files.pop(0) self.manage_conversions() def reject(self): """ Use standard dialog to ask whether procedure must stop or not. Use the SIGSTOP to stop the conversion process while waiting for user to respond and SIGCONT or kill depending on user's answer. """ if not self.files: QDialog.accept(self) return if self._type == 'AudioVideo': self.process.send_signal(signal.SIGSTOP) self.running = False reply = QMessageBox.question( self, 'FF Multi Converter - ' + self.tr('Cancel Conversion'), self.tr('Are you sure you want to cancel conversion?'), QMessageBox.Yes | QMessageBox.Cancel) if reply == QMessageBox.Yes: if self._type == 'AudioVideo': self.process.kill() self.running = False self.thread.join() QDialog.reject(self) if reply == QMessageBox.Cancel: self.running = True if self._type == 'AudioVideo': self.process.send_signal(signal.SIGCONT) else: self.manage_conversions() def convert_a_file(self): """ Update self.nowQL's text with current file's name, set self.nowQPBar value to zero and start the conversion procedure in a second thread using threading module. """ if not self.files: return from_file = list(self.files[0].keys())[0] to_file = list(self.files[0].values())[0] text = os.path.basename(from_file[1:-1]) num_file = self.num_total_files - len(self.files) + 1 text += ' ({0}/{1})'.format(num_file, self.num_total_files) self.nowQL.setText(self.tr('In progress:') + ' ' + text) self.nowQPBar.setValue(0) self.min_value = self.totalQPBar.value() self.max_value = self.min_value + self.step if not os.path.exists(from_file[1:-1]): self.error += 1 self.file_converted_signal.emit() return def convert(): if self._type == 'AudioVideo': conv_func = self.convert_video params = (from_file, to_file, self.cmd) elif self._type == 'Images': conv_func = self.convert_image params = (from_file, to_file, self.size, self.mntaspect, self.imgcmd) else: conv_func = self.convert_document params = (from_file, to_file) if conv_func(*params): self.ok += 1 if self.delete and not from_file == to_file: try: os.remove(from_file[1:-1]) except OSError: pass else: self.error += 1 self.file_converted_signal.emit() self.thread = threading.Thread(target=convert) self.thread.start() def convert_video(self, from_file, to_file, command): """ Create the ffmpeg command and execute it in a new process using the subprocess module. While the process is alive, parse ffmpeg output, estimate conversion progress using video's duration. With the result, emit the corresponding signal in order progressbars to be updated. Also emit regularly the corresponding signal in order an outputQTE to be updated with ffmpeg's output. Finally, save log information. Return True if conversion succeed, else False. """ # note: from_file and to_file names are inside quotation marks convert_cmd = '{0} -y -i {1} {2} {3}'.format(self.parent.vidconverter, from_file, command, to_file) self.update_text_edit_signal.emit(convert_cmd + '\n') self.process = subprocess.Popen(shlex.split(convert_cmd), stderr=subprocess.STDOUT, stdout=subprocess.PIPE) final_output = myline = '' reader = io.TextIOWrapper(self.process.stdout, encoding='utf8') while True: out = reader.read(1) if out == '' and self.process.poll() is not None: break myline += out if out in ('\r', '\n'): m = re.search("Duration: ([0-9:.]+)", myline) if m: total = utils.duration_in_seconds(m.group(1)) n = re.search("time=([0-9:]+)", myline) # time can be of format 'time=hh:mm:ss.ts' or 'time=ss.ts' # depending on ffmpeg version if n: time = n.group(1) if ':' in time: time = utils.duration_in_seconds(time) now_sec = int(float(time)) try: self.refr_bars_signal.emit(100 * now_sec / total) except (UnboundLocalError, ZeroDivisionError): pass self.update_text_edit_signal.emit(myline) final_output += myline myline = '' self.update_text_edit_signal.emit('\n\n') return_code = self.process.poll() log_data = { 'command': convert_cmd, 'returncode': return_code, 'type': 'VIDEO' } log_lvl = logging.info if return_code == 0 else logging.error log_lvl(final_output, extra=log_data) return return_code == 0 def convert_image(self, from_file, to_file, size, mntaspect, imgcmd): """ Convert an image using ImageMagick. Create conversion info ("cmd") and emit the corresponding signal in order an outputQTE to be updated with that info. Finally, save log information. Return True if conversion succeed, else False. """ # note: from_file and to_file names are inside quotation marks resize = '' if size: resize = '-resize {0}'.format(size) if not mntaspect: resize += '\!' imgcmd = ' ' + imgcmd.strip() + ' ' cmd = 'convert {0} {1}{2}{3}'.format(from_file, resize, imgcmd, to_file) self.update_text_edit_signal.emit(cmd + '\n') child = subprocess.Popen(shlex.split(cmd), stderr=subprocess.STDOUT, stdout=subprocess.PIPE) child.wait() reader = io.TextIOWrapper(child.stdout, encoding='utf8') final_output = reader.read() self.update_text_edit_signal.emit(final_output + '\n\n') return_code = child.poll() log_data = {'command': cmd, 'returncode': return_code, 'type': 'IMAGE'} log_lvl = logging.info if return_code == 0 else logging.error log_lvl(final_output, extra=log_data) return return_code == 0 def convert_document(self, from_file, to_file): """ Create the unoconv command and execute it using the subprocess module. Emit the corresponding signal in order an outputQTE to be updated with unoconv's output. Finally, save log information. Return True if conversion succeed, else False. """ # note: from_file and to_file names are inside quotation marks to_base, to_ext = os.path.splitext(to_file[1:-1]) cmd = 'unoconv -f {0} -o {1} {2}'.format(to_ext[1:], to_file, from_file) self.update_text_edit_signal.emit(cmd + '\n') child = subprocess.Popen(shlex.split(cmd), stderr=subprocess.STDOUT, stdout=subprocess.PIPE) child.wait() reader = io.TextIOWrapper(child.stdout, encoding='utf8') final_output = reader.read() self.update_text_edit_signal.emit(final_output + '\n\n') return_code = child.poll() log_data = { 'command': cmd, 'returncode': return_code, 'type': 'DOCUMENT' } log_lvl = logging.info if return_code == 0 else logging.error log_lvl(final_output, extra=log_data) return return_code == 0