633 lines
28 KiB
Python
633 lines
28 KiB
Python
import tkinter as tk
|
|
from tkinter import ttk, filedialog, messagebox
|
|
import threading
|
|
import subprocess
|
|
import os
|
|
import json
|
|
import psutil
|
|
from datetime import datetime
|
|
import cv2
|
|
from PIL import Image, ImageTk
|
|
import ffmpeg
|
|
import sv_ttk
|
|
|
|
class VodKeeper:
|
|
def __init__(self, root):
|
|
self.root = root
|
|
self.root.title("VodKeeper - Video Processing Workflow")
|
|
self.root.geometry("1200x800")
|
|
|
|
# Dark mode colors
|
|
self.colors = {
|
|
'bg': '#2b2b2b',
|
|
'fg': '#e0e0e0',
|
|
'select_bg': '#0078d4',
|
|
'select_fg': '#ffffff',
|
|
'entry_bg': '#3c3c3c',
|
|
'entry_fg': '#e0e0e0',
|
|
'button_bg': '#404040',
|
|
'button_fg': '#e0e0e0',
|
|
'frame_bg': '#383838',
|
|
'text_bg': '#1e1e1e',
|
|
'text_fg': '#e0e0e0'
|
|
}
|
|
|
|
# Configuration
|
|
self.config = {
|
|
'input_path': 'R:\\Videos\\mux\\input',
|
|
'output_path': 'R:\\Videos\\mux\\output',
|
|
'logs_path': 'R:\\Videos\\mux\\logs',
|
|
'codec': 'h265_nvenc',
|
|
'preset': 'p7',
|
|
'cq': 0,
|
|
'auto_bitrate': True,
|
|
'preview_duration': 60
|
|
}
|
|
|
|
# Render state
|
|
self.current_process = None
|
|
self.render_queue = []
|
|
self.is_rendering = False
|
|
|
|
self.setup_ui()
|
|
self.load_config()
|
|
self.refresh_files() # Populate file list on startup
|
|
|
|
def setup_dark_theme(self):
|
|
# Configure root window
|
|
self.root.configure(bg=self.colors['bg'])
|
|
|
|
# Configure ttk styles for dark theme
|
|
style = ttk.Style()
|
|
style.theme_use('clam')
|
|
|
|
# Configure Notebook (tabs)
|
|
style.configure('TNotebook', background=self.colors['bg'], borderwidth=0)
|
|
style.configure('TNotebook.Tab', background=self.colors['frame_bg'], foreground=self.colors['fg'],
|
|
padding=[10, 4])
|
|
style.map('TNotebook.Tab', background=[('selected', self.colors['select_bg']),
|
|
('active', self.colors['select_bg'])])
|
|
|
|
# Configure Frames
|
|
style.configure('TFrame', background=self.colors['bg'], borderwidth=0)
|
|
style.configure('TLabelFrame', background=self.colors['bg'], foreground=self.colors['fg'],
|
|
borderwidth=1, relief='solid')
|
|
style.configure('TLabelFrame.Label', background=self.colors['bg'], foreground=self.colors['fg'])
|
|
|
|
# Configure Labels
|
|
style.configure('TLabel', background=self.colors['bg'], foreground=self.colors['fg'])
|
|
|
|
# Configure Buttons
|
|
style.configure('TButton', background=self.colors['button_bg'], foreground=self.colors['button_fg'],
|
|
borderwidth=1, relief='solid')
|
|
style.map('TButton', background=[('active', self.colors['select_bg']),
|
|
('pressed', self.colors['entry_bg'])])
|
|
|
|
# Configure Entry widgets
|
|
style.configure('TEntry', fieldbackground=self.colors['entry_bg'], foreground=self.colors['entry_fg'],
|
|
borderwidth=1, insertcolor=self.colors['entry_fg'])
|
|
style.map('TEntry', focuscolor=[('!focus', self.colors['entry_bg'])])
|
|
|
|
# Configure Combobox
|
|
style.configure('TCombobox', fieldbackground=self.colors['entry_bg'], foreground=self.colors['entry_fg'],
|
|
background=self.colors['entry_bg'], borderwidth=1, arrowcolor=self.colors['fg'])
|
|
style.map('TCombobox', fieldbackground=[('readonly', self.colors['entry_bg'])],
|
|
background=[('readonly', self.colors['entry_bg'])])
|
|
|
|
# Configure Spinbox
|
|
style.configure('TSpinbox', fieldbackground=self.colors['entry_bg'], foreground=self.colors['entry_fg'],
|
|
background=self.colors['entry_bg'], borderwidth=1, arrowcolor=self.colors['fg'])
|
|
|
|
# Configure Checkbutton
|
|
style.configure('TCheckbutton', background=self.colors['bg'], foreground=self.colors['fg'],
|
|
focuscolor=self.colors['bg'])
|
|
|
|
# Configure Progressbar
|
|
style.configure('TProgressbar', background=self.colors['select_bg'], troughcolor=self.colors['entry_bg'],
|
|
borderwidth=1, lightcolor=self.colors['select_bg'], darkcolor=self.colors['select_bg'])
|
|
|
|
# Configure Treeview
|
|
style.configure('Treeview', background=self.colors['entry_bg'], foreground=self.colors['entry_fg'],
|
|
fieldbackground=self.colors['entry_bg'], borderwidth=1)
|
|
style.configure('Treeview.Heading', background=self.colors['frame_bg'], foreground=self.colors['fg'],
|
|
borderwidth=1, relief='solid')
|
|
style.map('Treeview', background=[('selected', self.colors['select_bg'])],
|
|
foreground=[('selected', self.colors['select_fg'])])
|
|
|
|
# Configure Scrollbar
|
|
style.configure('Vertical.TScrollbar', background=self.colors['frame_bg'],
|
|
troughcolor=self.colors['entry_bg'], borderwidth=1,
|
|
arrowcolor=self.colors['fg'], darkcolor=self.colors['frame_bg'],
|
|
lightcolor=self.colors['frame_bg'])
|
|
style.map('Vertical.TScrollbar', background=[('active', self.colors['select_bg'])])
|
|
|
|
style.configure('Horizontal.TScrollbar', background=self.colors['frame_bg'],
|
|
troughcolor=self.colors['entry_bg'], borderwidth=1,
|
|
arrowcolor=self.colors['fg'], darkcolor=self.colors['frame_bg'],
|
|
lightcolor=self.colors['frame_bg'])
|
|
style.map('Horizontal.TScrollbar', background=[('active', self.colors['select_bg'])])
|
|
|
|
def setup_ui(self):
|
|
# Create notebook for tabs
|
|
self.notebook = ttk.Notebook(self.root)
|
|
self.notebook.pack(fill='both', expand=True, padx=10, pady=10)
|
|
|
|
# Input Tab
|
|
self.setup_input_tab()
|
|
|
|
# Render Tab
|
|
self.setup_render_tab()
|
|
|
|
# QC Tab
|
|
self.setup_qc_tab()
|
|
|
|
def setup_input_tab(self):
|
|
input_frame = ttk.Frame(self.notebook)
|
|
self.notebook.add(input_frame, text="Input")
|
|
|
|
# Create main horizontal layout
|
|
main_frame = ttk.Frame(input_frame)
|
|
main_frame.pack(fill='both', expand=True, padx=10, pady=5)
|
|
main_frame.columnconfigure(0, weight=3) # File selection gets more space
|
|
main_frame.columnconfigure(1, weight=2) # File info gets proportional space
|
|
main_frame.rowconfigure(0, weight=1) # Allow vertical expansion
|
|
|
|
# File selection (left side)
|
|
file_frame = ttk.LabelFrame(main_frame, text="File Selection", padding=10)
|
|
file_frame.grid(row=0, column=0, sticky='nsew', padx=(0,5))
|
|
file_frame.columnconfigure(0, weight=1)
|
|
file_frame.columnconfigure(1, weight=1)
|
|
file_frame.columnconfigure(2, weight=0)
|
|
file_frame.rowconfigure(2, weight=1) # Make file list expand vertically
|
|
|
|
ttk.Label(file_frame, text="Input Directory:").grid(row=0, column=0, sticky='w', pady=2)
|
|
self.input_path_var = tk.StringVar(value=self.config['input_path'])
|
|
ttk.Entry(file_frame, textvariable=self.input_path_var, width=50).grid(row=0, column=1, padx=5, pady=2)
|
|
ttk.Button(file_frame, text="Browse", command=self.browse_input).grid(row=0, column=2, padx=5, pady=2)
|
|
|
|
# File list spanning full width
|
|
ttk.Label(file_frame, text="Available Files:").grid(row=1, column=0, sticky='w', pady=(10,2))
|
|
|
|
# Create frame for listbox with scrollbar
|
|
listbox_frame = ttk.Frame(file_frame)
|
|
listbox_frame.grid(row=2, column=0, columnspan=3, sticky='nsew', pady=2)
|
|
listbox_frame.rowconfigure(0, weight=1)
|
|
listbox_frame.columnconfigure(0, weight=1)
|
|
|
|
self.file_listbox = tk.Listbox(listbox_frame,
|
|
bg=self.colors['entry_bg'], fg=self.colors['entry_fg'],
|
|
selectbackground=self.colors['select_bg'],
|
|
selectforeground=self.colors['select_fg'])
|
|
self.file_listbox.grid(row=0, column=0, sticky='nsew')
|
|
self.file_listbox.bind('<<ListboxSelect>>', self.on_file_select)
|
|
|
|
# Add scrollbar to listbox
|
|
listbox_scrollbar = ttk.Scrollbar(listbox_frame, orient='vertical', command=self.file_listbox.yview)
|
|
listbox_scrollbar.grid(row=0, column=1, sticky='ns')
|
|
self.file_listbox.config(yscrollcommand=listbox_scrollbar.set)
|
|
|
|
# Refresh button
|
|
ttk.Button(file_frame, text="Refresh Files", command=self.refresh_files).grid(row=3, column=0, pady=5)
|
|
|
|
# File information (right side)
|
|
info_frame = ttk.LabelFrame(main_frame, text="File Information", padding=10)
|
|
info_frame.grid(row=0, column=1, sticky='nsew', padx=(5,0))
|
|
info_frame.rowconfigure(0, weight=1)
|
|
info_frame.columnconfigure(0, weight=1)
|
|
|
|
# Create Text widget with matching scrollbar style
|
|
self.file_info_text = tk.Text(info_frame,
|
|
bg=self.colors['text_bg'], fg=self.colors['text_fg'],
|
|
insertbackground=self.colors['text_fg'], wrap='word')
|
|
self.file_info_text.grid(row=0, column=0, sticky='nsew')
|
|
|
|
# Add matching scrollbar to file info
|
|
info_scrollbar = ttk.Scrollbar(info_frame, orient='vertical', command=self.file_info_text.yview)
|
|
info_scrollbar.grid(row=0, column=1, sticky='ns')
|
|
self.file_info_text.config(yscrollcommand=info_scrollbar.set)
|
|
|
|
# Configure the info frame columns
|
|
info_frame.columnconfigure(0, weight=1)
|
|
info_frame.columnconfigure(1, weight=0)
|
|
|
|
# Encoding settings
|
|
settings_frame = ttk.LabelFrame(input_frame, text="Encoding Settings", padding=10)
|
|
settings_frame.pack(fill='x', padx=10, pady=5)
|
|
|
|
# Codec selection
|
|
ttk.Label(settings_frame, text="Codec:").grid(row=0, column=0, sticky='w', pady=2)
|
|
self.codec_var = tk.StringVar(value=self.config['codec'])
|
|
codec_combo = ttk.Combobox(settings_frame, textvariable=self.codec_var,
|
|
values=['h265_nvenc', 'vp9', 'av1'], state='readonly')
|
|
codec_combo.grid(row=0, column=1, sticky='w', padx=5, pady=2)
|
|
|
|
# Preset selection
|
|
ttk.Label(settings_frame, text="Preset:").grid(row=1, column=0, sticky='w', pady=2)
|
|
self.preset_var = tk.StringVar(value=self.config['preset'])
|
|
preset_combo = ttk.Combobox(settings_frame, textvariable=self.preset_var,
|
|
values=['p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7'], state='readonly')
|
|
preset_combo.grid(row=1, column=1, sticky='w', padx=5, pady=2)
|
|
|
|
# CQ setting
|
|
ttk.Label(settings_frame, text="CQ:").grid(row=2, column=0, sticky='w', pady=2)
|
|
self.cq_var = tk.IntVar(value=self.config['cq'])
|
|
cq_spin = ttk.Spinbox(settings_frame, from_=0, to=51, textvariable=self.cq_var, width=10)
|
|
cq_spin.grid(row=2, column=1, sticky='w', padx=5, pady=2)
|
|
|
|
# Bitrate settings
|
|
bitrate_frame = ttk.LabelFrame(settings_frame, text="Bitrate Settings", padding=5)
|
|
bitrate_frame.grid(row=3, column=0, columnspan=2, sticky='ew', pady=10)
|
|
|
|
self.auto_bitrate_var = tk.BooleanVar(value=self.config['auto_bitrate'])
|
|
ttk.Checkbutton(bitrate_frame, text="Auto-detect bitrate cap",
|
|
variable=self.auto_bitrate_var).grid(row=0, column=0, sticky='w')
|
|
|
|
ttk.Label(bitrate_frame, text="Manual bitrate cap (Mbps):").grid(row=1, column=0, sticky='w', pady=2)
|
|
self.manual_bitrate_var = tk.DoubleVar(value=0)
|
|
manual_bitrate_entry = ttk.Entry(bitrate_frame, textvariable=self.manual_bitrate_var, width=10)
|
|
manual_bitrate_entry.grid(row=1, column=1, padx=5, pady=2)
|
|
|
|
|
|
|
|
# Action buttons
|
|
button_frame = ttk.Frame(input_frame)
|
|
button_frame.pack(fill='x', padx=10, pady=10)
|
|
|
|
ttk.Button(button_frame, text="Add to Queue", command=self.add_to_queue).pack(side='left', padx=5)
|
|
ttk.Button(button_frame, text="Preview (1 min)", command=self.preview_encode).pack(side='left', padx=5)
|
|
ttk.Button(button_frame, text="Save Settings", command=self.save_config).pack(side='right', padx=5)
|
|
|
|
def setup_render_tab(self):
|
|
render_frame = ttk.Frame(self.notebook)
|
|
self.notebook.add(render_frame, text="Render")
|
|
|
|
# Progress section
|
|
progress_frame = ttk.LabelFrame(render_frame, text="Render Progress", padding=10)
|
|
progress_frame.pack(fill='x', padx=10, pady=5)
|
|
|
|
self.progress_var = tk.DoubleVar()
|
|
self.progress_bar = ttk.Progressbar(progress_frame, variable=self.progress_var, maximum=100)
|
|
self.progress_bar.pack(fill='x', pady=5)
|
|
|
|
self.progress_label = ttk.Label(progress_frame, text="Ready")
|
|
self.progress_label.pack()
|
|
|
|
# Control buttons
|
|
control_frame = ttk.Frame(progress_frame)
|
|
control_frame.pack(pady=10)
|
|
|
|
self.start_button = ttk.Button(control_frame, text="Start Rendering", command=self.start_rendering)
|
|
self.start_button.pack(side='left', padx=5)
|
|
|
|
self.pause_button = ttk.Button(control_frame, text="Pause", command=self.pause_rendering, state='disabled')
|
|
self.pause_button.pack(side='left', padx=5)
|
|
|
|
self.stop_button = ttk.Button(control_frame, text="Stop", command=self.stop_rendering, state='disabled')
|
|
self.stop_button.pack(side='left', padx=5)
|
|
|
|
# Queue section
|
|
queue_frame = ttk.LabelFrame(render_frame, text="Render Queue", padding=10)
|
|
queue_frame.pack(fill='both', expand=True, padx=10, pady=5)
|
|
|
|
self.queue_tree = ttk.Treeview(queue_frame, columns=('Status', 'Progress'), show='tree headings')
|
|
self.queue_tree.heading('#0', text='File')
|
|
self.queue_tree.heading('Status', text='Status')
|
|
self.queue_tree.heading('Progress', text='Progress')
|
|
self.queue_tree.pack(fill='both', expand=True)
|
|
|
|
# Log output
|
|
log_frame = ttk.LabelFrame(render_frame, text="FFmpeg Output", padding=10)
|
|
log_frame.pack(fill='both', expand=True, padx=10, pady=5)
|
|
|
|
# Create frame for log text with consistent scrollbar
|
|
log_text_frame = ttk.Frame(log_frame)
|
|
log_text_frame.pack(fill='both', expand=True)
|
|
log_text_frame.rowconfigure(0, weight=1)
|
|
log_text_frame.columnconfigure(0, weight=1)
|
|
|
|
self.log_text = tk.Text(log_text_frame,
|
|
bg=self.colors['text_bg'], fg=self.colors['text_fg'],
|
|
insertbackground=self.colors['text_fg'], wrap='word')
|
|
self.log_text.grid(row=0, column=0, sticky='nsew')
|
|
|
|
# Add matching scrollbar
|
|
log_scrollbar = ttk.Scrollbar(log_text_frame, orient='vertical', command=self.log_text.yview)
|
|
log_scrollbar.grid(row=0, column=1, sticky='ns')
|
|
self.log_text.config(yscrollcommand=log_scrollbar.set)
|
|
|
|
def setup_qc_tab(self):
|
|
qc_frame = ttk.Frame(self.notebook)
|
|
self.notebook.add(qc_frame, text="QC")
|
|
|
|
# File selection for QC
|
|
qc_file_frame = ttk.LabelFrame(qc_frame, text="QC File Selection", padding=10)
|
|
qc_file_frame.pack(fill='x', padx=10, pady=5)
|
|
|
|
ttk.Label(qc_file_frame, text="Source File:").grid(row=0, column=0, sticky='w', pady=2)
|
|
self.qc_source_var = tk.StringVar()
|
|
ttk.Entry(qc_file_frame, textvariable=self.qc_source_var, width=50).grid(row=0, column=1, padx=5, pady=2)
|
|
ttk.Button(qc_file_frame, text="Browse", command=self.browse_qc_source).grid(row=0, column=2, padx=5, pady=2)
|
|
|
|
ttk.Label(qc_file_frame, text="Output File:").grid(row=1, column=0, sticky='w', pady=2)
|
|
self.qc_output_var = tk.StringVar()
|
|
ttk.Entry(qc_file_frame, textvariable=self.qc_output_var, width=50).grid(row=1, column=1, padx=5, pady=2)
|
|
ttk.Button(qc_file_frame, text="Browse", command=self.browse_qc_output).grid(row=1, column=2, padx=5, pady=2)
|
|
|
|
# QC controls
|
|
qc_control_frame = ttk.Frame(qc_frame)
|
|
qc_control_frame.pack(fill='x', padx=10, pady=5)
|
|
|
|
ttk.Button(qc_control_frame, text="Run QC Check", command=self.run_qc_check).pack(side='left', padx=5)
|
|
ttk.Button(qc_control_frame, text="Compare Frames", command=self.compare_frames).pack(side='left', padx=5)
|
|
ttk.Button(qc_control_frame, text="Export Report", command=self.export_qc_report).pack(side='left', padx=5)
|
|
|
|
# Frame comparison viewer
|
|
comparison_frame = ttk.LabelFrame(qc_frame, text="Frame Comparison", padding=10)
|
|
comparison_frame.pack(fill='both', expand=True, padx=10, pady=5)
|
|
|
|
# Frame navigation
|
|
nav_frame = ttk.Frame(comparison_frame)
|
|
nav_frame.pack(fill='x', pady=5)
|
|
|
|
ttk.Button(nav_frame, text="Previous Frame", command=self.prev_frame).pack(side='left', padx=5)
|
|
self.frame_label = ttk.Label(nav_frame, text="Frame: 0")
|
|
self.frame_label.pack(side='left', padx=20)
|
|
ttk.Button(nav_frame, text="Next Frame", command=self.next_frame).pack(side='left', padx=5)
|
|
|
|
# Frame display
|
|
display_frame = ttk.Frame(comparison_frame)
|
|
display_frame.pack(fill='both', expand=True)
|
|
|
|
self.source_canvas = tk.Canvas(display_frame, bg=self.colors['text_bg'])
|
|
self.source_canvas.pack(side='left', fill='both', expand=True, padx=5)
|
|
|
|
self.output_canvas = tk.Canvas(display_frame, bg=self.colors['text_bg'])
|
|
self.output_canvas.pack(side='right', fill='both', expand=True, padx=5)
|
|
|
|
# QC results
|
|
results_frame = ttk.LabelFrame(qc_frame, text="QC Results", padding=10)
|
|
results_frame.pack(fill='x', padx=10, pady=5)
|
|
|
|
# Create frame for QC results with consistent scrollbar
|
|
qc_text_frame = ttk.Frame(results_frame)
|
|
qc_text_frame.pack(fill='both', expand=True)
|
|
qc_text_frame.rowconfigure(0, weight=1)
|
|
qc_text_frame.columnconfigure(0, weight=1)
|
|
|
|
self.qc_results_text = tk.Text(qc_text_frame,
|
|
bg=self.colors['text_bg'], fg=self.colors['text_fg'],
|
|
insertbackground=self.colors['text_fg'], wrap='word')
|
|
self.qc_results_text.grid(row=0, column=0, sticky='nsew')
|
|
|
|
# Add matching scrollbar
|
|
qc_scrollbar = ttk.Scrollbar(qc_text_frame, orient='vertical', command=self.qc_results_text.yview)
|
|
qc_scrollbar.grid(row=0, column=1, sticky='ns')
|
|
self.qc_results_text.config(yscrollcommand=qc_scrollbar.set)
|
|
|
|
def browse_input(self):
|
|
# Use current path if it exists, otherwise use a reasonable default
|
|
initial_dir = self.input_path_var.get() if os.path.exists(self.input_path_var.get()) else os.path.expanduser("~")
|
|
|
|
path = filedialog.askdirectory(initialdir=initial_dir, title="Select Input Directory")
|
|
if path:
|
|
self.input_path_var.set(path)
|
|
self.config['input_path'] = path # Update config
|
|
self.refresh_files()
|
|
|
|
def refresh_files(self):
|
|
self.file_listbox.delete(0, tk.END)
|
|
input_path = self.input_path_var.get()
|
|
|
|
if not input_path:
|
|
self.file_listbox.insert(tk.END, "No input directory selected")
|
|
return
|
|
|
|
if not os.path.exists(input_path):
|
|
self.file_listbox.insert(tk.END, f"Directory not found: {input_path}")
|
|
return
|
|
|
|
try:
|
|
files = os.listdir(input_path)
|
|
video_files = [f for f in files if f.lower().endswith(('.mp4', '.avi', '.mkv', '.mov', '.wmv', '.m4v', '.flv', '.webm'))]
|
|
|
|
if not video_files:
|
|
self.file_listbox.insert(tk.END, "No video files found in directory")
|
|
else:
|
|
for file in sorted(video_files):
|
|
self.file_listbox.insert(tk.END, file)
|
|
|
|
except PermissionError:
|
|
self.file_listbox.insert(tk.END, "Permission denied accessing directory")
|
|
except Exception as e:
|
|
self.file_listbox.insert(tk.END, f"Error reading directory: {str(e)}")
|
|
|
|
def on_file_select(self, event):
|
|
selection = self.file_listbox.curselection()
|
|
if selection:
|
|
filename = self.file_listbox.get(selection[0])
|
|
# Check if it's an actual file or an error/status message
|
|
if filename and not filename.startswith(("No ", "Directory not found", "Permission denied", "Error reading")):
|
|
self.update_file_info(filename)
|
|
else:
|
|
# Clear file info if it's not a real file
|
|
self.file_info_text.delete(1.0, tk.END)
|
|
self.file_info_text.insert(1.0, "Select a valid video file to see information")
|
|
|
|
def update_file_info(self, filename):
|
|
filepath = os.path.join(self.input_path_var.get(), filename)
|
|
if os.path.exists(filepath):
|
|
try:
|
|
probe = ffmpeg.probe(filepath)
|
|
video_info = next(s for s in probe['streams'] if s['codec_type'] == 'video')
|
|
|
|
info_text = f"File: {filename}\n"
|
|
info_text += f"Duration: {float(probe['format']['duration']):.2f} seconds\n"
|
|
info_text += f"Resolution: {video_info['width']}x{video_info['height']}\n"
|
|
info_text += f"Codec: {video_info['codec_name']}\n"
|
|
info_text += f"Bitrate: {int(probe['format']['bit_rate']) / 1000000:.2f} Mbps\n"
|
|
|
|
self.file_info_text.delete(1.0, tk.END)
|
|
self.file_info_text.insert(1.0, info_text)
|
|
|
|
except Exception as e:
|
|
self.file_info_text.delete(1.0, tk.END)
|
|
self.file_info_text.insert(1.0, f"Error reading file: {str(e)}")
|
|
|
|
def add_to_queue(self):
|
|
selection = self.file_listbox.curselection()
|
|
if selection:
|
|
filename = self.file_listbox.get(selection[0])
|
|
self.render_queue.append({
|
|
'filename': filename,
|
|
'status': 'Queued',
|
|
'progress': 0
|
|
})
|
|
self.update_queue_display()
|
|
|
|
def preview_encode(self):
|
|
selection = self.file_listbox.curselection()
|
|
if selection:
|
|
filename = self.file_listbox.get(selection[0])
|
|
self.encode_file(filename, preview=True)
|
|
|
|
def start_rendering(self):
|
|
if self.render_queue and not self.is_rendering:
|
|
self.is_rendering = True
|
|
self.start_button.config(state='disabled')
|
|
self.pause_button.config(state='normal')
|
|
self.stop_button.config(state='normal')
|
|
|
|
# Start rendering thread
|
|
render_thread = threading.Thread(target=self.render_queue_worker)
|
|
render_thread.daemon = True
|
|
render_thread.start()
|
|
|
|
def render_queue_worker(self):
|
|
while self.render_queue and self.is_rendering:
|
|
job = self.render_queue[0]
|
|
self.encode_file(job['filename'])
|
|
if self.render_queue:
|
|
self.render_queue.pop(0)
|
|
self.update_queue_display()
|
|
|
|
self.is_rendering = False
|
|
self.start_button.config(state='normal')
|
|
self.pause_button.config(state='disabled')
|
|
self.stop_button.config(state='disabled')
|
|
|
|
def encode_file(self, filename, preview=False):
|
|
input_path = os.path.join(self.input_path_var.get(), filename)
|
|
output_filename = f"encoded_{filename}"
|
|
output_path = os.path.join(self.config['output_path'], output_filename)
|
|
|
|
# Build FFmpeg command
|
|
cmd = [
|
|
'ffmpeg', '-i', input_path,
|
|
'-c:v', self.codec_var.get(),
|
|
'-preset', self.preset_var.get(),
|
|
'-cq', str(self.cq_var.get())
|
|
]
|
|
|
|
if preview:
|
|
cmd.extend(['-t', str(self.config['preview_duration'])])
|
|
|
|
cmd.append(output_path)
|
|
|
|
try:
|
|
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
universal_newlines=True, bufsize=1)
|
|
|
|
self.current_process = process
|
|
|
|
for line in process.stdout:
|
|
self.log_text.insert(tk.END, line)
|
|
self.log_text.see(tk.END)
|
|
self.root.update_idletasks()
|
|
|
|
process.wait()
|
|
|
|
except Exception as e:
|
|
messagebox.showerror("Error", f"Encoding failed: {str(e)}")
|
|
|
|
def pause_rendering(self):
|
|
if self.current_process:
|
|
self.current_process.terminate()
|
|
|
|
def stop_rendering(self):
|
|
self.is_rendering = False
|
|
if self.current_process:
|
|
self.current_process.terminate()
|
|
|
|
def update_queue_display(self):
|
|
for item in self.queue_tree.get_children():
|
|
self.queue_tree.delete(item)
|
|
|
|
for i, job in enumerate(self.render_queue):
|
|
self.queue_tree.insert('', 'end', text=job['filename'],
|
|
values=(job['status'], f"{job['progress']}%"))
|
|
|
|
def browse_qc_source(self):
|
|
filename = filedialog.askopenfilename(
|
|
title="Select Source File",
|
|
filetypes=[("Video files", "*.mp4 *.avi *.mkv *.mov *.wmv")]
|
|
)
|
|
if filename:
|
|
self.qc_source_var.set(filename)
|
|
|
|
def browse_qc_output(self):
|
|
filename = filedialog.askopenfilename(
|
|
title="Select Output File",
|
|
filetypes=[("Video files", "*.mp4 *.avi *.mkv *.mov *.wmv")]
|
|
)
|
|
if filename:
|
|
self.qc_output_var.set(filename)
|
|
|
|
def run_qc_check(self):
|
|
source = self.qc_source_var.get()
|
|
output = self.qc_output_var.get()
|
|
|
|
if not source or not output:
|
|
messagebox.showerror("Error", "Please select both source and output files")
|
|
return
|
|
|
|
# Run QC check (placeholder for check_files.py functionality)
|
|
self.qc_results_text.delete(1.0, tk.END)
|
|
self.qc_results_text.insert(1.0, "QC check completed...\n")
|
|
|
|
def compare_frames(self):
|
|
source = self.qc_source_var.get()
|
|
output = self.qc_output_var.get()
|
|
|
|
if not source or not output:
|
|
messagebox.showerror("Error", "Please select both source and output files")
|
|
return
|
|
|
|
# Load frames for comparison
|
|
self.load_comparison_frames()
|
|
|
|
def load_comparison_frames(self):
|
|
# Placeholder for frame loading and display
|
|
pass
|
|
|
|
def prev_frame(self):
|
|
# Placeholder for frame navigation
|
|
pass
|
|
|
|
def next_frame(self):
|
|
# Placeholder for frame navigation
|
|
pass
|
|
|
|
def export_qc_report(self):
|
|
# Placeholder for QC report export
|
|
pass
|
|
|
|
def save_config(self):
|
|
self.config.update({
|
|
'input_path': self.input_path_var.get(),
|
|
'codec': self.codec_var.get(),
|
|
'preset': self.preset_var.get(),
|
|
'cq': self.cq_var.get(),
|
|
'auto_bitrate': self.auto_bitrate_var.get()
|
|
})
|
|
|
|
with open('vodkeeper_config.json', 'w') as f:
|
|
json.dump(self.config, f, indent=2)
|
|
|
|
messagebox.showinfo("Success", "Configuration saved")
|
|
|
|
def load_config(self):
|
|
try:
|
|
with open('vodkeeper_config.json', 'r') as f:
|
|
config = json.load(f)
|
|
self.config.update(config)
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
def main():
|
|
root = tk.Tk()
|
|
sv_ttk.use_dark_theme()
|
|
app = VodKeeper(root)
|
|
root.mainloop()
|
|
|
|
if __name__ == "__main__":
|
|
main() |