How To Create a File Converter in Python

Ampofo Amoh - Gyebi
11 min readFeb 24, 2022

Let's create a basic converter app, which takes an mp4 file and then extract the music out of it to create an mp3 file.

Please this article assumes that you’ve read my previous article here

Create Project Folders

  • Create a folder anywhere on your computer and name it say: converter
  • Then create another folder in it and call it UI
  • Next create a file besides it and name the file main.py.
  • Inside the UI folder, create two files, main.qml and AddComp.qml (The AddComp.qml begins with a capital letter because we want to call it inside another Qml file)
  • So now your directory structure should like something like this:

And inside your UI folder

- converter
- UI
- AddComp.qml
- main.qml
- main.py

Install Pyffmpeg

We will be using pyffmpeg library to handle the conversion.

In the terminal do:

>>> pip install pyffmpeg

The UI

Lets focus on the UI before moving on to the backend part in python.

Open the main.qml file inside the UI folder and then populate it with this code.

UI/main.qml

import QtQuick
import QtQuick.Controls.Basic
import QtQuick.Layouts


ApplicationWindow {
visible: true
width: 800
height: 500
title: "Converter"

property QtObject converter
property string inputFile


AddComp { id: addcomp }

StackView {
id: stack
anchors.fill: parent
initialItem: addcomp
}


}

The above code we have created properties for converter andinputFile which we will use later on. And then there is a Component AddComp with id: addComp. There is a StackView that fills the Window and it has the AddComp as its initial item.

Next open the AddComp.qml file and populate that one too with the code below

UI/AddComp.qml

import QtQuick
import QtQuick.Controls.Basic
import QtQuick.Layouts
import QtQuick.Dialogs

Component {

Rectangle {

ColumnLayout {
anchors.fill: parent
spacing: 0

Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 128
color: "purple"

Text {
anchors {
bottom: parent.bottom
left: parent.left
margins: 12
}

text: "Convert To MP3"
font.pixelSize: 16
color: "white"
}

}


Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: "#1e1e1e"

ColumnLayout {
anchors.fill: parent

Rectangle {
id: fn_cont
Layout.fillWidth: true
Layout.preferredHeight: 94
Layout.margins: 24
Layout.alignment: Qt.AlignTop
radius: 12
border.color: "#eee"
color: "#2f2f2f"
visible: true // line 51

Text {
id: fn
padding: 8
width: parent.width
height: parent.height
verticalAlignment: Text.AlignVCenter
text: "C:/folder/filename"
font.pixelSize: 12
color: "white"
}

MouseArea {
id: area
anchors.fill: parent
hoverEnabled: true
onClicked: stack.push(progresscomp) // line 69​
onEntered: parent.border.color = "gold"
onExited: parent.border.color = "#eee"

}

}

Button {
Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter
Layout.bottomMargin: 8
text: "Add"
onClicked: f_dialog.open()

background: Rectangle {
implicitWidth: 100
implicitHeight: 32
color: parent.hovered ? Qt.lighter("gold") : "gold"
radius: 4
}

}

}


}


}


FileDialog {
id: f_dialog
nameFilters: ['Video files(*.mp4)']

onAccepted: {
inputFile = f_dialog.currentFile
fn.text = inputFile
fn_cont.visible = true
}


}

}

}

In the code above, there is a Component to ensure that the items even though are visual do not display unless they have been assigned a View. . The nameFilters: ['Video files(\*.mp4)'] code of the FileDialog ensures that only files with .mp4 extension can be selected in the file picker. And then the remaining code just creates a card strip and a customized button. The onAccepted signal assigns the inputFile which we declared in main.qml to the current file the user has selected in the file picker.

Connect to Python

Let's connect the UI to Python

Open the main.py file and populate it with this code

main.py

import sys

from PyQt6.QtGui import QGuiApplication
from PyQt6.QtQml import QQmlApplicationEngine
from PyQt6.QtQuick import QQuickWindow


QQuickWindow.setSceneGraphBackend('software')

app = QGuiApplication(sys.argv)

engine = QQmlApplicationEngine()
engine.load('UI/main.qml')
engine.quit.connect(app.quit)

sys.exit(app.exec())

The code above loads our UI (UI/main.qml) into the Qml Application Engine.

Now open your terminal or command prompt and navigate to where you have your project files

>>> cd converter

Now from your terminal run the python file

>>> python main.py

Now you will see something like this:

The card strip that holds the text that says C:/folder/filename is set to visible true, possibly on line 51. This is so that you can see it.

But what should happen is; if a user adds an mp4 file using the File picker, then the card strip shows up and the text will have the full path of the file. So change it to say visible: false

UI/AddComp.qml

Rectangle {
id: fn_cont
...
border.color: "#eee"
color: "#2f2f2f"
visible: false // line 51

Text {
id: fn
padding: 8
...
text: "C:/folder/filename"
font.pixelSize: 12
color: "white"
}
...
}

Now it is not showing, just the add button is. In the onAccepted signal of the FileDialog we set it to visible, to show up after a user selects an mp4 file.

Next, add a Component to show a progress window.

Inside the UI folder, create a new file and name it ProgressComp.qml (should begin with a capital letter).

So your directory structure should look like this.

- converter
- UI
- AddComp.qml
- main.qml
- ProgressComp.qml
- main.py

Open the ProgressComp.qml file and populate it with this code

UI/ProgressComp.qml

import QtQuick
import QtQuick.Controls.Basic
import QtQuick.Layouts

Component {

Rectangle {

ColumnLayout {
anchors.fill: parent
spacing: 0

Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 128
color: "purple"

Text {
anchors {
bottom: parent.bottom
left: parent.left
margins: 12
}

text: "Convert To MP3"
font.pixelSize: 16
color: "white"
}
}


Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: "#1e1e1e"

ColumnLayout {
anchors.fill: parent

ProgressBar {
Layout.fillWidth: true
indeterminate: true
Layout.margins: 24
}

}


}


}

Component.onCompleted: {
converter.convert(inputFile)

}

}



}

The code above shows a progressBar that is set to indeterminate for now, but we will change it to show real progress in the future. It also runs the convert method of the converter class (even though we are yet to create this) when the component loads up.

Add this to the main.qml so it can be called easily by the stack

UI/main.qml

...

AddComp { id: addcomp }
ProgressComp { id: progresscomp }

StackView {
id: stack
anchors.fill: parent
initialItem: addcomp
}

...

You will realize that on line 69 of the AddComp.qml file we are pushing the progresscomp to the stack on click of the card strip.

UI/AddComp.qml

...
onClicked: stack.push(progresscomp) // line 69
...

That means that the UI will flip to the ProgressComp page. This is why the card strip is invisible till it has a proper file URL set. Because the convert method is called when the ProgressComp loads up, it will cause errors if the user mistakenly clicks on it and cause the convert method to run while no URL has been set.

While the file is converting we will show the user a progress bar, and then when it is done we will show an information page, that just says done, so the user knows, the conversion is done.

Let's create a file besides the ProgressComp.qml and call it InfoComp.qml.

So your file structure should look something like this:

- converter
- UI
- AddComp.qml
- InfoComp.qml
- main.qml
- ProgressComp.qml
- main.py

Now open the InfoComp.qml and populate it with this code.

UI/InfoComp.qml

import QtQuick
import QtQuick.Controls.Basic
import QtQuick.Layouts

Component {

Rectangle {

ColumnLayout {
anchors.fill: parent
spacing: 0

Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 128
color: "purple"

Text {
anchors {
bottom: parent.bottom
left: parent.left
margins: 12
}

text: "Convert To MP3"
font.pixelSize: 16
color: "white"
}
}


Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: "#1e1e1e"

Rectangle {
anchors.centerIn: parent
width: 200
height: 200
color: "transparent"
border.color: "green"

Text {
anchors.centerIn: parent
text: "Done!"
color: "green"
}

}


}


}


}



}

The code above is similar to that of all the other Components but here we have a Text with a green rectangular border that just says Done!.

Let's add this also to the main.qml

UI/main.qml

...

AddComp { id: addcomp }
InfoComp { id: infocomp }
ProgressComp { id: progresscomp }

StackView {
id: stack
anchors.fill: parent
initialItem: addcomp
}

...

The InfoComp will only be shown after the file has been converted so we have to signal back when we are done with the conversion.

Now the UI is done.

Run the code

>>> python main.py

And you should see something like this

Click the Add button and load up an mp4 file and you should see the card strip with the URL of the mp4 file starting with 'file:///'. This file protocol is opposed to other protocols such as 'https://' and 'ftp://'.

Create the Converter Class

Now let's create a class to handle the conversion

Let's put our conversion code in a new file so that we can separate it from the boot-up code, and our code will look cleaner and much readable.

Create a new file name func.py.

Your directory structure should look something like this:

- converter
- UI
- AddComp.qml
- InfoComp.qml
- main.qml
- ProgressComp.qml
- func.py
- main.py

Now open the func.py and populate it with this code

func.py

import os

from PyQt6.QtCore import QObject, pyqtSlot
from pyffmpeg import FFmpeg


class Converter(QObject):

def __init__(self, parent=None):
QObject.__init__(self)

@pyqtSlot(str)
def convert(self, input_file: str):
print('Inside Convert function')
input_file = input_file.replace('file:///', '')
audio_name = os.path.splitext(input_file)[0] + '.mp3'

ff_obj = FFmpeg()
ff_obj.convert(input_file, audio_name)
print('Done: ', audio_name)

The above code creates a slot known as convert. It takes an input_file, and then creates an output audio filename by just changing the file extension. Then we call FFmpeg and then call the convert method of FFmpeg and pass the input and output files to it. This is all it takes to make a basic conversion of a file from one format to another. But we are focusing on extracting the mp3 from mp4for now.

Make the Converter class available in main.py.

Also, we set the Qml property converter to our Converter Object.

main.py

import sys

from PyQt6.QtGui import QGuiApplication
from PyQt6.QtQml import QQmlApplicationEngine
from PyQt6.QtQuick import QQuickWindow

from func import Converter


QQuickWindow.setSceneGraphBackend('software')

app = QGuiApplication(sys.argv)

engine = QQmlApplicationEngine()
engine.load('UI/main.qml')
convert = Converter()
engine.rootObjects()[0].setProperty('converter', convert)

engine.quit.connect(app.quit)

sys.exit(app.exec())

In the code above we import our Converter class from our func module which is right next to the main.py. We create an object convert out of the Converter class and then set that to the Qml property converter which is declared in UI/main.qml using setProperty.

Run this with:

>>> python main.py

When the UI pops up click the Add button to add a video file.

Next click the Card Strip that shows the file URL.

You realize that even though we have the converter.convert(inputFile) in the ProgressComp.qml file and the inside converting function log message was printed, meaning the python code run, we didn’t see the ProgressComp and the UI stops responding if you attempted to interact with it.

This is the problem that threading solves.

The Fix

Let's incorporate threading into the Converter class.

Qt has its own threading classes, Python also has threading classes in its Standard Library. But wherein there is a standard library available, we use that one instead. So we will be using Python's own Threading class.

In the func.py file update the file to use threading when calling the convert method.

func.py

import os
import threading

from PyQt6.QtCore import QObject, pyqtSlot
from pyffmpeg import FFmpeg


class Converter(QObject):

def __init__(self, parent=None):
QObject.__init__(self)

@pyqtSlot(str)
def convert(self, input_file: str):
c_thread = threading.Thread(target=self._convert,
args=[input_file])
c_thread.daemon = True
c_thread.start()

def _convert(self, input_file: str):
print('Inside Convert function')
input_file = input_file.replace('file:///', '')
audio_name = os.path.splitext(input_file)[0] + '.mp3'

ff_obj = FFmpeg()
ff_obj.convert(input_file, audio_name)
print('Done: ', audio_name)

In the above code, we've moved the old convert code to a new underscore method. Notice this is not a Slot. And the old convert method which is the Qt Slot has been turned into a thread creator and starter. When it gets called by Qml it creates a Thread with the underscore method as the target and then starts it. The daemon set to True means that the newly created Thread will exit immediately the main Application (main thread) exits. This is good code to ensure that we don't have unmanaged threads still running on the user's computer even after the application has exited.

Now if you run it

>>> python main.py

You'll find out that the threading solved the freezing problem and that this time you see the ProgressComp show up, still in an indeterminate state, and that the UI is responsive. But after the conversion, we still need to make the final push to the InfoComp.

For that, we will use Signals.

In the func.py module, incorporate Signals.

func.py

import os
import threading

from PyQt6.QtCore import QObject, pyqtSlot, pyqtSignal
...


class Converter(QObject):

def __init__(self, parent=None):
QObject.__init__(self)

convertCompleted = pyqtSignal(str, arguments=['convert'])

@pyqtSlot(str)
def convert(self, input_file: str):
...

def _convert(self, input_file: str):
...

ff_obj = FFmpeg()
ff_obj.convert(input_file, audio_name)
print('Done: ', audio_name)
self.convertCompleted.emit('') # line 30

In the code above, we create a signal by name convertCompleted. This signal is emitted by the _convert method on possibly line 30 when conversion is done.

Now in the main.qml file, create the signal handler

UI/main.qml

import QtQuick
...


ApplicationWindow {
...


...

StackView {
id: stack
anchors.fill: parent
initialItem: addcomp
}


Connections {

target: converter

function onConvertCompleted() {
stack.push(infocomp)
}

}

}

In the code above, we add Connections and target the converter property which has been set in python to the convert object. we have also created a signal handler the signal convertCompleted . When we receive such a signal we then make the final push of infoComp to the Stack.

Now the whole thing is done. And the importance and implementation of Threading are understood by You.

Going Further

Let's now use progress information to update the progress bar.

Open the func.py and update the code as below.

func.py

import os
...


class Converter(QObject):

...

convertCompleted = pyqtSignal(str, arguments=['convert'])
progressUpdated = pyqtSignal(int, arguments=['progress'])

...

def _convert(self, input_file: str):
...

ff_obj = FFmpeg()
ff_obj.report_progress = True
ff_obj.onProgressChanged = self.updateProgress # line 30
ff_obj.convert(input_file, audio_name)
print('Done: ', audio_name)
self.convertCompleted.emit('')

def updateProgress(self, progress):
self.progressUpdated.emit(progress)

In the code above, we create a new signal progressUpdated which emits an int. Then we create a method updateProgress that emits the signal. The reason is that FFMpeg will require a function it can call and pass in the progress percent whenever the progress changes. So you can also see that on line 30, we set FFmpeg's onProgressChanged to our updateProgress method. Also, note that we need to set report_progress to True before we can get progress information.

Next, in the main.qml receive the signal progressUpdated

UI/main.qml

import QtQuick
import QtQuick.Controls.Basic
import QtQuick.Layouts


ApplicationWindow {
visible: true
...

property string inputFile
property int progressPercent

...


Connections {

target: converter

...

function onProgressUpdated(percent) {
progressPercent = percent;
}

}

}

In the code above we have created a signal handler onProgressUpdated that takes the int emitted by progressUpdated as percent and then set it as progressPercent

Now update the progressComp to use the progressPercent

UI/progressComp.qml

import QtQuick
import QtQuick.Controls.Basic
import QtQuick.Layouts

Component {

Rectangle {

ColumnLayout {

...
color: "#1e1e1e"

ColumnLayout {
anchors.fill: parent

ProgressBar {
Layout.fillWidth: true
Layout.margins: 24
from: 1
to: 100
value: progressPercent
}

}

...


}


}



}

In the code above, we have set from and to values for ProgressBar and have removed the indeterminate property. We have also set the value to progressPercent.

All Done

The complete main.py file

The complete func.py file

.

More resources can be found on:

--

--