Move state update to UIStateManager
This commit is contained in:
parent
e90744852a
commit
6807717714
2 changed files with 210 additions and 178 deletions
|
@ -1,14 +1,15 @@
|
||||||
import gi
|
import gi
|
||||||
import signal
|
import signal
|
||||||
|
from enum import Enum, auto
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
gi.require_version('Gtk', '4.0')
|
gi.require_version('Gtk', '4.0')
|
||||||
gi.require_version('Adw', '1')
|
gi.require_version('Adw', '1')
|
||||||
gi.require_version('Notify', '0.7')
|
gi.require_version('Notify', '0.7')
|
||||||
|
|
||||||
from gi.repository import Gtk, Gdk, Gio, Adw, GLib, Notify
|
from gi.repository import Gtk, Gdk, Gio, Adw, GLib, Notify
|
||||||
from functools import partial
|
|
||||||
|
|
||||||
from recoder.transcoder import Transcoder
|
from recoder.transcoder import Transcoder, BatchStatus
|
||||||
from recoder.utils import extract_video_files, notify_done, play_complete_sound
|
from recoder.utils import extract_video_files, notify_done, play_complete_sound
|
||||||
from recoder.file_entry_row import FileEntryRow
|
from recoder.file_entry_row import FileEntryRow
|
||||||
|
|
||||||
|
@ -45,27 +46,99 @@ class DropHandler:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class AppState(Enum):
|
||||||
|
IDLE = auto() # Nothing loaded, waiting for drop
|
||||||
|
FILES_LOADED = auto() # Files dropped, ready to start transcoding
|
||||||
|
TRANSCODING = auto() # Transcoding in progress
|
||||||
|
PAUSED = auto() # Transcoding paused
|
||||||
|
DONE = auto() # Transcoding finished
|
||||||
|
STOPPED = auto() # Transcoding canceled
|
||||||
|
ERROR = auto() # Error occurred
|
||||||
|
|
||||||
|
|
||||||
class UIStateManager:
|
class UIStateManager:
|
||||||
def __init__(self, window):
|
def __init__(self, window):
|
||||||
self.window = window
|
self.window = window
|
||||||
|
self.handlers = {
|
||||||
|
AppState.IDLE: self._handle_idle,
|
||||||
|
AppState.FILES_LOADED: self._handle_files_loaded,
|
||||||
|
AppState.TRANSCODING: self._handle_transcoding,
|
||||||
|
AppState.PAUSED: self._handle_paused,
|
||||||
|
AppState.DONE: self._handle_done,
|
||||||
|
AppState.STOPPED: lambda: self.set_state(AppState.IDLE),
|
||||||
|
AppState.ERROR: self._handle_error,
|
||||||
|
}
|
||||||
|
|
||||||
def reset_ui(self):
|
def set_state(self, state: AppState):
|
||||||
self.window.btn_transcode.set_label("Start Transcoding")
|
handler = self.handlers.get(state)
|
||||||
self.window.btn_transcode.set_sensitive(False)
|
if handler:
|
||||||
self.window.btn_transcode.remove_css_class("suggested-action")
|
handler()
|
||||||
self.window.btn_cancel.set_visible(False)
|
|
||||||
self.window.progress_bar.set_fraction(0.0)
|
|
||||||
self.window.progress_bar.set_text("")
|
|
||||||
self.window.progress_bar.set_visible(False)
|
|
||||||
self.window.drop_hint.set_visible(True)
|
|
||||||
|
|
||||||
if self.window.drop_hint.get_parent() != self.window.overlay:
|
def _handle_idle(self):
|
||||||
self.window.overlay.add_overlay(self.window.drop_hint)
|
w = self.window
|
||||||
|
w.clear_listbox()
|
||||||
|
w.file_rows.clear()
|
||||||
|
w.file_items_to_process = []
|
||||||
|
w.is_paused = False
|
||||||
|
w.progress_bar.set_visible(False)
|
||||||
|
w.progress_bar.set_fraction(0.0)
|
||||||
|
w.progress_bar.set_text("")
|
||||||
|
w.drop_hint.set_visible(True)
|
||||||
|
if w.drop_hint.get_parent() != w.overlay:
|
||||||
|
w.overlay.add_overlay(w.drop_hint)
|
||||||
|
w.btn_transcode.set_visible(False)
|
||||||
|
w.btn_cancel.set_visible(False)
|
||||||
|
|
||||||
|
def _handle_files_loaded(self):
|
||||||
|
w = self.window
|
||||||
|
w.drop_hint.set_visible(False)
|
||||||
|
w.progress_bar.set_visible(False)
|
||||||
|
w.btn_transcode.set_visible(True)
|
||||||
|
w.btn_transcode.set_sensitive(True)
|
||||||
|
w.btn_transcode.set_label("Start Transcoding")
|
||||||
|
w.btn_transcode.add_css_class("suggested-action")
|
||||||
|
w.btn_cancel.set_visible(False)
|
||||||
|
w.is_paused = False
|
||||||
|
|
||||||
|
def _handle_transcoding(self):
|
||||||
|
w = self.window
|
||||||
|
w.drop_hint.set_visible(False)
|
||||||
|
w.progress_bar.set_visible(True)
|
||||||
|
w.btn_transcode.set_visible(True)
|
||||||
|
w.btn_transcode.set_sensitive(True)
|
||||||
|
w.btn_transcode.set_label("Pause")
|
||||||
|
w.btn_transcode.remove_css_class("suggested-action")
|
||||||
|
w.btn_cancel.set_visible(False)
|
||||||
|
w.is_paused = False
|
||||||
|
|
||||||
|
def _handle_paused(self):
|
||||||
|
w = self.window
|
||||||
|
w.drop_hint.set_visible(False)
|
||||||
|
w.progress_bar.set_visible(True)
|
||||||
|
w.btn_transcode.set_visible(True)
|
||||||
|
w.btn_transcode.set_sensitive(True)
|
||||||
|
w.btn_transcode.set_label("Resume")
|
||||||
|
w.btn_cancel.set_visible(True)
|
||||||
|
w.is_paused = True
|
||||||
|
|
||||||
|
def _handle_done(self):
|
||||||
|
w = self.window
|
||||||
|
w.drop_hint.set_visible(False)
|
||||||
|
w.progress_bar.set_visible(True)
|
||||||
|
w.progress_bar.set_fraction(1.0)
|
||||||
|
w.btn_transcode.set_visible(False)
|
||||||
|
w.btn_transcode.remove_css_class("suggested-action")
|
||||||
|
w.btn_cancel.set_visible(True)
|
||||||
|
w.is_paused = False
|
||||||
|
|
||||||
|
def _handle_error(self):
|
||||||
|
w = self.window
|
||||||
|
w.drop_hint.set_visible(False)
|
||||||
|
w.progress_bar.set_visible(False)
|
||||||
|
w.btn_transcode.set_visible(False)
|
||||||
|
w.btn_cancel.set_visible(True)
|
||||||
|
w.is_paused = False
|
||||||
|
|
||||||
self.window.clear_listbox()
|
|
||||||
self.window.file_rows.clear()
|
|
||||||
self.window.file_items_to_process = []
|
|
||||||
self.window.is_paused = False
|
|
||||||
|
|
||||||
|
|
||||||
@Gtk.Template(resource_path="/net/jeena/recoder/recoder_window.ui")
|
@Gtk.Template(resource_path="/net/jeena/recoder/recoder_window.ui")
|
||||||
|
@ -73,12 +146,12 @@ class RecoderWindow(Adw.ApplicationWindow):
|
||||||
__gtype_name__ = "RecoderWindow"
|
__gtype_name__ = "RecoderWindow"
|
||||||
|
|
||||||
overlay = Gtk.Template.Child()
|
overlay = Gtk.Template.Child()
|
||||||
progress_bar = Gtk.Template.Child()
|
|
||||||
drop_hint = Gtk.Template.Child()
|
drop_hint = Gtk.Template.Child()
|
||||||
listbox = Gtk.Template.Child()
|
listbox = Gtk.Template.Child()
|
||||||
scrolled_window = Gtk.Template.Child()
|
scrolled_window = Gtk.Template.Child()
|
||||||
btn_transcode = Gtk.Template.Child()
|
btn_transcode = Gtk.Template.Child()
|
||||||
btn_cancel = Gtk.Template.Child()
|
btn_cancel = Gtk.Template.Child()
|
||||||
|
progress_bar = Gtk.Template.Child()
|
||||||
|
|
||||||
def __init__(self, application):
|
def __init__(self, application):
|
||||||
super().__init__(application=application)
|
super().__init__(application=application)
|
||||||
|
@ -88,16 +161,15 @@ class RecoderWindow(Adw.ApplicationWindow):
|
||||||
self.file_rows = {}
|
self.file_rows = {}
|
||||||
self.is_paused = False
|
self.is_paused = False
|
||||||
|
|
||||||
self.ui_manager = UIStateManager(self)
|
|
||||||
self.drop_handler = DropHandler(self.overlay, self.drop_hint, self.progress_bar, self.process_drop_value)
|
self.drop_handler = DropHandler(self.overlay, self.drop_hint, self.progress_bar, self.process_drop_value)
|
||||||
self.add_controller(self.drop_handler.controller())
|
self.add_controller(self.drop_handler.controller())
|
||||||
|
|
||||||
|
self.ui_manager = UIStateManager(self)
|
||||||
|
|
||||||
self.btn_transcode.connect("clicked", self.on_transcode_clicked)
|
self.btn_transcode.connect("clicked", self.on_transcode_clicked)
|
||||||
self.btn_cancel.connect("clicked", self.on_cancel_clicked)
|
self.btn_cancel.connect("clicked", self.on_cancel_clicked)
|
||||||
|
|
||||||
self.btn_transcode.set_sensitive(False)
|
self.ui_manager.set_state(AppState.IDLE)
|
||||||
self.btn_cancel.set_visible(False)
|
|
||||||
self.btn_transcode.set_label("Start Transcoding")
|
|
||||||
|
|
||||||
css_provider = Gtk.CssProvider()
|
css_provider = Gtk.CssProvider()
|
||||||
css_provider.load_from_resource("/net/jeena/recoder/style.css")
|
css_provider.load_from_resource("/net/jeena/recoder/style.css")
|
||||||
|
@ -123,9 +195,7 @@ class RecoderWindow(Adw.ApplicationWindow):
|
||||||
self.file_rows[file_item.file] = row
|
self.file_rows[file_item.file] = row
|
||||||
|
|
||||||
self.file_items_to_process = file_items
|
self.file_items_to_process = file_items
|
||||||
self.btn_transcode.set_sensitive(True)
|
self.ui_manager.set_state(AppState.FILES_LOADED)
|
||||||
self.btn_transcode.add_css_class("suggested-action")
|
|
||||||
self.btn_transcode.set_label("Start Transcoding")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def clear_listbox(self):
|
def clear_listbox(self):
|
||||||
|
@ -149,67 +219,49 @@ class RecoderWindow(Adw.ApplicationWindow):
|
||||||
return
|
return
|
||||||
|
|
||||||
self.remove_controller(self.drop_handler.controller())
|
self.remove_controller(self.drop_handler.controller())
|
||||||
self.btn_transcode.set_label("Pause")
|
|
||||||
self.btn_transcode.set_sensitive(True)
|
|
||||||
self.btn_transcode.remove_css_class("suggested-action")
|
|
||||||
self.btn_cancel.set_visible(False)
|
|
||||||
self.is_paused = False
|
|
||||||
|
|
||||||
self.progress_bar.set_fraction(0.0)
|
self.transcoder = Transcoder(self.file_items_to_process)
|
||||||
self.progress_bar.set_text("Starting transcoding...")
|
self.transcoder.connect("notify::batch-progress", self.on_transcoder_progress)
|
||||||
|
self.transcoder.connect("notify::batch-status", self.on_transcoder_status)
|
||||||
self.transcoder = Transcoder(
|
|
||||||
self.file_items_to_process,
|
|
||||||
progress_callback=self.update_progress,
|
|
||||||
done_callback=self._done_callback,
|
|
||||||
file_done_callback=self.mark_file_done,
|
|
||||||
)
|
|
||||||
self.transcoder.start()
|
self.transcoder.start()
|
||||||
|
|
||||||
|
self.ui_manager.set_state(AppState.TRANSCODING)
|
||||||
|
|
||||||
def pause_transcoding(self):
|
def pause_transcoding(self):
|
||||||
if self.transcoder:
|
if self.transcoder:
|
||||||
self.transcoder.pause()
|
self.transcoder.pause()
|
||||||
self.is_paused = True
|
self.is_paused = True
|
||||||
self.btn_transcode.set_label("Resume")
|
self.ui_manager.set_state(AppState.PAUSED)
|
||||||
self.progress_bar.set_text("Paused")
|
|
||||||
self.btn_cancel.set_visible(True)
|
|
||||||
|
|
||||||
def resume_transcoding(self):
|
def resume_transcoding(self):
|
||||||
if self.transcoder:
|
if self.transcoder:
|
||||||
self.transcoder.resume()
|
self.transcoder.resume()
|
||||||
self.is_paused = False
|
self.is_paused = False
|
||||||
self.btn_transcode.set_label("Pause")
|
self.ui_manager.set_state(AppState.TRANSCODING)
|
||||||
self.progress_bar.set_text("Resuming...")
|
|
||||||
self.btn_cancel.set_visible(False)
|
|
||||||
|
|
||||||
def on_cancel_clicked(self, button):
|
def on_cancel_clicked(self, button):
|
||||||
if self.transcoder and self.transcoder.is_processing:
|
if self.transcoder and self.transcoder.is_processing:
|
||||||
self.transcoder.stop()
|
self.transcoder.stop()
|
||||||
self.transcoder = None
|
self.transcoder = None
|
||||||
self.add_controller(self.drop_handler.controller())
|
self.add_controller(self.drop_handler.controller())
|
||||||
self.ui_manager.reset_ui()
|
self.ui_manager.set_state(AppState.STOPPED)
|
||||||
|
|
||||||
def update_progress(self, text, fraction):
|
def on_transcoder_progress(self, transcoder, param):
|
||||||
self.progress_bar.set_show_text(True)
|
self.progress_bar.set_fraction(transcoder.batch_progress / 100.0)
|
||||||
self.progress_bar.set_text(text)
|
self.progress_bar.set_visible(True)
|
||||||
self.progress_bar.set_fraction(fraction)
|
|
||||||
|
|
||||||
def mark_file_done(self, filepath):
|
def on_transcoder_status(self, transcoder, param):
|
||||||
if filepath in self.file_rows:
|
if transcoder.batch_status == BatchStatus.DONE:
|
||||||
row = self.file_rows[filepath]
|
play_complete_sound()
|
||||||
GLib.idle_add(row.mark_done)
|
notify_done("Recoder", "Transcoding finished!")
|
||||||
|
self.add_controller(self.drop_handler.controller())
|
||||||
|
self.ui_manager.set_state(AppState.DONE)
|
||||||
|
|
||||||
def _done_callback(self):
|
elif transcoder.batch_status == BatchStatus.STOPPED:
|
||||||
self.progress_bar.set_show_text(True)
|
self.add_controller(self.drop_handler.controller())
|
||||||
self.progress_bar.set_text("Transcoding Complete!")
|
self.ui_manager.set_state(AppState.STOPPED)
|
||||||
play_complete_sound()
|
|
||||||
notify_done("Recoder", "Transcoding finished!")
|
|
||||||
self.add_controller(self.drop_handler.controller())
|
|
||||||
self.btn_transcode.set_label("Start Transcoding")
|
|
||||||
self.btn_transcode.set_sensitive(False)
|
|
||||||
self.btn_cancel.set_visible(False)
|
|
||||||
self.is_paused = False
|
|
||||||
|
|
||||||
self.drop_hint.set_visible(True)
|
elif transcoder.batch_status == BatchStatus.ERROR:
|
||||||
if self.drop_hint.get_parent() != self.overlay:
|
notify_done("Recoder", "An error occurred during transcoding.")
|
||||||
self.overlay.add_overlay(self.drop_hint)
|
self.add_controller(self.drop_handler.controller())
|
||||||
|
self.ui_manager.set_state(AppState.ERROR)
|
||||||
|
|
|
@ -3,22 +3,35 @@ import threading
|
||||||
import subprocess
|
import subprocess
|
||||||
import re
|
import re
|
||||||
import signal
|
import signal
|
||||||
from gi.repository import GLib
|
|
||||||
|
|
||||||
|
from gi.repository import GLib, GObject
|
||||||
from recoder.models import FileStatus, FileItem
|
from recoder.models import FileStatus, FileItem
|
||||||
|
|
||||||
class Transcoder:
|
|
||||||
|
class BatchStatus(GObject.GEnum):
|
||||||
|
__gtype_name__ = 'BatchStatus'
|
||||||
|
|
||||||
|
IDLE = 0
|
||||||
|
RUNNING = 1
|
||||||
|
PAUSED = 2
|
||||||
|
DONE = 3
|
||||||
|
STOPPED = 4
|
||||||
|
ERROR = 5
|
||||||
|
|
||||||
|
|
||||||
|
class Transcoder(GObject.GObject):
|
||||||
TIME_RE = re.compile(r"time=(\d+):(\d+):(\d+)\.(\d+)")
|
TIME_RE = re.compile(r"time=(\d+):(\d+):(\d+)\.(\d+)")
|
||||||
|
|
||||||
def __init__(self, file_items, progress_callback=None, done_callback=None, file_done_callback=None):
|
batch_progress = GObject.Property(type=int, minimum=0, maximum=100, default=0)
|
||||||
|
batch_status = GObject.Property(type=BatchStatus, default=BatchStatus.IDLE)
|
||||||
|
|
||||||
|
def __init__(self, file_items):
|
||||||
|
super().__init__()
|
||||||
self.file_items = file_items
|
self.file_items = file_items
|
||||||
self.progress_callback = progress_callback
|
|
||||||
self.done_callback = done_callback
|
|
||||||
self.file_done_callback = file_done_callback
|
|
||||||
self.is_processing = False
|
self.is_processing = False
|
||||||
self._stop_requested = False
|
self._stop_requested = False
|
||||||
self._paused = threading.Event()
|
self._paused = threading.Event()
|
||||||
self._paused.set() # Initially not paused
|
self._paused.set()
|
||||||
self._process = None
|
self._process = None
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
|
@ -27,123 +40,81 @@ class Transcoder:
|
||||||
self.is_processing = True
|
self.is_processing = True
|
||||||
self._stop_requested = False
|
self._stop_requested = False
|
||||||
self._paused.set()
|
self._paused.set()
|
||||||
thread = threading.Thread(target=self._process_files, daemon=True)
|
self.batch_status = BatchStatus.RUNNING
|
||||||
thread.start()
|
threading.Thread(target=self._process_files, daemon=True).start()
|
||||||
|
|
||||||
def pause(self):
|
def pause(self):
|
||||||
self._paused.clear()
|
self._paused.clear()
|
||||||
|
self.batch_status = BatchStatus.PAUSED
|
||||||
if self._process and self._process.poll() is None:
|
if self._process and self._process.poll() is None:
|
||||||
self._process.send_signal(signal.SIGSTOP)
|
self._process.send_signal(signal.SIGSTOP)
|
||||||
|
|
||||||
def resume(self):
|
def resume(self):
|
||||||
self._paused.set()
|
self._paused.set()
|
||||||
|
self.batch_status = BatchStatus.RUNNING
|
||||||
if self._process and self._process.poll() is None:
|
if self._process and self._process.poll() is None:
|
||||||
self._process.send_signal(signal.SIGCONT)
|
self._process.send_signal(signal.SIGCONT)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self._stop_requested = True
|
self._stop_requested = True
|
||||||
self._paused.set() # Unpause so thread can exit if paused
|
self._paused.set()
|
||||||
if self._process and self._process.poll() is None:
|
if self._process and self._process.poll() is None:
|
||||||
self._process.terminate()
|
self._process.terminate()
|
||||||
|
self.batch_status = BatchStatus.STOPPED
|
||||||
|
|
||||||
def _process_files(self):
|
def _process_files(self):
|
||||||
total = len(self.file_items)
|
total = len(self.file_items)
|
||||||
|
|
||||||
for idx, file_item in enumerate(self.file_items, start=1):
|
for idx, file_item in enumerate(self.file_items, start=1):
|
||||||
if self._stop_requested:
|
if self._stop_requested:
|
||||||
GLib.idle_add(file_item.set_property, "status", FileStatus.WAITING)
|
GLib.idle_add(file_item.set_property, "status", FileStatus.WAITING)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
filepath = file_item.file.get_path()
|
path = file_item.file.get_path()
|
||||||
basename = os.path.basename(filepath)
|
base = os.path.basename(path)
|
||||||
|
|
||||||
GLib.idle_add(file_item.set_property, "status", FileStatus.PROCESSING)
|
GLib.idle_add(file_item.set_property, "status", FileStatus.PROCESSING)
|
||||||
GLib.idle_add(file_item.set_property, "progress", 0)
|
GLib.idle_add(file_item.set_property, "progress", 0)
|
||||||
|
|
||||||
self._update_progress(f"Starting {basename}", (idx - 1) / total)
|
|
||||||
|
|
||||||
success, output_path = self._transcode_file(
|
success, _ = self._transcode_file(path, os.path.join(os.path.dirname(path), "transcoded"),
|
||||||
filepath,
|
base, idx, total, file_item)
|
||||||
os.path.join(os.path.dirname(filepath), "transcoded"),
|
|
||||||
basename,
|
|
||||||
idx,
|
|
||||||
total,
|
|
||||||
file_item,
|
|
||||||
)
|
|
||||||
|
|
||||||
new_status = FileStatus.DONE if success else FileStatus.ERROR
|
new_status = FileStatus.DONE if success else FileStatus.ERROR
|
||||||
GLib.idle_add(file_item.set_property, "status", new_status)
|
GLib.idle_add(file_item.set_property, "status", new_status)
|
||||||
GLib.idle_add(file_item.set_property, "progress", 100 if success else 0)
|
GLib.idle_add(file_item.set_property, "progress", 100 if success else 0)
|
||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
self._update_progress(f"Error transcoding {basename}", idx / total)
|
self.batch_status = BatchStatus.ERROR
|
||||||
else:
|
break
|
||||||
self._update_progress(f"Finished {basename}", idx / total)
|
|
||||||
|
|
||||||
if self.file_done_callback:
|
|
||||||
GLib.idle_add(self.file_done_callback, filepath)
|
|
||||||
|
|
||||||
if self._stop_requested:
|
if self._stop_requested:
|
||||||
break
|
break
|
||||||
|
|
||||||
self.is_processing = False
|
self.is_processing = False
|
||||||
self._update_progress("All done!", 1.0)
|
|
||||||
if not self._stop_requested:
|
|
||||||
self._notify_done()
|
|
||||||
|
|
||||||
def _transcode_file(self, input_path, output_dir, basename, idx, total, file_item=None):
|
if not self._stop_requested and self.batch_status != BatchStatus.ERROR:
|
||||||
|
self.batch_status = BatchStatus.DONE
|
||||||
|
elif self._stop_requested:
|
||||||
|
self.batch_status = BatchStatus.STOPPED
|
||||||
|
|
||||||
|
GLib.idle_add(self.set_property, "batch_progress", 0)
|
||||||
|
|
||||||
|
def _transcode_file(self, input_path, output_dir, basename, idx, total, file_item):
|
||||||
os.makedirs(output_dir, exist_ok=True)
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
output_path = self._get_output_path(output_dir, basename)
|
output_path = self._get_output_path(output_dir, basename)
|
||||||
|
|
||||||
duration = self._get_duration(input_path) or 1.0
|
duration = self._get_duration(input_path) or 1.0
|
||||||
width, height, rotate = self._get_video_info(input_path)
|
width, height, rotate = self._get_video_info(input_path)
|
||||||
|
|
||||||
vf = self._build_filters(width, height, rotate)
|
vf = self._build_filters(width, height, rotate)
|
||||||
cmd = self._build_ffmpeg_command(input_path, output_path, vf)
|
cmd = self._build_ffmpeg_command(input_path, output_path, vf)
|
||||||
|
|
||||||
return self._run_ffmpeg(cmd, duration, idx, total, basename, file_item, output_path)
|
return self._run_ffmpeg(cmd, duration, idx, total, file_item, output_path)
|
||||||
|
|
||||||
def _get_output_path(self, output_dir, basename):
|
def _run_ffmpeg(self, cmd, duration, idx, total, file_item, output_path):
|
||||||
name, _ = os.path.splitext(basename)
|
|
||||||
return os.path.join(output_dir, f"{name}.mov")
|
|
||||||
|
|
||||||
def _build_filters(self, width, height, rotate):
|
|
||||||
filters = []
|
|
||||||
if rotate in [90, 270] or (width and height and height > width):
|
|
||||||
filters.append("transpose=1")
|
|
||||||
width, height = height, width
|
|
||||||
|
|
||||||
if (width, height) != (1920, 1080):
|
|
||||||
filters.append("scale=1920:1080")
|
|
||||||
|
|
||||||
return ",".join(filters) if filters else None
|
|
||||||
|
|
||||||
def _build_ffmpeg_command(self, input_path, output_path, vf):
|
|
||||||
cmd = [
|
|
||||||
"ffmpeg",
|
|
||||||
"-y",
|
|
||||||
"-i", input_path,
|
|
||||||
"-vcodec", "dnxhd",
|
|
||||||
"-acodec", "pcm_s16le",
|
|
||||||
"-b:v", "36M",
|
|
||||||
"-pix_fmt", "yuv422p",
|
|
||||||
"-r", "30000/1001",
|
|
||||||
"-f", "mov",
|
|
||||||
"-map_metadata", "0",
|
|
||||||
]
|
|
||||||
|
|
||||||
if vf:
|
|
||||||
cmd += ["-vf", vf]
|
|
||||||
|
|
||||||
cmd.append(output_path)
|
|
||||||
return cmd
|
|
||||||
|
|
||||||
def _run_ffmpeg(self, cmd, duration, idx, total, basename, file_item, output_path):
|
|
||||||
self._process = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True)
|
self._process = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
# Pause support
|
|
||||||
self._paused.wait()
|
self._paused.wait()
|
||||||
|
|
||||||
if self._stop_requested:
|
if self._stop_requested:
|
||||||
self._process.terminate()
|
self._process.terminate()
|
||||||
return False, output_path
|
return False, output_path
|
||||||
|
@ -154,56 +125,65 @@ class Transcoder:
|
||||||
|
|
||||||
match = self.TIME_RE.search(line)
|
match = self.TIME_RE.search(line)
|
||||||
if match:
|
if match:
|
||||||
hours, minutes, seconds, milliseconds = map(int, match.groups())
|
h, m, s, ms = map(int, match.groups())
|
||||||
elapsed = hours * 3600 + minutes * 60 + seconds + milliseconds / 1000.0
|
elapsed = h * 3600 + m * 60 + s + ms / 1000.0
|
||||||
progress_fraction = min(elapsed / duration, 1.0)
|
file_progress = min(elapsed / duration, 1.0)
|
||||||
overall_fraction = (idx - 1 + progress_fraction) / total
|
|
||||||
self._update_progress(f"Transcoding {basename}", overall_fraction)
|
|
||||||
|
|
||||||
if file_item:
|
file_percent = int(file_progress * 100)
|
||||||
percent = int(progress_fraction * 100)
|
batch_fraction = (idx - 1 + file_progress) / total
|
||||||
GLib.idle_add(file_item.set_property, "progress", percent)
|
batch_percent = int(batch_fraction * 100)
|
||||||
|
|
||||||
|
GLib.idle_add(file_item.set_property, "progress", file_percent)
|
||||||
|
GLib.idle_add(self.set_property, "batch_progress", batch_percent)
|
||||||
|
|
||||||
self._process.wait()
|
self._process.wait()
|
||||||
success = self._process.returncode == 0
|
return self._process.returncode == 0, output_path
|
||||||
return success, output_path
|
|
||||||
|
|
||||||
def _get_duration(self, input_path):
|
def _get_output_path(self, out_dir, basename):
|
||||||
cmd = [
|
name, _ = os.path.splitext(basename)
|
||||||
"ffprobe",
|
return os.path.join(out_dir, f"{name}.mov")
|
||||||
"-v", "error",
|
|
||||||
"-show_entries", "format=duration",
|
def _get_duration(self, path):
|
||||||
"-of", "default=noprint_wrappers=1:nokey=1",
|
|
||||||
input_path,
|
|
||||||
]
|
|
||||||
try:
|
try:
|
||||||
output = subprocess.check_output(cmd, text=True)
|
out = subprocess.check_output([
|
||||||
return float(output.strip())
|
"ffprobe", "-v", "error", "-show_entries", "format=duration",
|
||||||
|
"-of", "default=noprint_wrappers=1:nokey=1", path
|
||||||
|
], text=True)
|
||||||
|
return float(out.strip())
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _get_video_info(self, input_path):
|
def _get_video_info(self, path):
|
||||||
cmd = [
|
|
||||||
"ffprobe",
|
|
||||||
"-v", "error",
|
|
||||||
"-select_streams", "v:0",
|
|
||||||
"-show_entries", "stream=width,height:stream_tags=rotate",
|
|
||||||
"-of", "default=noprint_wrappers=1:nokey=1",
|
|
||||||
input_path,
|
|
||||||
]
|
|
||||||
try:
|
try:
|
||||||
output = subprocess.check_output(cmd, text=True).splitlines()
|
out = subprocess.check_output([
|
||||||
width = int(output[0])
|
"ffprobe", "-v", "error", "-select_streams", "v:0",
|
||||||
height = int(output[1])
|
"-show_entries", "stream=width,height:stream_tags=rotate",
|
||||||
rotate = int(output[2]) if len(output) > 2 else 0
|
"-of", "default=noprint_wrappers=1:nokey=1", path
|
||||||
|
], text=True).splitlines()
|
||||||
|
width = int(out[0])
|
||||||
|
height = int(out[1])
|
||||||
|
rotate = int(out[2]) if len(out) > 2 else 0
|
||||||
return width, height, rotate
|
return width, height, rotate
|
||||||
except Exception:
|
except Exception:
|
||||||
return None, None, 0
|
return None, None, 0
|
||||||
|
|
||||||
def _update_progress(self, text, fraction):
|
def _build_filters(self, width, height, rotate):
|
||||||
if self.progress_callback:
|
filters = []
|
||||||
GLib.idle_add(self.progress_callback, text, fraction)
|
if rotate in [90, 270] or (width and height and height > width):
|
||||||
|
filters.append("transpose=1")
|
||||||
|
width, height = height, width
|
||||||
|
if (width, height) != (1920, 1080):
|
||||||
|
filters.append("scale=1920:1080")
|
||||||
|
return ",".join(filters) if filters else None
|
||||||
|
|
||||||
def _notify_done(self):
|
def _build_ffmpeg_command(self, in_path, out_path, vf):
|
||||||
if self.done_callback:
|
cmd = [
|
||||||
GLib.idle_add(self.done_callback)
|
"ffmpeg", "-y", "-i", in_path,
|
||||||
|
"-vcodec", "dnxhd", "-acodec", "pcm_s16le",
|
||||||
|
"-b:v", "36M", "-pix_fmt", "yuv422p", "-r", "30000/1001",
|
||||||
|
"-f", "mov", "-map_metadata", "0"
|
||||||
|
]
|
||||||
|
if vf:
|
||||||
|
cmd += ["-vf", vf]
|
||||||
|
cmd.append(out_path)
|
||||||
|
return cmd
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue