diff --git a/src/domain/__init__.py b/src/domain/__init__.py new file mode 100644 index 0000000..0435d86 --- /dev/null +++ b/src/domain/__init__.py @@ -0,0 +1 @@ +"""Domain package.""" diff --git a/src/domain/backend.py b/src/domain/backend.py index 287f240..e639d4f 100644 --- a/src/domain/backend.py +++ b/src/domain/backend.py @@ -1,41 +1,45 @@ -import os -from typing import List, Generator, Dict +"""Backend that interacts with tafrigh.""" +from platform import system +from subprocess import Popen +from typing import Any -from PySide6.QtCore import QObject, Signal, Property, Slot, QThreadPool -from tafrigh import farrigh, Config +from PySide6.QtCore import Property, QObject, QThreadPool, Signal, Slot +from tafrigh import Config, farrigh +from domain.config import AppConfig, CaseSensitiveConfigParser from domain.progress import Progress -from domain.config import CaseSensitiveConfigParser, AppConfig -from domain.threadpool import WorkerSignals, Worker - -os.system('cls') +from domain.threadpool import Worker, WorkerSignals # BACKEND class Backend(QObject): + + """Backend object.""" + result = Signal(dict) progress = Signal(int, int) error = Signal(tuple) finish = Signal() - def __init__(self, parent=None) -> None: + def __init__(self, parent: QObject | None = None) -> None: + """Initialize backend object.""" super().__init__(parent=parent) self.signals = WorkerSignals() self.threadpool = QThreadPool() self._is_running = False - self._urls = [] + self._urls: list[str] = [] - def on_error(self, error) -> None: + def on_error(self, error: tuple[str, int, str]) -> None: self.error.emit(error) self._is_running = False - def on_result(self, result) -> None: + def on_result(self, result: dict[str, Any]) -> None: self.result.emit(result) def on_progress(self, progress: Progress) -> None: self.progress.emit(progress.value, progress.remaining_time) - def on_finish(self): + def on_finish(self) -> None: self.finish.emit() @Slot() @@ -48,43 +52,52 @@ def start(self) -> None: self.threadpool.start(worker) @Property(list) - def urls(self) -> List[str]: + def urls(self) -> list[str]: return self._urls - @urls.setter - def urls(self, value: List[str]): - self._urls = list(map(lambda x: x.replace("file:///", ""), value)) + @urls.setter # type: ignore[no-redef] + def urls(self, value: list[str]): + self._urls = [x.replace("file:///", "") for x in value] - def run(self, *args, **kwargs) -> Generator[Dict[str, int], None, None]: + def run(self, *args: Any, **kwargs: Any) -> Any: app_config: AppConfig = CaseSensitiveConfigParser.read_config() config = Config( + *args, urls_or_paths=self.urls, playlist_items="", verbose=False, skip_if_output_exist=True, model_name_or_path=app_config.whisper_model, - task='transcribe', + task="transcribe", language=app_config.convert_language, use_whisper_jax=False, use_faster_whisper=True, beam_size=5, - ct2_compute_type='default', - - wit_client_access_token=app_config.wit_convert_key if app_config.is_wit_engine else None, + ct2_compute_type="default", + wit_client_access_token=app_config.wit_convert_key + if app_config.is_wit_engine + else None, max_cutting_duration=int(app_config.max_part_length), - min_words_per_segment=app_config.word_count, save_files_before_compact=False, save_yt_dlp_responses=app_config.download_json, output_sample=0, output_formats=app_config.get_output_formats(), output_dir=app_config.save_location.replace("file:///", ""), + **kwargs, ) return farrigh(config) - @Slot(str) @staticmethod - def open_folder(path: str): - os.startfile(path) + @Slot(str) + def open_folder(path: str) -> None: + if system() == "Windows": + from os import startfile # type: ignore[attr-defined] + + startfile(path) # noqa: S606 + elif system() == "Darwin": + Popen(["open", path], shell=False) # noqa: S603, S607 + else: + Popen(["xdg-open", path], shell=False) # noqa: S603, S607 diff --git a/src/domain/config.py b/src/domain/config.py index 157384e..adb05eb 100644 --- a/src/domain/config.py +++ b/src/domain/config.py @@ -1,12 +1,20 @@ +"""A module contains the configuration parser. + +It is used to read the settings.ini file and convert it to a pydantic model. +""" import re -from pydantic import BaseModel from configparser import ConfigParser -from typing import List, Type, TypeVar +from typing import TypeVar, cast + +from pydantic import BaseModel -T = TypeVar('T', bound=BaseModel) +T = TypeVar("T", bound=BaseModel) class AppConfig(BaseModel): + + """App configuration model.""" + download_json: bool convert_engine: str save_location: str @@ -21,36 +29,33 @@ class AppConfig(BaseModel): export_srt: bool export_txt: bool - def get_output_formats(self) -> List[str]: - formats = { - 'srt': self.export_srt, - 'vtt': self.export_vtt, - 'txt': self.export_txt - } + def get_output_formats(self) -> list[str]: + formats = {"srt": self.export_srt, "vtt": self.export_vtt, "txt": self.export_txt} return [key for key, value in formats.items() if value] class CaseSensitiveConfigParser(ConfigParser): - def optionxform(self, optionstr) -> str: - return optionstr + + """A case sensitive config parser.""" + + def optionxform(self, option_str: str) -> str: + return option_str @staticmethod def camel_to_snake(camel_case: str) -> str: - snake_case = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', camel_case) + snake_case = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", camel_case) return snake_case.lower() @classmethod def read_config( cls, - model: Type[T] = AppConfig, - filename: str = 'settings.ini', - default_section: str = 'config' + model: type[T] = AppConfig, + filename: str = "settings.ini", + default_section: str = "config", ) -> T: parser = cls(default_section=default_section) parser.read(filename) - data = { - cls.camel_to_snake(key): value for key, value in parser.defaults().items() - } + data = {cls.camel_to_snake(key): value for key, value in parser.defaults().items()} - return model(**data) + return cast(T, model(**data)) diff --git a/src/domain/progress.py b/src/domain/progress.py index 4e6f436..bfc7ced 100644 --- a/src/domain/progress.py +++ b/src/domain/progress.py @@ -1,7 +1,11 @@ +"""Progress domain model.""" from dataclasses import dataclass @dataclass class Progress: + + """Progress data class.""" + value: float = 0.0 - remaining_time: float = None + remaining_time: float | None = None diff --git a/src/domain/threadpool.py b/src/domain/threadpool.py index d75571a..46c696c 100644 --- a/src/domain/threadpool.py +++ b/src/domain/threadpool.py @@ -1,29 +1,33 @@ -from typing import Callable, Generator, Dict - -from PySide6.QtCore import Signal, QObject, QRunnable -import traceback +"""Custom thread class and signals emitted by worker threads.""" import sys +import traceback +from collections.abc import Callable, Generator +from typing import Any + +from PySide6.QtCore import QObject, QRunnable, Signal from domain.progress import Progress -# THREAD SIGNALS class WorkerSignals(QObject): + + """Signals emitted by worker threads.""" + finished = Signal() error = Signal(tuple) result = Signal(dict) progress = Signal(Progress) -# CUSTOM THREAD CLASS class Worker(QRunnable): + + """Custom thread class.""" + def __init__( - self, - func: Callable[..., Generator[Dict[str, int], None, None]], - *args, - **kwargs + self, func: Callable[..., Generator[dict[str, int], None, None]], *args: Any, **kwargs: Any ) -> None: - super(Worker, self).__init__() + """Initialize worker object.""" + super().__init__() self.func = func self.args = args self.kwargs = kwargs @@ -34,10 +38,13 @@ def run(self) -> None: results = self.func(args=self.args, kwargs=self.kwargs) for result in results: progress_value, remaining_time = result["progress"], result["remaining_time"] - progress = Progress(value=progress_value, remaining_time=remaining_time, ) + progress = Progress( + value=progress_value, + remaining_time=remaining_time, + ) self.signals.progress.emit(progress) self.signals.result.emit(result) - except Exception: + except Exception: # noqa: BLE001 traceback.print_exc() exc_type, value = sys.exc_info()[:2] self.signals.error.emit((exc_type, value, traceback.format_exc())) diff --git a/src/qml/components/ConvertPage.qml b/src/qml/components/ConvertPage.qml index 0bd2540..a84813b 100644 --- a/src/qml/components/ConvertPage.qml +++ b/src/qml/components/ConvertPage.qml @@ -8,7 +8,7 @@ import "convert" Rectangle { id: root //audioUrls must be in jsonstring format - signal convertRequested(list urls) + signal convertRequested(list urls) color: theme.background @@ -39,7 +39,6 @@ Rectangle { topMargin: 64 rightMargin: 75 } - } Image { @@ -54,7 +53,6 @@ Rectangle { topMargin: title.anchors.topMargin + 48 rightMargin: 60 } - } Text { @@ -74,7 +72,6 @@ Rectangle { topMargin: 6 rightMargin: 10 } - } Text { @@ -92,14 +89,15 @@ Rectangle { right: title.right topMargin: 16 } - } AcceptTasks { id: acceptTasks - onAddedNewAudio: (audio) => { - audioFilesModel.append({'file': audio}); + onAddedNewAudio: audio => { + audioFilesModel.append({ + "file": audio + }); } anchors { @@ -109,7 +107,6 @@ Rectangle { rightMargin: 72 leftMargin: 72 } - } Rectangle { @@ -144,9 +141,7 @@ Rectangle { audioFilesModel.remove(index); // Remove the audio file from the model } } - } - } RowLayout { @@ -166,31 +161,29 @@ Rectangle { text: qsTr("البــــدء") Layout.fillWidth: true onClicked: { - var listData = [] + var listData = []; for (var i = 0; i < audioFilesModel.count; i++) { - var item = audioFilesModel.get(i) - listData.push(item.file) + var item = audioFilesModel.get(i); + listData.push(item.file); } - - root.convertRequested(listData) + root.convertRequested(listData); } } CustomButton { - text: qsTr("الغــاء") + text: qsTr("إلغــاء") backColor: theme.card Layout.fillWidth: true onClicked: audioFilesModel.clear() } } - Connections { target: backend enabled: root.visible function onFinish() { - audioFilesModel.clear() + audioFilesModel.clear(); } } } diff --git a/src/qml/components/ProcessPage.qml b/src/qml/components/ProcessPage.qml index 6f83108..2448cdb 100644 --- a/src/qml/components/ProcessPage.qml +++ b/src/qml/components/ProcessPage.qml @@ -106,7 +106,7 @@ Rectangle { } function onFinish() { - progressBar.value = 0 + progressBar.value = 0; } } } diff --git a/src/qml/components/SideBar.qml b/src/qml/components/SideBar.qml index ff92df1..806f27b 100644 --- a/src/qml/components/SideBar.qml +++ b/src/qml/components/SideBar.qml @@ -6,7 +6,7 @@ Rectangle { property int index: 0 signal sidebarButtonClicked(int index) - signal folderClick() + signal folderClick color: theme.primary width: 80 @@ -80,7 +80,6 @@ Rectangle { hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: folderClick() - } } diff --git a/src/qml/components/convert/CircularProgressBar.qml b/src/qml/components/convert/CircularProgressBar.qml index 78283be..2a3f208 100644 --- a/src/qml/components/convert/CircularProgressBar.qml +++ b/src/qml/components/convert/CircularProgressBar.qml @@ -1,7 +1,6 @@ import QtQuick 6.4 import QtQuick.Controls 6.4 - Item { id: progressBar property int value: 0 @@ -15,7 +14,7 @@ Item { onValueChanged: { canvas.degree = value; - labelValue = value + labelValue = value; } Canvas { @@ -34,7 +33,6 @@ Item { const radius = Math.min(width, height) * 0.4; const startAngle = -90; // Start angle at the top (-90 degrees) const endAngle = startAngle + (degree / 100) * 360; // Calculate the end angle based on progress - const ctx = getContext("2d"); ctx.clearRect(0, 0, width, height); // Clear the canvas @@ -52,14 +50,14 @@ Item { ctx.stroke(); } - Behavior on degree { + Behavior on degree { NumberAnimation { duration: progressBar.animationDuration } } } - Row { + Row { anchors.centerIn: parent Text { text: progressBar.labelValue @@ -76,7 +74,7 @@ Item { } } - Behavior on labelValue { + Behavior on labelValue { NumberAnimation { duration: progressBar.animationDuration } diff --git a/src/qml/main.qml b/src/qml/main.qml index 85c1778..268fc46 100644 --- a/src/qml/main.qml +++ b/src/qml/main.qml @@ -30,18 +30,16 @@ ApplicationWindow { anchors.fill: parent hoverEnabled: true - onPressed: (mouse) => { + onPressed: mouse => { clickPos = Qt.point(mouse.x, mouse.y); } - onMouseXChanged: (mouse) => { + onMouseXChanged: mouse => { if (mouse.buttons === Qt.LeftButton) mainWindow.x += (mouse.x - clickPos.x); - } - onMouseYChanged: (mouse) => { + onMouseYChanged: mouse => { if (mouse.buttons === Qt.LeftButton) mainWindow.y += (mouse.y - clickPos.y); - } } @@ -52,9 +50,9 @@ ApplicationWindow { anchors.bottom: parent.bottom anchors.left: parent.left onFolderClick: { - backend.open_folder(settingsPage.saveLocation) + backend.open_folder(settingsPage.saveLocation); } - onSidebarButtonClicked: (index) => { + onSidebarButtonClicked: index => { stackLayout.currentIndex = index; } } @@ -70,17 +68,17 @@ ApplicationWindow { } ConvertPage { - onConvertRequested: (urls) => { - backend.urls = urls - backend.start() - stackLayout.currentIndex = 2 + onConvertRequested: urls => { + backend.urls = urls; + backend.start(); + stackLayout.currentIndex = 2; } } SettingsPage { id: settingsPage - onThemeChanged: (state) => { + onThemeChanged: state => { isLightTheme = !state; } } @@ -103,7 +101,7 @@ ApplicationWindow { enabled: mainWindow.visible function onFinish() { - timer.start() + timer.start(); } } @@ -115,7 +113,7 @@ ApplicationWindow { repeat: false onTriggered: { timer.stop(); - stackLayout.currentIndex = 0 + stackLayout.currentIndex = 0; } } }