Create FileItem model

This commit is contained in:
Jeena 2025-06-03 13:26:55 +09:00
parent 620018c4c7
commit 449bac079e
4 changed files with 121 additions and 69 deletions

View file

@ -8,6 +8,12 @@ class FileStatus(GObject.GEnum):
DONE = 3 DONE = 3
class FileItem(GObject.GObject): class FileItem(GObject.GObject):
__gtype_name__ = 'FileItem'
file = GObject.Property(type=Gio.File) file = GObject.Property(type=Gio.File)
progress = GObject.Property(type=int, minimum=0, maximum=100, default=0) progress = GObject.Property(type=int, minimum=0, maximum=100, default=0)
status = GObject.Property(type=FileStatus, default=FileStatus.WAITING) status = GObject.Property(type=FileStatus, default=FileStatus.WAITING)
def __init__(self, file: Gio.File):
super().__init__()
self.file = file

View file

@ -4,11 +4,13 @@ import subprocess
import re import re
from gi.repository import GLib 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+)") TIME_RE = re.compile(r"time=(\d+):(\d+):(\d+)\.(\d+)")
def __init__(self, files, progress_callback=None, done_callback=None, file_done_callback=None): def __init__(self, file_items, progress_callback=None, done_callback=None, file_done_callback=None):
self.files = files self.file_items = file_items
self.progress_callback = progress_callback self.progress_callback = progress_callback
self.done_callback = done_callback self.done_callback = done_callback
self.file_done_callback = file_done_callback self.file_done_callback = file_done_callback
@ -23,8 +25,9 @@ class TranscoderWorker:
thread.start() thread.start()
def _process_files(self): def _process_files(self):
total = len(self.files) total = len(self.file_items)
for idx, filepath in enumerate(self.files, start=1): for idx, file_item in enumerate(self.file_items, start=1):
filepath = file_item.file.get_path()
basename = os.path.basename(filepath) basename = os.path.basename(filepath)
self._update_progress(f"Starting {basename}", (idx - 1) / total) self._update_progress(f"Starting {basename}", (idx - 1) / total)

View file

@ -1,39 +1,66 @@
import os import os
import gi import gi
import shutil
import subprocess
gi.require_version('Gtk', '4.0') gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
gi.require_version('Notify', '0.7') 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 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): class FileEntryRow(Gtk.ListBoxRow):
def __init__(self, path): def __init__(self, item):
super().__init__() super().__init__()
self.path = path self.item = item
self.icon = Gtk.Image.new_from_icon_name("media-playback-pause-symbolic")
self.label = Gtk.Label(label=os.path.basename(path), xalign=0) 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 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
hbox.append(self.icon) hbox.append(self.icon)
hbox.append(self.label) hbox.append(self.label)
hbox.append(self.progress_label)
self.set_child(hbox) self.set_child(hbox)
def mark_done(self): self.item.connect("notify::status", self.on_status_changed)
self.icon.set_from_icon_name("emblem-ok-symbolic") self.item.connect("notify::progress", self.on_progress_changed)
self.label.set_text(f"{os.path.basename(self.path)} - Done")
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): def __init__(self, application, **kwargs):
self.file_items_to_process = []
self.files_to_process = []
self.transcoder = None self.transcoder = None
self.file_rows = {} self.file_rows = {}
# Load UI from resource
builder = Gtk.Builder() builder = Gtk.Builder()
builder.add_from_resource("/net/jeena/recoder/recoder.ui") 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("drop", self.on_drop)
self.drop_target.connect("enter", self.on_drop_enter) self.drop_target.connect("enter", self.on_drop_enter)
self.drop_target.connect("leave", self.on_drop_leave) self.drop_target.connect("leave", self.on_drop_leave)
self.window.add_controller(self.drop_target) self.window.add_controller(self.drop_target)
css_provider = Gtk.CssProvider() css_provider = Gtk.CssProvider()
@ -75,60 +101,48 @@ class RecoderWindow():
GLib.idle_add(partial(self.process_drop_value, value)) GLib.idle_add(partial(self.process_drop_value, value))
self.overlay.remove_overlay(self.drop_hint) self.overlay.remove_overlay(self.drop_hint)
self.progress.set_visible(True) 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) self.drop_hint.set_visible(False)
return value return value
def process_drop_value(self, value): def process_drop_value(self, value):
if isinstance(value, Gio.File): file_items = extract_video_files(value)
uris = [value] if not file_items:
elif isinstance(value, list):
uris = value
else:
return False return False
paths = [] # Clear previous rows
for file in uris: self.clear_listbox()
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
self.file_rows.clear() self.file_rows.clear()
# Add new files to listbox with custom FileEntryRow for file_item in file_items:
for path in paths: row = FileEntryRow(file_item)
row = FileEntryRow(path)
self.listbox.append(row) 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() self.start_transcoding()
return False 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): def start_transcoding(self):
if self.transcoder and self.transcoder.is_processing: if self.transcoder and self.transcoder.is_processing:
return return
self.window.remove_controller(self.drop_target)
self.progress.set_fraction(0.0) self.progress.set_fraction(0.0)
self.progress.set_text("Starting transcoding...") self.progress.set_text("Starting transcoding...")
self.transcoder = TranscoderWorker( self.transcoder = Transcoder(
self.files_to_process, self.file_items_to_process,
progress_callback=self.update_progress, progress_callback=self.update_progress,
done_callback=self.notify_done, done_callback=self._done_callback,
file_done_callback=self.mark_file_done, file_done_callback=self.mark_file_done,
) )
self.transcoder.start() self.transcoder.start()
@ -143,19 +157,9 @@ class RecoderWindow():
row = self.file_rows[filepath] row = self.file_rows[filepath]
GLib.idle_add(row.mark_done) GLib.idle_add(row.mark_done)
def notify_done(self): def _done_callback(self):
self.progress.set_show_text(True) self.progress.set_show_text(True)
self.progress.set_text("Transcoding Complete!") self.progress.set_text("Transcoding Complete!")
self.play_complete_sound() play_complete_sound()
notification = Notify.Notification.new( notify_done("Recoder", "Transcoding finished!")
"Recoder", self.window.add_controller(self.drop_target)
"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.")

39
src/recoder/utils.py Normal file
View file

@ -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.")