Files
VodKeeper/vodkeeper.py
T

531 lines
23 KiB
Python

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('<<ListboxSelect>>', 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()