Restructure app and move into files

This commit is contained in:
Jeena 2025-06-01 12:18:58 +09:00
parent 32a4e78d1b
commit 86bf8750de
13 changed files with 205 additions and 186 deletions

6
.gitignore vendored
View file

@ -1 +1,7 @@
__pycache__/
*.py[cod]
*$py.class
pkg/
*.tar.zst
*.tar.xz

View file

@ -1,15 +0,0 @@
pkgname=recoder
pkgver=1.0
pkgrel=1
pkgdesc="GTK4/libadwaita Video Recoder for DaVinci Resolve"
arch=('any')
url="https://example.com"
license=('MIT')
depends=('python' 'python-gobject' 'gtk4' 'libadwaita')
source=('recoder.py' 'recoder.desktop')
sha256sums=('SKIP' 'SKIP')
package() {
install -Dm755 "recoder.py" "$pkgdir/usr/bin/recoder"
install -Dm644 "recoder.desktop" "$pkgdir/usr/share/applications/recoder.desktop"
}

10
Pipfile
View file

@ -1,10 +0,0 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[packages]
PyGObject = "*"
[requires]
python_version = "3.10"

0
README.md Normal file
View file

25
packaging/PKGBUILD Normal file
View file

@ -0,0 +1,25 @@
# Maintainer: Jeena <your-email@example.com>
pkgname=recoder
pkgver=1.0.0
pkgrel=1
pkgdesc="A GTK4 video transcoding GUI application"
arch=('x86_64' 'aarch64')
url="https://github.com/yourusername/recoder"
license=('GPL3')
depends=('gtk4' 'libadwaita' 'gobject-introspection' 'python' 'python-gobject' 'pulseaudio' 'ffmpeg')
optdepends=('libcanberra: play system notification sounds')
makedepends=('python-setuptools')
source=()
noextract=()
sha256sums=()
package() {
install -dm755 "$pkgdir/usr/bin"
install -m755 ../src/app.py "$pkgdir/usr/bin/recoder"
install -dm755 "$pkgdir/usr/lib/recoder"
cp -r ../src/* "$pkgdir/usr/lib/recoder/"
install -Dm644 ../data/recoder.desktop "$pkgdir/usr/share/applications/recoder.desktop"
install -Dm644 ../data/icons/recoder.png "$pkgdir/usr/share/icons/hicolor/256x256/apps/recoder.png"
}

View file

@ -1,8 +0,0 @@
[Desktop Entry]
Type=Application
Name=Recoder
Exec=recoder
Icon=video-x-generic
Terminal=false
Categories=AudioVideo;Video;GTK;
StartupNotify=true

View file

@ -0,0 +1,9 @@
[Desktop Entry]
Name=Recoder
Comment=GTK4 Video Transcoding GUI Application
Exec=recoder
Icon=recoder
Terminal=false
Type=Application
Categories=AudioVideo;Video;Utility;
StartupNotify=true

28
src/app.py Normal file
View file

@ -0,0 +1,28 @@
import sys
import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Adw, Gio
from ui import RecoderWindow
Adw.init()
class RecoderApp(Adw.Application):
def __init__(self):
super().__init__(application_id="net.jeena.recoder",
flags=Gio.ApplicationFlags.FLAGS_NONE)
self.window = None
def do_activate(self):
if not self.window:
self.window = RecoderWindow(application=self)
self.window.present()
def main():
app = RecoderApp()
return app.run(sys.argv)
if __name__ == "__main__":
sys.exit(main())

16
src/config.py Normal file
View file

@ -0,0 +1,16 @@
import json
from pathlib import Path
from gi.repository import GLib
CONFIG_PATH = Path(GLib.get_user_config_dir()) / "recoder" / "config.json"
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
def save_config(config):
with open(CONFIG_PATH, "w") as f:
json.dump(config, f)
def load_config():
if CONFIG_PATH.exists():
with open(CONFIG_PATH) as f:
return json.load(f)
return {}

57
src/transcoder_worker.py Normal file
View file

@ -0,0 +1,57 @@
import os
import threading
from gi.repository import GLib
import subprocess
def transcode_file(input_path, output_dir):
os.makedirs(output_dir, exist_ok=True)
basename = os.path.basename(input_path)
output_path = os.path.join(output_dir, basename)
cmd = [
"ffmpeg", "-y", "-i", input_path,
"-c:v", "libx264", "-preset", "fast",
"-c:a", "aac", output_path
]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return result.returncode == 0, output_path
class TranscoderWorker:
def __init__(self, files, progress_callback=None, done_callback=None):
self.files = files
self.progress_callback = progress_callback
self.done_callback = done_callback
self.is_processing = False
def start(self):
if self.is_processing:
return
self.is_processing = True
thread = threading.Thread(target=self._process_files)
thread.daemon = True
thread.start()
def _process_files(self):
total = len(self.files)
for idx, filepath in enumerate(self.files, start=1):
basename = os.path.basename(filepath)
self._update_progress(f"Processing {basename} ({idx}/{total})...", idx / total)
success, output_path = transcode_file(filepath, os.path.join(os.path.dirname(filepath), "transcoded"))
if not success:
self._update_progress(f"Error transcoding {basename}", idx / total)
else:
self._update_progress(f"Finished {basename}", idx / total)
self.is_processing = False
self._update_progress("All done!", 1.0)
self._notify_done()
def _update_progress(self, text, fraction):
if self.progress_callback:
GLib.idle_add(self.progress_callback, text, fraction)
def _notify_done(self):
if self.done_callback:
GLib.idle_add(self.done_callback)

201
recoder.py → src/ui.py Executable file → Normal file
View file

@ -1,35 +1,14 @@
#!/usr/bin/env python3
import os
import gi
import shutil
import subprocess
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw, Gio, Gdk, GLib, GObject
from gi.repository import Gtk, Adw, Gio, Gdk, GLib
from functools import partial
import os
import sys
import subprocess
import threading
import json
from pathlib import Path
import asyncio
from models import FileItem, FileStatus
from transcoder import transcode_file
Adw.init()
CONFIG_PATH = Path(GLib.get_user_config_dir()) / "recoder" / "config.json"
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
def save_config(config):
with open(CONFIG_PATH, "w") as f:
json.dump(config, f)
def load_config():
if CONFIG_PATH.exists():
with open(CONFIG_PATH) as f:
return json.load(f)
return {}
from config import load_config, save_config
from transcoder_worker import TranscoderWorker
class RecoderWindow(Adw.ApplicationWindow):
def __init__(self, **kwargs):
@ -38,35 +17,53 @@ class RecoderWindow(Adw.ApplicationWindow):
self.set_default_size(700, 400)
self.config = load_config()
self.current_dir = self.config.get("last_directory", str(Path.home()))
self.current_dir = self.config.get("last_directory", str(os.path.expanduser("~")))
# Main Box
self.files_to_process = []
self.transcoder = None
# UI setup
self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
# Create the header bar
self.header_bar = Adw.HeaderBar()
# Create the toolbar view and set the header bar
self.toolbar_view = Adw.ToolbarView()
self.toolbar_view.add_top_bar(self.header_bar)
self.toolbar_view.set_content(self.vbox)
# Set the toolbar view as the window content
self.set_content(self.toolbar_view)
# TextView to list files
self.textview = Gtk.TextView()
self.textview.set_editable(False)
self.textbuffer = self.textview.get_buffer()
# Create a DropTarget for URI list (files)
self.drop_target = Gtk.DropTarget.new(Gio.File, Gdk.DragAction.COPY)
self.drop_target.connect("enter", self.on_drop_enter)
self.drop_target.connect("leave", self.on_drop_leave)
self.drop_target.connect("drop", self.on_drop)
self.textview.add_controller(self.drop_target)
# CSS for highlight
# CSS
self._setup_css()
self.drop_hint = Gtk.Label(label="📂 Drop files here to get started")
self.drop_hint.add_css_class("dim-label")
self._setup_dim_label_css()
overlay = Gtk.Overlay()
overlay.set_child(self.textview)
overlay.add_overlay(self.drop_hint)
self.drop_hint.set_halign(Gtk.Align.CENTER)
self.drop_hint.set_valign(Gtk.Align.CENTER)
self.scrolled = Gtk.ScrolledWindow()
self.scrolled.set_child(overlay)
self.vbox.append(self.scrolled)
self.scrolled.set_vexpand(True)
self.scrolled.set_hexpand(True)
self.progress = Gtk.ProgressBar()
self.vbox.append(self.progress)
self.progress.set_hexpand(True)
def _setup_css(self):
css = b"""
textview.drop-highlight {
background-color: @theme_selected_bg_color;
@ -81,11 +78,7 @@ class RecoderWindow(Adw.ApplicationWindow):
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
)
# Create label overlay
self.drop_hint = Gtk.Label(label="📂 Drop files here to get started")
self.drop_hint.add_css_class("dim-label")
# Optional: use CSS to style it
def _setup_dim_label_css(self):
css = b"""
.dim-label {
font-size: 16px;
@ -100,47 +93,19 @@ class RecoderWindow(Adw.ApplicationWindow):
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
)
# Overlay
overlay = Gtk.Overlay()
overlay.set_child(self.textview)
overlay.add_overlay(self.drop_hint)
self.drop_hint.set_halign(Gtk.Align.CENTER)
self.drop_hint.set_valign(Gtk.Align.CENTER)
# Scroll container for textview
self.scrolled = Gtk.ScrolledWindow()
self.scrolled.set_child(overlay)
self.vbox.append(self.scrolled)
self.scrolled.set_vexpand(True)
self.scrolled.set_hexpand(True)
# Progress bar
self.progress = Gtk.ProgressBar()
self.vbox.append(self.progress)
self.progress.set_hexpand(True)
self.files_to_process = []
self.is_processing = False
def load_files_from_directory(self, directory):
self.files_to_process = []
self.textbuffer.set_text("")
for entry in os.scandir(directory):
if entry.is_file() and entry.name.lower().endswith((".mp4", ".mov", ".mkv", ".avi")):
self.files_to_process.append(entry.path)
self.textbuffer.insert_at_cursor(f"{entry.name}\n")
if self.files_to_process:
self.start_transcoding()
def on_drop_enter(self, drop_target, x, y):
self.textview.add_css_class("drop-highlight")
return True
def on_drop_leave(self, drop_target):
self.textview.remove_css_class("drop-highlight")
return True
def on_drop(self, drop_target, value, x, y):
# value is a Gio.File or a list of Gio.Files
GLib.idle_add(partial(self.process_drop_value, value))
return value
def process_drop_value(self, value):
if isinstance(value, Gio.File):
uris = [value]
elif isinstance(value, list):
@ -152,7 +117,6 @@ class RecoderWindow(Adw.ApplicationWindow):
for file in uris:
path = file.get_path()
if os.path.isdir(path):
# Add all supported video files from the dropped folder
for entry in os.scandir(path):
if entry.is_file() and entry.name.lower().endswith((".mp4", ".mov", ".mkv", ".avi")):
paths.append(entry.path)
@ -163,75 +127,38 @@ class RecoderWindow(Adw.ApplicationWindow):
return False
self.textview.remove_css_class("drop-highlight")
self.drop_hint.set_visible(False) # Hide hint
self.drop_hint.set_visible(False)
self.files_to_process = paths
self.textbuffer.set_text("\n".join(os.path.basename(p) for p in paths) + "\n")
self.start_transcoding()
return True
return False
def start_transcoding(self):
if self.is_processing:
if self.transcoder and self.transcoder.is_processing:
return
self.is_processing = True
self.progress.set_fraction(0.0)
self.progress.set_text("Starting transcoding...")
thread = threading.Thread(target=self.transcode_files)
thread.daemon = True
thread.start()
self.transcoder = TranscoderWorker(
self.files_to_process,
progress_callback=self.update_progress,
done_callback=self.notify_done,
)
self.transcoder.start()
def transcode_files(self):
total = len(self.files_to_process)
for idx, filepath in enumerate(self.files_to_process, start=1):
basename = os.path.basename(filepath)
self.update_progress_text(f"Processing {basename} ({idx}/{total})...")
success, output_path = transcode_file(filepath, os.path.join(os.path.dirname(filepath), "transcoded"))
if not success:
self.update_progress_text(f"Error transcoding {basename}")
else:
self.update_progress_text(f"Finished {basename}")
self.update_progress_fraction(idx / total)
self.is_processing = False
self.update_progress_text("All done!")
self.progress.set_fraction(1.0)
self.notify_done()
def update_progress_text(self, text):
GLib.idle_add(self.progress.set_show_text, True)
GLib.idle_add(self.progress.set_text, text)
def update_progress_fraction(self, fraction):
GLib.idle_add(self.progress.set_fraction, fraction)
def update_progress(self, text, fraction):
self.progress.set_show_text(True)
self.progress.set_text(text)
self.progress.set_fraction(fraction)
def notify_done(self):
# Visual cue - change progress bar color briefly
GLib.idle_add(self.progress.set_show_text, True)
GLib.idle_add(self.progress.set_text, "Transcoding Complete!")
# Audio cue
subprocess.Popen(["paplay", "/usr/share/sounds/freedesktop/stereo/complete.oga"])
class RecoderApp(Adw.Application):
def __init__(self):
super().__init__(application_id="net.jeena.recoder",
flags=Gio.ApplicationFlags.FLAGS_NONE)
self.window = None
def do_activate(self):
if not self.window:
self.window = RecoderWindow(application=self)
self.window.present()
def main():
app = RecoderApp()
return app.run(sys.argv)
if __name__ == "__main__":
sys.exit(main())
self.progress.set_show_text(True)
self.progress.set_text("Transcoding Complete!")
self.play_complete_sound()
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.")

View file

@ -1,16 +0,0 @@
import os
import subprocess
def transcode_file(input_path, output_dir):
os.makedirs(output_dir, exist_ok=True)
basename = os.path.basename(input_path)
output_path = os.path.join(output_dir, basename)
cmd = [
"ffmpeg", "-y", "-i", input_path,
"-c:v", "libx264", "-preset", "fast",
"-c:a", "aac", output_path
]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return result.returncode == 0, output_path