#!/usr/bin/env python3 """ YouTube Channel Archiver GUI A simple graphical interface for the YouTube channel archiver script. """ import tkinter as tk from tkinter import ttk, filedialog, messagebox, scrolledtext import subprocess import threading import sys import os import platform from pathlib import Path import queue class YouTubeArchiverGUI: def __init__(self, root): self.root = root self.root.title("YouTube Channel Archiver") self.root.geometry("600x500") self.root.resizable(True, True) # Queue for thread communication self.output_queue = queue.Queue() # Variables self.channel_url = tk.StringVar() self.output_dir = tk.StringVar(value=str(Path.cwd())) self.quality = tk.StringVar(value="best") self.audio_only = tk.BooleanVar() self.download_thumbnails = tk.BooleanVar(value=True) self.download_metadata = tk.BooleanVar(value=True) # Track download process self.download_process = None self.is_downloading = False # Check Python installation on Windows first if not self.check_python_installation(): return # Exit if Python installation failed self.create_widgets() self.check_dependencies() # Start checking queue for output updates self.root.after(100, self.check_queue) def check_python_installation(self): """Check if Python is properly installed on Windows.""" if platform.system() != "Windows": return True # Not Windows, proceed normally try: # Try to run python and get version result = subprocess.run([sys.executable, "--version"], capture_output=True, text=True, check=True) python_version = result.stdout.strip() print(f"Python is available: {python_version}") return True except (subprocess.CalledProcessError, FileNotFoundError): # Python not found, try to install it return self.install_python_windows() def install_python_windows(self): """Install Python on Windows using the provided MSIX installer.""" installer_path = Path("windows/python.msix") if not installer_path.exists(): messagebox.showerror( "Python Installation Error", f"Python is not installed and the installer was not found at:\n{installer_path.absolute()}\n\n" "Please install Python manually from python.org or place the installer at the expected location." ) return False # Show installation dialog result = messagebox.askyesno( "Python Installation Required", "Python is not installed on this system. Would you like to install it now?\n\n" f"The installer will be launched from:\n{installer_path.absolute()}" ) if not result: messagebox.showinfo( "Installation Cancelled", "Python installation cancelled. The application cannot run without Python." ) return False try: # Create a temporary window to show installation progress install_window = tk.Toplevel(self.root) install_window.title("Installing Python") install_window.geometry("400x200") install_window.resizable(False, False) install_window.transient(self.root) install_window.grab_set() # Center the window install_window.update_idletasks() x = (install_window.winfo_screenwidth() // 2) - (install_window.winfo_width() // 2) y = (install_window.winfo_screenheight() // 2) - (install_window.winfo_height() // 2) install_window.geometry(f"+{x}+{y}") # Add progress indicator and message frame = ttk.Frame(install_window, padding="20") frame.pack(fill="both", expand=True) ttk.Label(frame, text="Installing Python...", font=('Arial', 12, 'bold')).pack(pady=(0, 10)) progress = ttk.Progressbar(frame, mode='indeterminate') progress.pack(fill="x", pady=(0, 10)) progress.start() status_label = ttk.Label(frame, text="Launching installer...", wraplength=350) status_label.pack(pady=(0, 10)) # Update the window install_window.update() # Launch the MSIX installer status_label.config(text="Running Python installer. Please follow the installation prompts.") install_window.update() # Use PowerShell to install the MSIX package powershell_cmd = f'Add-AppxPackage -Path "{installer_path.absolute()}"' result = subprocess.run([ "powershell", "-Command", powershell_cmd ], capture_output=True, text=True) progress.stop() if result.returncode == 0: status_label.config(text="Installation completed successfully!") install_window.update() # Wait a moment and then verify installation self.root.after(2000, lambda: self.verify_python_installation(install_window)) return True else: # Installation failed error_msg = result.stderr or result.stdout or "Unknown error occurred" status_label.config(text=f"Installation failed: {error_msg[:100]}...") install_window.update() messagebox.showerror( "Installation Failed", f"Python installation failed:\n{error_msg}\n\n" "Please try installing Python manually from python.org" ) install_window.destroy() return False except Exception as e: messagebox.showerror( "Installation Error", f"An error occurred while installing Python:\n{str(e)}\n\n" "Please try installing Python manually from python.org" ) if 'install_window' in locals(): install_window.destroy() return False def verify_python_installation(self, install_window): """Verify that Python was installed successfully.""" try: # Try to find Python in common locations possible_python_paths = [ "python", "python3", "py", os.path.expandvars(r"%LOCALAPPDATA%\Microsoft\WindowsApps\python.exe"), os.path.expandvars(r"%LOCALAPPDATA%\Programs\Python\Python*\python.exe"), ] python_found = False for python_path in possible_python_paths: try: result = subprocess.run([python_path, "--version"], capture_output=True, text=True, check=True) python_version = result.stdout.strip() print(f"Python found at {python_path}: {python_version}") python_found = True break except (subprocess.CalledProcessError, FileNotFoundError): continue install_window.destroy() if python_found: messagebox.showinfo( "Installation Successful", f"Python has been installed successfully!\n{python_version}\n\n" "The application will now continue." ) return True else: messagebox.showwarning( "Installation Verification Failed", "Python installation completed, but Python could not be found in the expected locations.\n\n" "You may need to restart the application or your system for the installation to take effect." ) return False except Exception as e: install_window.destroy() messagebox.showerror( "Verification Error", f"Could not verify Python installation:\n{str(e)}" ) return False def create_widgets(self): # Main frame main_frame = ttk.Frame(self.root, padding="10") main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) # Configure grid weights self.root.columnconfigure(0, weight=1) self.root.rowconfigure(0, weight=1) main_frame.columnconfigure(1, weight=1) # Title title_label = ttk.Label(main_frame, text="YouTube Channel Archiver", font=('Arial', 16, 'bold')) title_label.grid(row=0, column=0, columnspan=3, pady=(0, 20)) # Channel URL input ttk.Label(main_frame, text="Channel URL:").grid(row=1, column=0, sticky=tk.W, pady=5) url_entry = ttk.Entry(main_frame, textvariable=self.channel_url, width=50) url_entry.grid(row=1, column=1, columnspan=2, sticky=(tk.W, tk.E), pady=5, padx=(10, 0)) # Output directory ttk.Label(main_frame, text="Output Directory:").grid(row=2, column=0, sticky=tk.W, pady=5) dir_entry = ttk.Entry(main_frame, textvariable=self.output_dir, width=40) dir_entry.grid(row=2, column=1, sticky=(tk.W, tk.E), pady=5, padx=(10, 5)) browse_btn = ttk.Button(main_frame, text="Browse", command=self.browse_directory) browse_btn.grid(row=2, column=2, pady=5) # Options frame options_frame = ttk.LabelFrame(main_frame, text="Options", padding="10") options_frame.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10) options_frame.columnconfigure(1, weight=1) # Quality selection ttk.Label(options_frame, text="Quality:").grid(row=0, column=0, sticky=tk.W, pady=2) quality_combo = ttk.Combobox(options_frame, textvariable=self.quality, values=["best", "worst", "1080p", "720p", "480p"], state="readonly", width=15) quality_combo.grid(row=0, column=1, sticky=tk.W, pady=2, padx=(10, 0)) # Checkboxes audio_check = ttk.Checkbutton(options_frame, text="Audio only", variable=self.audio_only) audio_check.grid(row=1, column=0, sticky=tk.W, pady=2) thumb_check = ttk.Checkbutton(options_frame, text="Download thumbnails", variable=self.download_thumbnails) thumb_check.grid(row=1, column=1, sticky=tk.W, pady=2) meta_check = ttk.Checkbutton(options_frame, text="Download metadata", variable=self.download_metadata) meta_check.grid(row=2, column=0, sticky=tk.W, pady=2) # Download button self.download_btn = ttk.Button(main_frame, text="Download Channel", command=self.start_download, style="Accent.TButton") self.download_btn.grid(row=4, column=0, columnspan=3, pady=20) # Progress bar self.progress = ttk.Progressbar(main_frame, mode='indeterminate') self.progress.grid(row=5, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10)) # Output text area output_frame = ttk.LabelFrame(main_frame, text="Output", padding="5") output_frame.grid(row=6, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10)) output_frame.columnconfigure(0, weight=1) output_frame.rowconfigure(0, weight=1) main_frame.rowconfigure(6, weight=1) self.output_text = scrolledtext.ScrolledText(output_frame, height=12, width=70) self.output_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) # Status bar self.status_var = tk.StringVar(value="Ready") status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W) status_bar.grid(row=7, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(5, 0)) def check_dependencies(self): """Check if yt-dlp is available.""" try: result = subprocess.run(['yt-dlp', '--version'], capture_output=True, text=True, check=True) version = result.stdout.strip() self.log_output(f"✓ yt-dlp is available: {version}") self.status_var.set("Ready - yt-dlp found") except (subprocess.CalledProcessError, FileNotFoundError): self.log_output("⚠ yt-dlp not found. It will be installed automatically when needed.") self.status_var.set("Ready - yt-dlp will be auto-installed") def browse_directory(self): """Open directory browser.""" directory = filedialog.askdirectory(initialdir=self.output_dir.get()) if directory: self.output_dir.set(directory) def log_output(self, message): """Add message to output text area.""" self.output_text.insert(tk.END, message + "\n") self.output_text.see(tk.END) self.root.update_idletasks() def clear_output(self): """Clear the output text area.""" self.output_text.delete(1.0, tk.END) def validate_inputs(self): """Validate user inputs.""" if not self.channel_url.get().strip(): messagebox.showerror("Error", "Please enter a channel URL") return False if not self.output_dir.get().strip(): messagebox.showerror("Error", "Please select an output directory") return False # Check if output directory exists or can be created try: output_path = Path(self.output_dir.get()) output_path.mkdir(parents=True, exist_ok=True) except Exception as e: messagebox.showerror("Error", f"Cannot create output directory: {e}") return False return True def build_command(self): """Build the yt-dlp command based on GUI settings.""" # Find the archiver script script_path = Path(__file__).parent / "youtube_archiver.py" if not script_path.exists(): # If not found, assume it's in the same directory script_path = "youtube_archiver.py" cmd = [sys.executable, str(script_path)] # Add channel URL cmd.append(self.channel_url.get().strip()) # Add options cmd.extend(["--output", self.output_dir.get()]) cmd.extend(["--quality", self.quality.get()]) if self.audio_only.get(): cmd.append("--audio-only") if not self.download_thumbnails.get(): cmd.append("--no-thumbnails") if not self.download_metadata.get(): cmd.append("--no-metadata") return cmd def start_download(self): """Start the download process in a separate thread.""" if self.is_downloading: self.stop_download() return if not self.validate_inputs(): return # Clear previous output self.clear_output() # Update UI self.is_downloading = True self.download_btn.config(text="Stop Download") self.progress.start() self.status_var.set("Downloading...") # Build command cmd = self.build_command() self.log_output(f"Starting download with command: {' '.join(cmd)}") self.log_output("-" * 60) # Start download in separate thread self.download_thread = threading.Thread(target=self.run_download, args=(cmd,)) self.download_thread.daemon = True self.download_thread.start() def run_download(self, cmd): """Run the download process.""" try: # Start the process self.download_process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, universal_newlines=True ) # Read output line by line for line in iter(self.download_process.stdout.readline, ''): if not self.is_downloading: # Check if stopped break # Put output in queue for main thread to handle self.output_queue.put(('output', line.rstrip())) # Wait for process to complete return_code = self.download_process.wait() if return_code == 0: self.output_queue.put(('status', 'Download completed successfully!')) else: self.output_queue.put(('status', f'Download failed with code {return_code}')) except Exception as e: self.output_queue.put(('error', f'Error during download: {str(e)}')) finally: self.output_queue.put(('finished', None)) def stop_download(self): """Stop the current download.""" if self.download_process and self.download_process.poll() is None: self.download_process.terminate() self.log_output("\n⚠ Download stopped by user") self.download_finished() def download_finished(self): """Handle download completion.""" self.is_downloading = False self.download_btn.config(text="Download Channel") self.progress.stop() self.status_var.set("Ready") def check_queue(self): """Check the queue for messages from the download thread.""" try: while True: msg_type, message = self.output_queue.get_nowait() if msg_type == 'output': self.log_output(message) elif msg_type == 'status': self.log_output(f"\n{message}") self.status_var.set(message) elif msg_type == 'error': self.log_output(f"\n❌ {message}") messagebox.showerror("Download Error", message) elif msg_type == 'finished': self.download_finished() break except queue.Empty: pass # Schedule next check self.root.after(100, self.check_queue) def main(): root = tk.Tk() # Set up styling style = ttk.Style() # Try to use a modern theme try: style.theme_use('clam') # Modern looking theme except: pass # Use default theme if clam is not available app = YouTubeArchiverGUI(root) # Only proceed if Python installation check passed if not hasattr(app, 'output_queue'): root.destroy() return # Handle window closing def on_closing(): if app.is_downloading: if messagebox.askokcancel("Quit", "Download in progress. Do you want to stop and quit?"): app.stop_download() root.destroy() else: root.destroy() root.protocol("WM_DELETE_WINDOW", on_closing) # Center window on screen root.update_idletasks() x = (root.winfo_screenwidth() // 2) - (root.winfo_width() // 2) y = (root.winfo_screenheight() // 2) - (root.winfo_height() // 2) root.geometry(f"+{x}+{y}") root.mainloop() if __name__ == "__main__": main()