Files
video-converter/gui.py

245 lines
11 KiB
Python

import customtkinter
import tkinter as tk
from tkinter import filedialog, messagebox
import os
import sys
import threading
import time
import converter
import downloader
import utils
import config
class VideoConverterGUI:
def __init__(self, master):
self.master = master
master.title("Video Converter")
# Set a default size to prevent the narrow window
master.geometry("450x700")
# --- Set Dark Mode by Default ---
customtkinter.set_appearance_mode("Dark") # Modes: "System", "Dark", "Light"
customtkinter.set_default_color_theme(os.path.join(os.path.dirname(__file__), "custom_theme_dark.json"))
# --- Pack bottom elements first ---
# This makes them stick to the bottom
self.status_var = customtkinter.StringVar(value="Ready") # Give it a default value
self.status_label = customtkinter.CTkLabel(master, textvariable=self.status_var)
# Add padding (x, y)
self.status_label.pack(side="bottom", fill="x", padx=10, pady=(5, 0))
self.progress = customtkinter.CTkProgressBar(master, orientation="horizontal")
self.progress.pack(side="bottom", fill="x", padx=10, pady=(0, 10))
self.progress.set(0) # Default to 0
# --- Mode Switch ---
self.mode_switch = customtkinter.CTkSwitch(master, text="Dark Mode", command=self.toggle_mode)
# anchor="w" (west) aligns it to the left
self.mode_switch.pack(pady=10, padx=20, anchor="w")
self.mode_switch.select() # Start in the "on" (Dark) state
self.toggle_mode() # Ensure initial mode is set based on switch state
# --- Main Widgets ---
self.label = customtkinter.CTkLabel(master, text="Select video file or enter URL:")
self.label.pack(pady=(10, 5)) # (top_padding, bottom_padding)
self.filepath_label = customtkinter.CTkLabel(master, text="No file selected")
# fill="x" makes it expand to show the full path
self.filepath_label.pack(pady=5, padx=20, fill="x")
self.browse_button = customtkinter.CTkButton(master, text="Browse", command=self.browse_file)
self.browse_button.pack(pady=5)
self.url_label = customtkinter.CTkLabel(master, text="URL:")
self.url_label.pack(pady=(15, 5)) # Add extra padding on top to create a group
# Removed width=50, will use fill="x" in pack() instead
self.url_entry = customtkinter.CTkEntry(master, placeholder_text="https://...")
self.url_entry.pack(pady=5, padx=20, fill="x")
self.quality_label = customtkinter.CTkLabel(master, text="Quality:")
self.quality_label.pack(pady=(15, 5))
self.quality_var = customtkinter.StringVar(master, value="medium")
self.quality_menu = customtkinter.CTkOptionMenu(master, variable=self.quality_var, values=["low", "medium", "high", "archive"])
self.quality_menu.pack(pady=5)
self.cookies_label = customtkinter.CTkLabel(master, text="Cookies from Browser:")
self.cookies_label.pack(pady=(15, 5))
self.cookies_var = customtkinter.StringVar(master, value="none")
self.cookies_menu = customtkinter.CTkOptionMenu(master, variable=self.cookies_var, values=["none", "brave", "chrome", "chromium", "edge", "firefox", "opera", "safari", "vivaldi", "whale"])
self.cookies_menu.pack(pady=5)
self.output_dir_label = customtkinter.CTkLabel(master, text="Output Directory:")
self.output_dir_label.pack(pady=(15, 5))
self.output_dir_button = customtkinter.CTkButton(master, text="Select Directory", command=self.select_output_dir)
self.output_dir_button.pack(pady=5)
self.output_dir_path_label = customtkinter.CTkLabel(master, text="No directory selected")
self.output_dir_path_label.pack(pady=5, padx=20, fill="x")
self.convert_button = customtkinter.CTkButton(master, text="Convert", command=self.convert)
self.convert_button.pack(pady=(20, 5)) # Extra top padding
self.cancel_button = customtkinter.CTkButton(master, text="Cancel", command=self.cancel_conversion, state="disabled")
self.cancel_button.pack(pady=5)
def toggle_mode(self):
# Check if the switch is on (1) or off (0)
if self.mode_switch.get() == 1:
customtkinter.set_appearance_mode("Dark")
customtkinter.set_default_color_theme(os.path.join(os.path.dirname(__file__), "custom_theme_dark.json"))
else:
customtkinter.set_appearance_mode("Light")
customtkinter.set_default_color_theme(os.path.join(os.path.dirname(__file__), "custom_theme_light.json"))
def browse_file(self):
filepath = filedialog.askopenfilename()
if filepath:
# --- FIX: Use .configure() ---
self.filepath_label.configure(text=filepath)
def select_output_dir(self):
output_dir = filedialog.askdirectory()
if output_dir:
# --- FIX: Use .configure() ---
self.output_dir_path_label.configure(text=output_dir)
def convert(self):
input_path = self.filepath_label.cget("text")
url = self.url_entry.get()
output_dir = self.output_dir_path_label.cget("text")
quality = self.quality_var.get()
# Clear file path if URL is being used
if url:
input_path = ""
self.filepath_label.configure(text="Using URL")
elif input_path == "No file selected":
input_path = "" # Ensure it's empty
if not (input_path or url):
messagebox.showerror("Error", "Please select a file or enter a URL.")
return
if output_dir == "No directory selected":
messagebox.showerror("Error", "Please select an output directory.")
return
# --- FIX: Use .configure() and string states ---
self.convert_button.configure(state="disabled")
self.cancel_button.configure(state="normal")
self.status_var.set("Starting...")
self.conversion_thread = threading.Thread(
target=self._run_conversion,
args=(input_path, url, output_dir, quality, self.cookies_var.get()),
daemon=True
)
self.conversion_thread.start()
def _run_conversion(self, input_path, url, output_dir, quality, cookies_from_browser):
try:
if url:
self.status_var.set("Downloading video...")
# --- FIX: Use .configure() ---
self.progress.configure(mode='indeterminate')
self.progress.start()
input_path = downloader.download_video(url, output_dir, cookies_from_browser)
self.progress.stop()
# --- FIX: Use .configure() ---
self.progress.configure(mode='determinate')
if not input_path:
raise Exception("Download failed.")
self.status_var.set("Converting video...")
duration = utils.get_video_duration(input_path)
if not duration:
# If we can't get the duration, use indeterminate mode
self.progress.configure(mode='indeterminate')
self.progress.start()
else:
# --- FIX: Set mode to determinate and clear value ---
self.progress.configure(mode='determinate')
self.progress.set(0)
# --- FIX: Pass duration to the progress updater ---
self.progress_thread = threading.Thread(target=self._update_progress, args=(duration,), daemon=True)
self.progress_thread.start()
success = converter.convert_video(input_path, output_dir, quality)
if success:
self.status_var.set("Conversion successful!")
self.progress.set(1) # Fill the bar on success
messagebox.showinfo("Success", "Conversion completed successfully!")
else:
raise Exception("Conversion failed. Check logs for details.")
except Exception as e:
self.status_var.set(f"Error: {e}")
try:
with open(config.FFMPEG_LOG_FILE_PATH, "r") as f:
log_lines = f.readlines()
last_10_lines = "".join(log_lines[-10:])
messagebox.showerror("Error", f"An error occurred: {e}\n\nLast 10 lines of log:\n{last_10_lines}")
except FileNotFoundError:
messagebox.showerror("Error", f"An error occurred: {e}\n\nCould not find log file.")
except Exception as log_e:
messagebox.showerror("Error", f"An error occurred: {e}\n\nCould not read log file: {log_e}")
finally:
# --- FIX: Use .configure() and string states ---
self.convert_button.configure(state="normal")
self.cancel_button.configure(state="disabled")
self.progress.stop()
# --- FIX: Use .set() to reset progress bar ---
if "Error" not in self.status_var.get():
self.progress.set(1) # Keep it full on success
else:
self.progress.set(0) # Reset on error
if self.status_var.get() == "Starting...": # Handle cancellation before start
self.status_var.set("Ready")
# --- FIX: Accept duration as an argument ---
def _update_progress(self, duration):
progress_file = "ffmpeg_progress.log"
while self.conversion_thread.is_alive():
try:
with open(progress_file, "r") as f:
lines = f.readlines()
if lines:
last_line = lines[-1]
if "out_time_ms" in last_line:
time_ms = int(last_line.split("=")[1])
current_time_sec = time_ms / 1000000
# --- FIX: Calculate progress as 0.0-1.0 ---
if duration and duration > 0:
progress_value = max(0, min(1, current_time_sec / duration))
self.progress.set(progress_value)
except (FileNotFoundError, IndexError, ValueError):
pass
time.sleep(0.1)
def cancel_conversion(self):
# This is still a difficult operation.
# A more robust solution involves inter-thread communication
# or sending a 'q' to the ffmpeg process.
self.status_var.set("Cancellation requested...")
messagebox.showinfo("Cancel", "Cancellation requested. The current operation will try to stop.")
# A simple way to try and stop (if converter supports it):
# You would need to build a way to signal the converter thread
# to find and kill the ffmpeg subprocess.
# For now, we just update the UI.
self.convert_button.configure(state="normal")
self.cancel_button.configure(state="disabled")
if __name__ == '__main__':
root = customtkinter.CTk()
gui = VideoConverterGUI(root)
root.mainloop()