#!/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 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 self.create_widgets() self.check_dependencies() # Start checking queue for output updates self.root.after(100, self.check_queue) 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) # 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()