How To Create a File Converter in Python
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:
- PyQt/PySide —YouTube, Udemy, Qt official docs, Riverbank Computing
- QML — evileg.com, Qt Official docs
- Software Engineering Advice