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
Exemple #3
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))
Exemple #5
0
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