Python, mkvextract et un petit gui (en PyQt)

Publié le 10 février 2011 par Mikebrant

edit 17/02/2011 : MAJ du code

La PS3 et MKV ne faisant pas bon ménage,
on est obligé d’extraire les tracks (vidéo/audio/sous-titres ) du conteneur via mkvextract (et mkvinfo).

On fait :

mkvinfo fichier.mkv

pour connaitre les ID des tracks , puis:

mkvextract tracks fichier.mkv 1: fichier.mp4 2:fichier.mp3

pour les extraire.

Mais n’étant pas très fan des lignes de commandes, j’ai fait un petit script python qui prend en paramètre le nom du fichier(chemin inclus) et qui va s’occuper de taper ces deux commandes .

Je n’extraie ni les chapitres, ni les covers  : le script permet juste d’extraire les vidéos/audio/sous titres.

#-*- coding:utf-8 -*-
import os
import re
import sys
from subprocess import Popen,PIPE,STDOUT

VIDEO_EXTENSIONS = {'V_MPEG4/ISO/AVC':'mp4','default' : ''}
AUDIO_EXTENSIONS = {'A_AAC':'aac','default':''}
SUB_EXTENSIONS = {'default':'srt'}
EXTENSIONS = {'video':VIDEO_EXTENSIONS,'audio':AUDIO_EXTENSIONS,'subtitles':SUB_EXTENSIONS}

def get_extension(type,codec):
    """ """
    if codec in EXTENSIONS[type].keys():
        return EXTENSIONS[type][codec]
    return EXTENSIONS[type]['default']

def get_filename(new_names,filename,type,codec):
    """ obligé d'avoir un nom de fichier qui n'existe pas
        puisque mkvextract n'écrase pas mais retourne une erreur
    """
    new_name = (".").join(filename.split('.')[:-1])
    extension = get_extension(type,codec)
    number = 0
    while os.path.exists("%s.%d.%s"%(new_name,number,extension)) or "%s.%d.%s"%(new_name,number,extension) in new_names :
        number += 1
    return "%s.%d.%s"%(new_name,number,extension)

def get_tracks(filename):
    """ """
    ps = Popen(['mkvinfo',filename],stdout=PIPE,stderr=STDOUT)
    output = ps.communicate()[0]
    if ps.returncode == 0:
        results = re.findall(
            r"""(?mx)
                ^\|\s*[+]\sTrack\snumber:\s(\d+)\s
                (?:^.+\s)*?
                ^\|\s*[+]\sTrack\stype:\s*(.+)\s
                (?:^.+\s)*?
                ^\|\s*[+]\sCodec\sID:\s*(.+)\s
                (?:^.+\s)*?
                (?:^(?:\|\s*[+]\sLanguage:\s*(.+)\s|\|\s*[+]\sA\strack$))
            """,output)
        return results
    return ()

def extract(filename,tracks):
    """  tracks => ((number,type,codec),... ) """
    cmd = ['mkvextract','tracks',filename]

    new_names = []
    for track in tracks:
        name = get_filename(new_names,filename,track[1],track[2])
        new_names.append(name)
        cmd.append("%s:%s"%(track[0],name))

    ps = Popen(cmd,stdout=PIPE,stderr=STDOUT)
    output = ps.communicate()[0]
    if ps.returncode == 0:
        return 0
    #print output
    return output

if __name__ == '__main__':
    file = sys.argv[1]
    tracks = get_tracks(file)
    if tracks:
        extract(file,tracks)

Le script est simple, donc pas grand chose à dire si ce n’est :
* l’utilisation de Popen() pour éxécuter commande: la sortie standard/d’erreur peut être redirigée dans un fichier, un pipe,etc.  On passe la commande sous forme de liste (par ex. pour un « ls -l« , on passera ["ls","-l"])

* dans la regex , j’utilise (?:…) Ça permet de matcher l’expression régulière mais sans rien retourner/capturer

* la fonction get_filename() n’est pas très jolie, mais j’ai pas réussi à faire mieux

Et j’y ai donc rajouté un petit gui (en PyQt), qui permet de choisir quels tracks on veut extraire. Il est fait un peu à l’arrache (j’aurais pu mettre ma QTableWidget dans un autre fichier)

Le voici :

* fichier qt_thread.py :

from mkv import extract
from PyQt4.QtCore import QEvent, QThread
from PyQt4.QtGui import QApplication

class MyEvent(QEvent):
    """ """

    def __init__(self,elements):
        """ """
        QEvent.__init__(self,QEvent.User)
        self.elements = elements

class MyThread(QThread):
    """ """
    def __init__(self,receiver,filename,tracks):
        QThread.__init__(self)
        self.receiver = receiver
        self.filename = filename
        self.tracks = tracks

    def run(self):
        """ """
        extract(self.filename,self.tracks)
        QApplication.postEvent(self.receiver,MyEvent(self.tracks))
et gui.py :
#-*- coding:utf-8 -*-
from mkv import get_tracks
from qt_thread import MyThread
from PyQt4.QtCore import  Qt, SIGNAL
from PyQt4.QtGui import *
import os
import sys

class MainWindow(QMainWindow):
    """ La fenêtre principale"""

    def __init__(self):
        """ """
        QMainWindow.__init__(self)
        self.setWindowTitle(u"mkvextract gui")
        self.resize(430,600)
        self.draw()
        self.center()
        self.thread = None
        self.filename = None

    def draw(self):
        """ """
        main_widget = QWidget(self)
        main_layout = QGridLayout(main_widget)
        self.open_button = QPushButton(u"Choisir un .mkv")
        self.extract_button = QPushButton(u"Extraire")
        self.extract_button.setDisabled(True)
        self.info_label = QLabel()
        self.info_label.setAlignment(Qt.AlignCenter)
        self.table_widget = QTableWidget(main_widget)
        self.draw_table()
        main_layout.addWidget(self.open_button,0,0)
        main_layout.addWidget(self.info_label,0,1)
        main_layout.addWidget(self.table_widget,1,0,2,0)
        main_layout.addWidget(self.extract_button,2,0,1,2)
        self.setCentralWidget(main_widget)
        self.connect(self.open_button,SIGNAL("clicked()"),self.open_file)
        self.connect(self.extract_button,SIGNAL("clicked()"),self.extract)

    def draw_table(self):
        """ """
        self.table_widget.setSelectionBehavior( QAbstractItemView.SelectRows )#sélectionne toute la ligne
        self.table_widget.setColumnCount(4)
        self.table_widget.setHorizontalHeaderItem(0,QTableWidgetItem(u"ID"))
        self.table_widget.setHorizontalHeaderItem(1,QTableWidgetItem(u"Type"))
        self.table_widget.setHorizontalHeaderItem(2,QTableWidgetItem(u"Codec"))
        self.table_widget.setHorizontalHeaderItem(3,QTableWidgetItem(u"Code Langue"))
        self.table_widget.verticalHeader().hide()# cache les numéros de lignes
        self.table_widget.setEditTriggers(QAbstractItemView.NoEditTriggers)#pas d'inline editing

    def add_row(self,element):
        """ """
        count = self.table_widget.rowCount()
        self.table_widget.insertRow(count)
        self.table_widget.setItem(count,0,QTableWidgetItem(element[0]))
        self.table_widget.setItem(count,1,QTableWidgetItem(element[1]))
        self.table_widget.setItem(count,2,QTableWidgetItem(element[2]))
        self.table_widget.setItem(count,3,QTableWidgetItem(element[3]))

    def center(self):
        """ """
        screen = QDesktopWidget().screenGeometry()
        size =  self.geometry()
        self.move((screen.width()-size.width())/2, (screen.height()-size.height())/2)

    def open_file(self):
        """ """
        file_dialog = QFileDialog(self,caption=u"Choisissez un .mkv",filter=u"Conteneur(*.mkv)")
        file_dialog.setFileMode(QFileDialog.ExistingFile)#pour sélectionner un seul fichier
        file_dialog.setViewMode(QFileDialog.Detail) # vue "détails"
        if file_dialog.exec_():
            self.filename = file_dialog.selectedFiles()[0]
            self.info_label.setText(os.path.split(unicode(self.filename.toUtf8()))[-1])
            #pas de thread vu que ca va être assez rapide
            tracks = get_tracks(self.filename)
            for element in tracks:
                self.add_row(element)
            self.extract_button.setDisabled(False)

    def customEvent(self,event):
        """ """
        self.open_button.setDisabled(False)
        self.info_label.setText(u"Extraction terminée")

        self.table_widget.clearSelection()
        count = self.table_widget.rowCount()
        for i in range(count-1,-1,-1):
            self.table_widget.removeRow(i)
        self.filename = None
        self.table_widget.setDisabled(False)

    def extract(self):
        """ """
        selected = self.table_widget.selectedItems()
        if selected:
            tracks = []
            for i in range(0,len(selected),4):
                tracks.append([ unicode(item.text().toUtf8()) for item in selected[i:i+4] ])

            self.extract_button.setDisabled(True)
            self.open_button.setDisabled(True)
            self.table_widget.setDisabled(True)
            self.info_label.setText(u"Extraction en cours")
            self.thread = MyThread(self,unicode(self.filename.toUtf8()),tracks)
            self.thread.start()

if __name__ == '__main__':
    appli = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(appli.exec_())
Le code est lui aussi assez bateau, 2 petites choses :
* j’utilise QThread/postEvent pour threader l’appel de la commande mkvextract
* setEditTriggers(QAbstractItemView.NoEditTriggers) pour ne pas autoriser l’inline editing de la table Et puis voilà . L’archive :  mkvextract_gui.py.tar Et une petite vidéo du gui (assez pourri): [hana-flv-player video="http://qui-saura.fr/wp-content/uploads/css/out.flv" width="400" height="330" description="GUI mkvextract" player="4" autoload="true" autoplay="false" loop="false" autorewind="true" /]