import tkinter as tk from tkinter import ttk, filedialog, messagebox, scrolledtext 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:\\mux\\input', 'output_path': 'R:\\mux\\output', 'logs_path': 'R:\\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() 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'])]) 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") # File selection file_frame = ttk.LabelFrame(input_frame, text="File Selection", padding=10) file_frame.pack(fill='x', padx=10, pady=5) 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 ttk.Label(file_frame, text="Available Files:").grid(row=1, column=0, sticky='w', pady=(10,2)) self.file_listbox = tk.Listbox(file_frame, height=8, width=70, 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=2, column=0, columnspan=3, sticky='ew', pady=2) self.file_listbox.bind('<>', self.on_file_select) # Refresh button ttk.Button(file_frame, text="Refresh Files", command=self.refresh_files).grid(row=3, column=0, pady=5) # 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) # File info info_frame = ttk.LabelFrame(input_frame, text="File Information", padding=10) info_frame.pack(fill='x', padx=10, pady=5) self.file_info_text = scrolledtext.ScrolledText(info_frame, height=6, width=80, bg=self.colors['text_bg'], fg=self.colors['text_fg'], insertbackground=self.colors['text_fg']) self.file_info_text.pack(fill='both', expand=True) # 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) self.log_text = scrolledtext.ScrolledText(log_frame, height=10, bg=self.colors['text_bg'], fg=self.colors['text_fg'], insertbackground=self.colors['text_fg']) self.log_text.pack(fill='both', expand=True) 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) self.qc_results_text = scrolledtext.ScrolledText(results_frame, height=6, bg=self.colors['text_bg'], fg=self.colors['text_fg'], insertbackground=self.colors['text_fg']) self.qc_results_text.pack(fill='both', expand=True) def browse_input(self): path = filedialog.askdirectory(initialdir=self.config['input_path']) if path: self.input_path_var.set(path) self.refresh_files() def refresh_files(self): self.file_listbox.delete(0, tk.END) input_path = self.input_path_var.get() if os.path.exists(input_path): for file in os.listdir(input_path): if file.lower().endswith(('.mp4', '.avi', '.mkv', '.mov', '.wmv')): self.file_listbox.insert(tk.END, file) def on_file_select(self, event): selection = self.file_listbox.curselection() if selection: filename = self.file_listbox.get(selection[0]) self.update_file_info(filename) 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()