diff --git a/src/recoder/models.py b/src/recoder/models.py index 529c74c..7c56f48 100644 --- a/src/recoder/models.py +++ b/src/recoder/models.py @@ -8,6 +8,12 @@ class FileStatus(GObject.GEnum): DONE = 3 class FileItem(GObject.GObject): + __gtype_name__ = 'FileItem' + file = GObject.Property(type=Gio.File) progress = GObject.Property(type=int, minimum=0, maximum=100, default=0) status = GObject.Property(type=FileStatus, default=FileStatus.WAITING) + + def __init__(self, file: Gio.File): + super().__init__() + self.file = file diff --git a/src/recoder/transcoder_worker.py b/src/recoder/transcoder.py similarity index 90% rename from src/recoder/transcoder_worker.py rename to src/recoder/transcoder.py index e381a40..f108ce1 100644 --- a/src/recoder/transcoder_worker.py +++ b/src/recoder/transcoder.py @@ -4,11 +4,13 @@ import subprocess import re from gi.repository import GLib -class TranscoderWorker: +from recoder.models import FileStatus, FileItem + +class Transcoder: TIME_RE = re.compile(r"time=(\d+):(\d+):(\d+)\.(\d+)") - def __init__(self, files, progress_callback=None, done_callback=None, file_done_callback=None): - self.files = files + def __init__(self, file_items, progress_callback=None, done_callback=None, file_done_callback=None): + self.file_items = file_items self.progress_callback = progress_callback self.done_callback = done_callback self.file_done_callback = file_done_callback @@ -23,8 +25,9 @@ class TranscoderWorker: thread.start() def _process_files(self): - total = len(self.files) - for idx, filepath in enumerate(self.files, start=1): + total = len(self.file_items) + for idx, file_item in enumerate(self.file_items, start=1): + filepath = file_item.file.get_path() basename = os.path.basename(filepath) self._update_progress(f"Starting {basename}", (idx - 1) / total) diff --git a/src/recoder/ui.py b/src/recoder/ui.py index e91418e..d566844 100644 --- a/src/recoder/ui.py +++ b/src/recoder/ui.py @@ -1,39 +1,66 @@ import os import gi -import shutil -import subprocess gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') gi.require_version('Notify', '0.7') -from gi.repository import Gtk, Adw, Gio, Gdk, GLib, Notify + +from gi.repository import Gtk, Gdk, Gio, GLib, Notify from functools import partial -from recoder.transcoder_worker import TranscoderWorker +from recoder.transcoder import Transcoder +from recoder.utils import extract_video_files, notify_done, play_complete_sound +from recoder.models import FileStatus, FileItem class FileEntryRow(Gtk.ListBoxRow): - def __init__(self, path): + def __init__(self, item): super().__init__() - self.path = path - self.icon = Gtk.Image.new_from_icon_name("media-playback-pause-symbolic") - self.label = Gtk.Label(label=os.path.basename(path), xalign=0) + self.item = item + + self.icon = Gtk.Image.new_from_icon_name("object-select-symbolic") + self.label = Gtk.Label(xalign=0, hexpand=True) + self.progress_label = Gtk.Label(xalign=1) + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) hbox.append(self.icon) hbox.append(self.label) + hbox.append(self.progress_label) self.set_child(hbox) - def mark_done(self): - self.icon.set_from_icon_name("emblem-ok-symbolic") - self.label.set_text(f"{os.path.basename(self.path)} - Done") + self.item.connect("notify::status", self.on_status_changed) + self.item.connect("notify::progress", self.on_progress_changed) -class RecoderWindow(): + self.update_display() + def on_status_changed(self, *_): + self.update_display() + + def on_progress_changed(self, *_): + self.update_display() + + def update_display(self): + basename = self.item.file.get_basename() + self.label.set_text(basename) + + match self.item.status: + case FileStatus.WAITING: + self.icon.set_from_icon_name("media-playback-pause-symbolic") + self.progress_label.set_text("Waiting") + case FileStatus.PROCESSING: + self.icon.set_from_icon_name("view-refresh-symbolic") + self.progress_label.set_text(f"{self.item.progress}%") + case FileStatus.DONE: + self.icon.set_from_icon_name("task-complete-symbolic") + self.progress_label.set_text("Done") + case FileStatus.ERROR: + self.icon.set_from_icon_name("dialog-error-symbolic") + self.progress_label.set_text("Error") + + +class RecoderWindow: def __init__(self, application, **kwargs): - - self.files_to_process = [] + self.file_items_to_process = [] self.transcoder = None self.file_rows = {} - # Load UI from resource builder = Gtk.Builder() builder.add_from_resource("/net/jeena/recoder/recoder.ui") @@ -50,7 +77,6 @@ class RecoderWindow(): self.drop_target.connect("drop", self.on_drop) self.drop_target.connect("enter", self.on_drop_enter) self.drop_target.connect("leave", self.on_drop_leave) - self.window.add_controller(self.drop_target) css_provider = Gtk.CssProvider() @@ -75,60 +101,48 @@ class RecoderWindow(): GLib.idle_add(partial(self.process_drop_value, value)) self.overlay.remove_overlay(self.drop_hint) self.progress.set_visible(True) - self.progress.set_fraction(0.0) # optionally reset + self.progress.set_fraction(0.0) self.drop_hint.set_visible(False) return value def process_drop_value(self, value): - if isinstance(value, Gio.File): - uris = [value] - elif isinstance(value, list): - uris = value - else: + file_items = extract_video_files(value) + if not file_items: return False - paths = [] - for file in uris: - path = file.get_path() - if os.path.isdir(path): - for entry in os.scandir(path): - if entry.is_file() and entry.name.lower().endswith((".mp4", ".mov", ".mkv", ".avi")): - paths.append(entry.path) - else: - paths.append(path) - - if not paths: - return False - - # Clear previous listbox rows - children = self.listbox.get_first_child() - while children: - next_child = children.get_next_sibling() - self.listbox.remove(children) - children = next_child + # Clear previous rows + self.clear_listbox() self.file_rows.clear() - # Add new files to listbox with custom FileEntryRow - for path in paths: - row = FileEntryRow(path) + for file_item in file_items: + row = FileEntryRow(file_item) self.listbox.append(row) - self.file_rows[path] = row + self.file_rows[file_item] = row - self.files_to_process = paths + self.file_items_to_process = file_items self.start_transcoding() return False + def clear_listbox(self): + child = self.listbox.get_first_child() + while child: + next_child = child.get_next_sibling() + self.listbox.remove(child) + child = next_child + def start_transcoding(self): if self.transcoder and self.transcoder.is_processing: return + self.window.remove_controller(self.drop_target) + self.progress.set_fraction(0.0) self.progress.set_text("Starting transcoding...") - self.transcoder = TranscoderWorker( - self.files_to_process, + self.transcoder = Transcoder( + self.file_items_to_process, progress_callback=self.update_progress, - done_callback=self.notify_done, + done_callback=self._done_callback, file_done_callback=self.mark_file_done, ) self.transcoder.start() @@ -143,19 +157,9 @@ class RecoderWindow(): row = self.file_rows[filepath] GLib.idle_add(row.mark_done) - def notify_done(self): + def _done_callback(self): self.progress.set_show_text(True) self.progress.set_text("Transcoding Complete!") - self.play_complete_sound() - notification = Notify.Notification.new( - "Recoder", - "Transcoding finished!", - "net.jeena.Recoder" - ) - notification.show() - - def play_complete_sound(self): - if shutil.which("canberra-gtk-play"): - subprocess.Popen(["canberra-gtk-play", "--id", "complete"]) - else: - print("canberra-gtk-play not found.") + play_complete_sound() + notify_done("Recoder", "Transcoding finished!") + self.window.add_controller(self.drop_target) diff --git a/src/recoder/utils.py b/src/recoder/utils.py new file mode 100644 index 0000000..86ece2f --- /dev/null +++ b/src/recoder/utils.py @@ -0,0 +1,39 @@ +import os +import shutil +import subprocess +from typing import Union, List +from gi.repository import Gio, Notify + +from recoder.models import FileItem + +SUPPORTED_EXTENSIONS = (".mp4", ".mov", ".mkv", ".avi") + +def extract_video_files(value: Union[Gio.File, List[Gio.File]]) -> List[FileItem]: + if isinstance(value, Gio.File): + uris = [value] + elif isinstance(value, list): + uris = value + else: + return [] + + files = [] + for file in uris: + path = file.get_path() + if os.path.isdir(path): + for entry in os.scandir(path): + if entry.is_file() and entry.name.lower().endswith(SUPPORTED_EXTENSIONS): + files.append(FileItem(Gio.File.new_for_path(entry.path))) + elif os.path.isfile(path) and path.lower().endswith(SUPPORTED_EXTENSIONS): + files.append(FileItem(file)) + + return files + +def notify_done(title, body): + notification = Notify.Notification.new(title, body, "net.jeena.Recoder") + notification.show() + +def play_complete_sound(): + if shutil.which("canberra-gtk-play"): + subprocess.Popen(["canberra-gtk-play", "--id", "complete"]) + else: + print("canberra-gtk-play not found.")