Add build workflow and requirements
This commit is contained in:
57
.gitea/workflows/build.yaml
Normal file
57
.gitea/workflows/build.yaml
Normal file
@@ -0,0 +1,57 @@
|
||||
name: Build Binaries
|
||||
run-name: ${{ gitea.actor }} is building binaries 🚀
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y python3-tk
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Build with PyInstaller
|
||||
run: |
|
||||
pyinstaller --onefile --windowed --name vhs-decode-gui-linux vhs_decode_gui.py
|
||||
|
||||
- name: Upload Linux Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: vhs-decode-gui-linux
|
||||
path: dist/vhs-decode-gui-linux
|
||||
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Build with PyInstaller
|
||||
run: |
|
||||
pyinstaller --onefile --windowed --name vhs-decode-gui-windows.exe vhs_decode_gui.py
|
||||
|
||||
- name: Upload Windows Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: vhs-decode-gui-windows.exe
|
||||
path: dist/vhs-decode-gui-windows.exe
|
||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
build/
|
||||
dist/
|
||||
*.spec
|
||||
.env
|
||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
pyinstaller
|
||||
25
session_summary.md
Normal file
25
session_summary.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Session Summary: VHS Decode GUI Development
|
||||
|
||||
This session focused on creating a graphical user interface (GUI) wrapper for the `vhs-decode` command-line application.
|
||||
|
||||
## Key Accomplishments:
|
||||
|
||||
1. **CLI Argument Investigation**: Explored the `vhs-decode` command-line interface to understand its various arguments, including input/output files, video systems (NTSC, PAL), tape formats, tape speeds, and advanced settings like frequency and threads.
|
||||
2. **Technology Choice**: Opted for Python with `tkinter` (using `ttk` for improved aesthetics) for the GUI development, balancing cross-platform compatibility with ease of development.
|
||||
3. **Virtual Environment Setup**: Guided the user to create and activate a Python virtual environment (`.venv`) to manage dependencies.
|
||||
4. **Basic GUI Implementation**: Developed the initial GUI structure including:
|
||||
* Input and output file/directory selection with browse buttons.
|
||||
* Core settings for Video System, Tape Format, Tape Speed, and RF Frequency (with CXADC option).
|
||||
* Advanced settings for Threads, Start Frame, Length, Overwrite, Chroma Trap, and Debugging.
|
||||
* "Start Decode Now" and "Stop Decode" buttons.
|
||||
* An output log area to display the command-line application's output.
|
||||
5. **Video System Refinement**: Modified the "Video System" dropdown to remove the "Default" option, ensuring users explicitly select a system (PAL, NTSC, PAL-M, NTSC-J) and setting "PAL" as the initial default.
|
||||
6. **Queue System Integration**: Implemented a job queuing mechanism, inspired by tools like Handbrake:
|
||||
* Added an "Add to Queue" button to capture current settings as a job.
|
||||
* Introduced a `ttk.Notebook` widget at the bottom, separating the output log into a "Log" tab and adding a new "Queue" tab.
|
||||
* The "Queue" tab features a `ttk.Treeview` to display queued jobs with their status (Pending, Running, Completed, Failed), input/output files, video system, and tape format.
|
||||
* Added "Start Queue", "Remove Selected", and "Clear Queue" controls for queue management.
|
||||
* Developed the logic to process queued jobs sequentially, updating their status in the UI and logging output to the "Log" tab.
|
||||
7. **Enhanced File Dialogs (Linux)**: Integrated `zenity` to provide a more functional and native-looking file and directory selection experience on Linux systems, allowing users to create new folders within the dialog. The application gracefully falls back to `tkinter.filedialog` on Windows (which uses native Windows dialogs) or if `zenity` is not available on Linux.
|
||||
|
||||
The resulting `vhs_decode_gui.py` script provides a user-friendly interface for interacting with the `vhs-decode` CLI application.
|
||||
507
vhs_decode_gui.py
Normal file
507
vhs_decode_gui.py
Normal file
@@ -0,0 +1,507 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox
|
||||
import subprocess
|
||||
import threading
|
||||
import queue
|
||||
import os
|
||||
|
||||
class VHSDecodeGUI:
|
||||
def __init__(self, master):
|
||||
self.master = master
|
||||
master.title("VHS Decode GUI")
|
||||
master.geometry("800x700")
|
||||
|
||||
self.process = None
|
||||
self.log_queue = queue.Queue()
|
||||
self.job_queue = []
|
||||
self.is_queue_running = False
|
||||
self.stop_queue_requested = False
|
||||
|
||||
self.create_widgets()
|
||||
self.master.after(100, self.poll_log_queue) # Start polling the log queue
|
||||
|
||||
def create_widgets(self):
|
||||
# Configure grid for responsiveness
|
||||
self.master.columnconfigure(0, weight=1)
|
||||
self.master.rowconfigure(5, weight=1)
|
||||
|
||||
# Input File Selection
|
||||
input_frame = ttk.LabelFrame(self.master, text="Input File")
|
||||
input_frame.grid(row=0, column=0, sticky="ew", padx=10, pady=5)
|
||||
input_frame.columnconfigure(0, weight=1)
|
||||
|
||||
self.input_file_path = tk.StringVar()
|
||||
self.input_entry = ttk.Entry(input_frame, textvariable=self.input_file_path)
|
||||
self.input_entry.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
|
||||
self.input_button = ttk.Button(input_frame, text="Browse", command=self.browse_input_file)
|
||||
self.input_button.grid(row=0, column=1, sticky="e", padx=5, pady=5)
|
||||
|
||||
# Output File Selection
|
||||
output_frame = ttk.LabelFrame(self.master, text="Output File (Base Name)")
|
||||
output_frame.grid(row=1, column=0, sticky="ew", padx=10, pady=5)
|
||||
output_frame.columnconfigure(0, weight=1)
|
||||
|
||||
self.output_file_path = tk.StringVar()
|
||||
self.output_entry = ttk.Entry(output_frame, textvariable=self.output_file_path)
|
||||
self.output_entry.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
|
||||
self.output_button = ttk.Button(output_frame, text="Browse", command=self.browse_output_file)
|
||||
self.output_button.grid(row=0, column=1, sticky="e", padx=5, pady=5)
|
||||
|
||||
# Core Settings
|
||||
settings_frame = ttk.LabelFrame(self.master, text="Core Settings")
|
||||
settings_frame.grid(row=2, column=0, sticky="ew", padx=10, pady=5)
|
||||
settings_frame.columnconfigure(1, weight=1)
|
||||
|
||||
# System
|
||||
ttk.Label(settings_frame, text="Video System:").grid(row=0, column=0, sticky="w", padx=5, pady=2)
|
||||
self.system_var = tk.StringVar(value="PAL")
|
||||
system_options = ["PAL", "NTSC", "PAL-M", "NTSC-J"]
|
||||
self.system_menu = ttk.OptionMenu(settings_frame, self.system_var, "PAL", *system_options)
|
||||
self.system_menu.grid(row=0, column=1, sticky="ew", padx=5, pady=2)
|
||||
|
||||
# Tape Format
|
||||
ttk.Label(settings_frame, text="Tape Format:").grid(row=1, column=0, sticky="w", padx=5, pady=2)
|
||||
self.tape_format_var = tk.StringVar(value="VHS")
|
||||
tape_formats = ["VHS", "SVHS", "HI8", "BETAMAX", "U-MATIC", "VIDEO2000", "UMATIC_HI", "VCR_LP", "VCR", "EIAJ", "VHSHQ", "VIDEO8", "BETAMAX_HIFI", "SVHS_ET", "QUADRUPLEX", "TYPEB", "SUPERBETA", "TYPEC"]
|
||||
self.tape_format_menu = ttk.OptionMenu(settings_frame, self.tape_format_var, *tape_formats)
|
||||
self.tape_format_menu.grid(row=1, column=1, sticky="ew", padx=5, pady=2)
|
||||
|
||||
# Tape Speed
|
||||
ttk.Label(settings_frame, text="Tape Speed:").grid(row=2, column=0, sticky="w", padx=5, pady=2)
|
||||
self.tape_speed_var = tk.StringVar(value="SP")
|
||||
tape_speeds = ["SP", "LP", "SLP", "EP", "VP"]
|
||||
self.tape_speed_menu = ttk.OptionMenu(settings_frame, self.tape_speed_var, *tape_speeds)
|
||||
self.tape_speed_menu.grid(row=2, column=1, sticky="ew", padx=5, pady=2)
|
||||
|
||||
# Frequency
|
||||
ttk.Label(settings_frame, text="RF Frequency (MHz):").grid(row=3, column=0, sticky="w", padx=5, pady=2)
|
||||
self.frequency_var = tk.StringVar(value="40")
|
||||
self.frequency_entry = ttk.Entry(settings_frame, textvariable=self.frequency_var)
|
||||
self.frequency_entry.grid(row=3, column=1, sticky="ew", padx=5, pady=2)
|
||||
self.cxadc_var = tk.BooleanVar(value=False)
|
||||
self.cxadc_check = ttk.Checkbutton(settings_frame, text="Use CXADC frequency (~28.63 MHz)", variable=self.cxadc_var)
|
||||
self.cxadc_check.grid(row=4, column=0, columnspan=2, sticky="w", padx=5, pady=2)
|
||||
|
||||
# Advanced Settings
|
||||
advanced_frame = ttk.LabelFrame(self.master, text="Advanced Settings")
|
||||
advanced_frame.grid(row=3, column=0, sticky="ew", padx=10, pady=5)
|
||||
advanced_frame.columnconfigure(0, weight=1)
|
||||
|
||||
ttk.Label(advanced_frame, text="Threads:").grid(row=0, column=0, sticky="w", padx=5, pady=2)
|
||||
self.threads_var = tk.IntVar(value=os.cpu_count() or 1)
|
||||
self.threads_spinbox = ttk.Spinbox(advanced_frame, from_=1, to=os.cpu_count() * 2 or 16, textvariable=self.threads_var, width=5)
|
||||
self.threads_spinbox.grid(row=0, column=1, sticky="ew", padx=5, pady=2)
|
||||
|
||||
ttk.Label(advanced_frame, text="Start Frame:").grid(row=1, column=0, sticky="w", padx=5, pady=2)
|
||||
self.start_frame_var = tk.IntVar(value=0)
|
||||
self.start_frame_entry = ttk.Entry(advanced_frame, textvariable=self.start_frame_var, width=10)
|
||||
self.start_frame_entry.grid(row=1, column=1, sticky="ew", padx=5, pady=2)
|
||||
|
||||
ttk.Label(advanced_frame, text="Length (frames):").grid(row=2, column=0, sticky="w", padx=5, pady=2)
|
||||
self.length_var = tk.IntVar(value=0)
|
||||
self.length_entry = ttk.Entry(advanced_frame, textvariable=self.length_var, width=10)
|
||||
self.length_entry.grid(row=2, column=1, sticky="ew", padx=5, pady=2)
|
||||
|
||||
self.overwrite_var = tk.BooleanVar(value=False)
|
||||
self.overwrite_check = ttk.Checkbutton(advanced_frame, text="Overwrite existing files", variable=self.overwrite_var)
|
||||
self.overwrite_check.grid(row=3, column=0, columnspan=2, sticky="w", padx=5, pady=2)
|
||||
|
||||
self.chroma_trap_var = tk.BooleanVar(value=False)
|
||||
self.chroma_trap_check = ttk.Checkbutton(advanced_frame, text="Enable Chroma Trap", variable=self.chroma_trap_var)
|
||||
self.chroma_trap_check.grid(row=4, column=0, columnspan=2, sticky="w", padx=5, pady=2)
|
||||
|
||||
self.debug_var = tk.BooleanVar(value=False)
|
||||
self.debug_check = ttk.Checkbutton(advanced_frame, text="Enable Debug Logging", variable=self.debug_var)
|
||||
self.debug_check.grid(row=5, column=0, columnspan=2, sticky="w", padx=5, pady=2)
|
||||
|
||||
self.ire0_adjust_var = tk.BooleanVar(value=False)
|
||||
self.ire0_adjust_check = ttk.Checkbutton(advanced_frame, text="ire0_adjust", variable=self.ire0_adjust_var)
|
||||
self.ire0_adjust_check.grid(row=6, column=0, columnspan=2, sticky="w", padx=5, pady=2)
|
||||
|
||||
self.recheck_phase_var = tk.BooleanVar(value=False)
|
||||
self.recheck_phase_check = ttk.Checkbutton(advanced_frame, text="recheck_phase", variable=self.recheck_phase_var)
|
||||
self.recheck_phase_check.grid(row=7, column=0, columnspan=2, sticky="w", padx=5, pady=2)
|
||||
|
||||
|
||||
# Control Buttons
|
||||
button_frame = ttk.Frame(self.master)
|
||||
button_frame.grid(row=4, column=0, sticky="ew", padx=10, pady=5)
|
||||
button_frame.columnconfigure(0, weight=1)
|
||||
button_frame.columnconfigure(1, weight=1)
|
||||
button_frame.columnconfigure(2, weight=1)
|
||||
|
||||
self.start_button = ttk.Button(button_frame, text="Start Decode Now", command=self.run_current_job)
|
||||
self.start_button.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
|
||||
|
||||
self.add_queue_button = ttk.Button(button_frame, text="Add to Queue", command=self.add_to_queue)
|
||||
self.add_queue_button.grid(row=0, column=1, sticky="ew", padx=5, pady=5)
|
||||
|
||||
self.stop_button = ttk.Button(button_frame, text="Stop", command=self.stop_decode, state=tk.DISABLED)
|
||||
self.stop_button.grid(row=0, column=2, sticky="ew", padx=5, pady=5)
|
||||
|
||||
|
||||
# Bottom Area: Notebook for Queue and Log
|
||||
self.notebook = ttk.Notebook(self.master)
|
||||
self.notebook.grid(row=5, column=0, sticky="nsew", padx=10, pady=5)
|
||||
|
||||
# Queue Tab
|
||||
queue_frame = ttk.Frame(self.notebook)
|
||||
self.notebook.add(queue_frame, text="Queue")
|
||||
queue_frame.columnconfigure(0, weight=1)
|
||||
queue_frame.rowconfigure(0, weight=1)
|
||||
|
||||
# Queue Treeview
|
||||
columns = ("status", "input", "output", "system", "format")
|
||||
self.queue_tree = ttk.Treeview(queue_frame, columns=columns, show="headings", height=8)
|
||||
self.queue_tree.heading("status", text="Status")
|
||||
self.queue_tree.heading("input", text="Input File")
|
||||
self.queue_tree.heading("output", text="Output Base")
|
||||
self.queue_tree.heading("system", text="System")
|
||||
self.queue_tree.heading("format", text="Format")
|
||||
|
||||
self.queue_tree.column("status", width=80)
|
||||
self.queue_tree.column("input", width=200)
|
||||
self.queue_tree.column("output", width=150)
|
||||
self.queue_tree.column("system", width=60)
|
||||
self.queue_tree.column("format", width=60)
|
||||
|
||||
self.queue_tree.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
|
||||
|
||||
queue_scrollbar = ttk.Scrollbar(queue_frame, orient="vertical", command=self.queue_tree.yview)
|
||||
queue_scrollbar.grid(row=0, column=1, sticky="ns")
|
||||
self.queue_tree.configure(yscrollcommand=queue_scrollbar.set)
|
||||
|
||||
# Queue Controls
|
||||
queue_controls = ttk.Frame(queue_frame)
|
||||
queue_controls.grid(row=1, column=0, columnspan=2, sticky="ew", padx=5, pady=5)
|
||||
|
||||
self.start_queue_button = ttk.Button(queue_controls, text="Start Queue", command=self.start_queue)
|
||||
self.start_queue_button.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.remove_queue_button = ttk.Button(queue_controls, text="Remove Selected", command=self.remove_from_queue)
|
||||
self.remove_queue_button.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.clear_queue_button = ttk.Button(queue_controls, text="Clear Queue", command=self.clear_queue)
|
||||
self.clear_queue_button.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
|
||||
# Log Tab
|
||||
log_frame = ttk.Frame(self.notebook)
|
||||
self.notebook.add(log_frame, text="Log")
|
||||
log_frame.columnconfigure(0, weight=1)
|
||||
log_frame.rowconfigure(0, weight=1)
|
||||
|
||||
self.log_text = tk.Text(log_frame, state=tk.DISABLED, wrap=tk.WORD, height=10)
|
||||
self.log_text.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
|
||||
log_scrollbar = ttk.Scrollbar(log_frame, command=self.log_text.yview)
|
||||
log_scrollbar.grid(row=0, column=1, sticky="ns")
|
||||
self.log_text['yscrollcommand'] = log_scrollbar.set
|
||||
|
||||
def _linux_file_dialog(self, title, filetypes):
|
||||
# Construct zenity command
|
||||
cmd = ["zenity", "--file-selection", f"--title={title}"]
|
||||
|
||||
# Add file filters
|
||||
# filetypes is a list of tuples: [("Name", "*.ext *.ext2"), ...]
|
||||
for name, pattern in filetypes:
|
||||
# Zenity format: --file-filter=Name | *.ext *.ext2
|
||||
filter_str = f"{name} | {pattern}"
|
||||
cmd.append(f"--file-filter={filter_str}")
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
if result.returncode == 0:
|
||||
return result.stdout.strip()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
return None
|
||||
|
||||
def _linux_dir_dialog(self, title, initialdir=None):
|
||||
cmd = ["zenity", "--file-selection", "--directory", f"--title={title}"]
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
if result.returncode == 0:
|
||||
return result.stdout.strip()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
return None
|
||||
|
||||
def browse_input_file(self):
|
||||
if os.name != 'nt' and os.path.exists("/usr/bin/zenity"):
|
||||
file_path = self._linux_file_dialog(
|
||||
title="Select Input RF File",
|
||||
filetypes=[("RF Files", "*.u8 *.u16"), ("All Files", "*")]
|
||||
)
|
||||
else:
|
||||
file_path = filedialog.askopenfilename(
|
||||
title="Select Input RF File",
|
||||
filetypes=[("RF Files", "*.u8 *.u16"), ("All Files", "*.*")]
|
||||
)
|
||||
|
||||
if file_path:
|
||||
self.input_file_path.set(file_path)
|
||||
|
||||
def browse_output_file(self):
|
||||
initial_dir = os.path.dirname(self.input_file_path.get()) if self.input_file_path.get() else ""
|
||||
initial_file = os.path.splitext(os.path.basename(self.input_file_path.get()))[0] if self.input_file_path.get() else "output"
|
||||
|
||||
if os.name != 'nt' and os.path.exists("/usr/bin/zenity"):
|
||||
dir_path = self._linux_dir_dialog(title="Select Output Directory", initialdir=initial_dir)
|
||||
else:
|
||||
dir_path = filedialog.askdirectory(title="Select Output Directory", initialdir=initial_dir)
|
||||
|
||||
if dir_path:
|
||||
self.output_file_path.set(os.path.join(dir_path, initial_file))
|
||||
|
||||
def get_command_args(self):
|
||||
input_file = self.input_file_path.get()
|
||||
output_file = self.output_file_path.get()
|
||||
|
||||
if not input_file:
|
||||
return None, "Please select an input file."
|
||||
if not output_file:
|
||||
return None, "Please specify an output file base name."
|
||||
|
||||
command = []
|
||||
command.append(input_file)
|
||||
command.append(output_file)
|
||||
|
||||
system_selection = self.system_var.get()
|
||||
if system_selection == "NTSC":
|
||||
command.append("-n")
|
||||
elif system_selection == "PAL":
|
||||
command.append("-p")
|
||||
elif system_selection == "PAL-M":
|
||||
command.append("--pm")
|
||||
elif system_selection == "NTSC-J":
|
||||
command.append("--NTSCJ")
|
||||
|
||||
command.extend(["--tf", self.tape_format_var.get()])
|
||||
command.extend(["--ts", self.tape_speed_var.get()])
|
||||
|
||||
if self.cxadc_var.get():
|
||||
command.append("--cxadc")
|
||||
else:
|
||||
try:
|
||||
freq_val = float(self.frequency_var.get())
|
||||
if freq_val > 0:
|
||||
command.extend(["-f", self.frequency_var.get()])
|
||||
except ValueError:
|
||||
pass # Ignore invalid frequency, let default handle or it was already warned?
|
||||
# Ideally validation happens before calling this.
|
||||
|
||||
threads = self.threads_var.get()
|
||||
if threads > 0:
|
||||
command.extend(["-t", str(threads)])
|
||||
|
||||
start_frame = self.start_frame_var.get()
|
||||
if start_frame > 0:
|
||||
command.extend(["-s", str(start_frame)])
|
||||
|
||||
length = self.length_var.get()
|
||||
if length > 0:
|
||||
command.extend(["-l", str(length)])
|
||||
|
||||
if self.overwrite_var.get():
|
||||
command.append("--overwrite")
|
||||
if self.chroma_trap_var.get():
|
||||
command.append("--ct")
|
||||
if self.debug_var.get():
|
||||
command.append("--debug")
|
||||
if self.ire0_adjust_var.get():
|
||||
command.append("--ire0_adjust")
|
||||
if self.recheck_phase_var.get():
|
||||
command.append("--recheck_phase")
|
||||
|
||||
job_details = {
|
||||
'input': input_file,
|
||||
'output': output_file,
|
||||
'system': system_selection,
|
||||
'format': self.tape_format_var.get(),
|
||||
'command': command
|
||||
}
|
||||
return job_details, None
|
||||
|
||||
def add_to_queue(self):
|
||||
job_details, error = self.get_command_args()
|
||||
if error:
|
||||
messagebox.showerror("Error", error)
|
||||
return
|
||||
|
||||
self.job_queue.append(job_details)
|
||||
self.queue_tree.insert("", tk.END, values=("Pending", job_details['input'], job_details['output'], job_details['system'], job_details['format']))
|
||||
self.notebook.select(0) # Switch to queue tab
|
||||
|
||||
def remove_from_queue(self):
|
||||
selected_item = self.queue_tree.selection()
|
||||
if not selected_item:
|
||||
return
|
||||
|
||||
# We need to map treeview item to list index.
|
||||
# Since we append to both, indices should match.
|
||||
# However, if we remove from middle, we need to be careful.
|
||||
# It's better to just rebuild the list or store the ID.
|
||||
# Simple approach: Iterate backwards to avoid index shifting issues if multiple selected?
|
||||
# Treeview selection returns tuple of IDs.
|
||||
|
||||
# Let's find the index of the selected item
|
||||
for item in selected_item:
|
||||
index = self.queue_tree.index(item)
|
||||
del self.job_queue[index]
|
||||
self.queue_tree.delete(item)
|
||||
|
||||
def clear_queue(self):
|
||||
self.job_queue = []
|
||||
self.queue_tree.delete(*self.queue_tree.get_children())
|
||||
|
||||
def run_current_job(self):
|
||||
job_details, error = self.get_command_args()
|
||||
if error:
|
||||
messagebox.showerror("Error", error)
|
||||
return
|
||||
|
||||
self.notebook.select(1) # Switch to Log tab
|
||||
threading.Thread(target=self._run_single_job_thread, args=(job_details['command'],)).start()
|
||||
|
||||
def start_queue(self):
|
||||
if self.is_queue_running:
|
||||
return
|
||||
if not self.job_queue:
|
||||
messagebox.showinfo("Queue", "Queue is empty.")
|
||||
return
|
||||
|
||||
self.is_queue_running = True
|
||||
self.stop_queue_requested = False
|
||||
self.start_queue_button.config(state=tk.DISABLED)
|
||||
self.remove_queue_button.config(state=tk.DISABLED)
|
||||
self.clear_queue_button.config(state=tk.DISABLED)
|
||||
self.add_queue_button.config(state=tk.DISABLED)
|
||||
self.start_button.config(state=tk.DISABLED)
|
||||
|
||||
self.notebook.select(1) # Switch to Log tab
|
||||
threading.Thread(target=self._run_queue_thread).start()
|
||||
|
||||
def _run_subprocess(self, command):
|
||||
if os.name == 'nt':
|
||||
cli_command = "decode"
|
||||
else:
|
||||
cli_command = "vhs-decode"
|
||||
|
||||
full_command = [cli_command] + command
|
||||
|
||||
try:
|
||||
self.process = subprocess.Popen(
|
||||
full_command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
universal_newlines=True
|
||||
)
|
||||
self.stop_button.config(state=tk.NORMAL)
|
||||
|
||||
for line in self.process.stdout:
|
||||
self.log_queue.put(line)
|
||||
for line in self.process.stderr:
|
||||
self.log_queue.put(line)
|
||||
|
||||
self.process.wait()
|
||||
return self.process.returncode
|
||||
except FileNotFoundError:
|
||||
self.log_queue.put(f"Error: '{cli_command}' command not found.\n")
|
||||
return -1
|
||||
except Exception as e:
|
||||
self.log_queue.put(f"An unexpected error occurred: {e}\n")
|
||||
return -1
|
||||
finally:
|
||||
self.process = None
|
||||
self.master.after(0, lambda: self.stop_button.config(state=tk.DISABLED))
|
||||
|
||||
def _run_single_job_thread(self, command):
|
||||
self.start_button.config(state=tk.DISABLED)
|
||||
self.log_queue.put("--- Starting Decode Process ---\n")
|
||||
|
||||
self._run_subprocess(command)
|
||||
|
||||
self.log_queue.put("\n--- Process Finished ---\n")
|
||||
self.master.after(0, lambda: self.start_button.config(state=tk.NORMAL))
|
||||
|
||||
def _run_queue_thread(self):
|
||||
# Iterate through a copy or just index?
|
||||
# We want to process items that are "Pending".
|
||||
|
||||
items = self.queue_tree.get_children()
|
||||
for index, item_id in enumerate(items):
|
||||
if self.stop_queue_requested:
|
||||
self.log_queue.put("\n--- Queue Execution Stopped by User ---\n")
|
||||
break
|
||||
|
||||
# Check status
|
||||
values = self.queue_tree.item(item_id, "values")
|
||||
if values[0] == "Completed":
|
||||
continue
|
||||
|
||||
# Update status to Running
|
||||
new_values = ("Running",) + values[1:]
|
||||
self.master.after(0, lambda i=item_id, v=new_values: self.queue_tree.item(i, values=v))
|
||||
|
||||
# Get command
|
||||
job = self.job_queue[index]
|
||||
self.log_queue.put(f"\n--- Starting Job {index + 1}/{len(items)}: {job['input']} ---\n")
|
||||
|
||||
# Run
|
||||
return_code = self._run_subprocess(job['command'])
|
||||
|
||||
# Update Status
|
||||
status = "Completed" if return_code == 0 else "Failed"
|
||||
final_values = (status,) + values[1:]
|
||||
self.master.after(0, lambda i=item_id, v=final_values: self.queue_tree.item(i, values=v))
|
||||
|
||||
if return_code != 0:
|
||||
self.log_queue.put(f"Job failed with code {return_code}. Continuing queue...\n")
|
||||
# Optionally break? For now continue.
|
||||
|
||||
self.is_queue_running = False
|
||||
self.master.after(0, lambda: self.start_queue_button.config(state=tk.NORMAL))
|
||||
self.master.after(0, lambda: self.remove_queue_button.config(state=tk.NORMAL))
|
||||
self.master.after(0, lambda: self.clear_queue_button.config(state=tk.NORMAL))
|
||||
self.master.after(0, lambda: self.add_queue_button.config(state=tk.NORMAL))
|
||||
self.master.after(0, lambda: self.start_button.config(state=tk.NORMAL))
|
||||
|
||||
def stop_decode(self):
|
||||
if self.is_queue_running:
|
||||
self.stop_queue_requested = True
|
||||
|
||||
if self.process and self.process.poll() is None:
|
||||
self.log_queue.put("\n--- Attempting to terminate process... ---\n")
|
||||
try:
|
||||
os.killpg(os.getpgid(self.process.pid), subprocess.SIGTERM)
|
||||
self.process.wait(timeout=5)
|
||||
self.log_queue.put("--- Process terminated. ---\n")
|
||||
except Exception as e:
|
||||
if self.process:
|
||||
self.process.kill()
|
||||
self.log_queue.put(f"Error terminating process: {e}. Force killing...\n")
|
||||
else:
|
||||
self.log_queue.put("\n--- No active process to stop. ---\n")
|
||||
|
||||
def poll_log_queue(self):
|
||||
while not self.log_queue.empty():
|
||||
try:
|
||||
line = self.log_queue.get_nowait()
|
||||
self.log_text.config(state=tk.NORMAL)
|
||||
self.log_text.insert(tk.END, line)
|
||||
self.log_text.see(tk.END)
|
||||
self.log_text.config(state=tk.DISABLED)
|
||||
except queue.Empty:
|
||||
pass
|
||||
self.master.after(100, self.poll_log_queue)
|
||||
|
||||
def main():
|
||||
root = tk.Tk()
|
||||
app = VHSDecodeGUI(root)
|
||||
root.mainloop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user