import customtkinter import tkinter as tk from tkinter import filedialog, messagebox import os import sys import threading import time import converter import webbrowser 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 # You can change the "widthxheight" and add "+x+y" for position. master.geometry("600x750+100+100") # --- 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")) # --- Footer Frame --- self.footer_frame = customtkinter.CTkFrame(master, fg_color="transparent") self.footer_frame.pack(side="bottom", fill="x", padx=10, pady=(5, 10)) self.nickname_label = customtkinter.CTkLabel(self.footer_frame, text="by ramforth") self.nickname_label.pack(side="left", padx=(10, 0)) self.gitea_link = customtkinter.CTkLabel(self.footer_frame, text="https://gitea.ramforth.net/ramforth", text_color="#4077d1", cursor="hand2") self.gitea_link.pack(side="right", padx=(0, 10)) self.gitea_link.bind("", lambda e: self.open_link("https://gitea.ramforth.net/ramforth")) # Add underline font underline_font = customtkinter.CTkFont(family="sans-serif", size=12, underline=True) self.gitea_link.configure(font=underline_font) # --- 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:", fg_color="transparent") self.label.pack(pady=(10, 5)) # (top_padding, bottom_padding) # Use a fg_color to make the label background visible self.filepath_label = customtkinter.CTkLabel(master, text="No file selected", fg_color=("gray75", "gray25")) 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:", fg_color="transparent") 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:", fg_color="transparent") 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:", fg_color="transparent") 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", fg_color=("gray75", "gray25")) 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 open_link(self, url): """Opens the given URL in a web browser.""" webbrowser.open_new(url) 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()