#!/usr/bin/env python3 """ YouTube Channel Archiver - Standalone Windows Executable Fixed version that avoids subprocess recursion issues with PyInstaller. """ 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 import tempfile class YouTubeArchiverStandalone: def __init__(self, root): self.root = root self.root.title("YouTube Channel Archiver v1.0") self.root.geometry("700x600") 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.home() / "Downloads" / "YouTube_Archives")) 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 processes self.download_process = None self.is_downloading = False self.dependencies_checked = False self.total_videos = 0 self.downloaded_videos = 0 self.current_video = "" self.create_widgets() # Check dependencies in background self.root.after(1000, self.check_all_dependencies) # Start checking queue for output updates self.root.after(100, self.check_queue) def get_python_executable(self): """Get the actual Python executable path, not the PyInstaller executable.""" if getattr(sys, 'frozen', False): # Running as PyInstaller executable # Try to find Python in common locations possible_paths = [ 'python', 'python3', 'py', os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Python', 'Python*', 'python.exe'), os.path.join(os.environ.get('PROGRAMFILES', ''), 'Python*', 'python.exe'), os.path.join(os.environ.get('PROGRAMFILES(X86)', ''), 'Python*', 'python.exe'), ] for path in possible_paths: try: result = subprocess.run([path, '--version'], capture_output=True, text=True, check=True, timeout=5) if 'Python' in result.stdout: return path except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired): continue # If we can't find Python, we'll have to ask the user return None else: # Running as normal Python script return sys.executable def create_widgets(self): # Main frame with padding main_frame = ttk.Frame(self.root, padding="15") 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 and info title_frame = ttk.Frame(main_frame) title_frame.grid(row=0, column=0, columnspan=3, pady=(0, 20)) title_label = ttk.Label(title_frame, text="YouTube Channel Archiver", font=('Arial', 18, 'bold')) title_label.pack() subtitle_label = ttk.Label(title_frame, text="Download entire YouTube channels with ease", font=('Arial', 10)) subtitle_label.pack() # Status frame status_frame = ttk.LabelFrame(main_frame, text="System Status", padding="10") status_frame.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 15)) status_frame.columnconfigure(1, weight=1) self.python_status = ttk.Label(status_frame, text="Python: Checking...", foreground="orange") self.python_status.grid(row=0, column=0, sticky=tk.W, pady=2) self.ytdlp_status = ttk.Label(status_frame, text="yt-dlp: Checking...", foreground="orange") self.ytdlp_status.grid(row=1, column=0, sticky=tk.W, pady=2) # Channel URL input ttk.Label(main_frame, text="YouTube Channel URL:", font=('Arial', 10, 'bold')).grid( row=2, column=0, sticky=tk.W, pady=(5, 2)) url_frame = ttk.Frame(main_frame) url_frame.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10)) url_frame.columnconfigure(0, weight=1) url_entry = ttk.Entry(url_frame, textvariable=self.channel_url, font=('Arial', 10)) url_entry.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 10)) paste_btn = ttk.Button(url_frame, text="Paste", command=self.paste_from_clipboard) paste_btn.grid(row=0, column=1) # Examples examples_label = ttk.Label(main_frame, text="Examples: https://www.youtube.com/@channelname or https://www.youtube.com/c/channelname", font=('Arial', 8), foreground="gray") examples_label.grid(row=4, column=0, columnspan=3, sticky=tk.W) # Output directory ttk.Label(main_frame, text="Download Location:", font=('Arial', 10, 'bold')).grid( row=5, column=0, sticky=tk.W, pady=(15, 2)) dir_frame = ttk.Frame(main_frame) dir_frame.grid(row=6, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10)) dir_frame.columnconfigure(0, weight=1) dir_entry = ttk.Entry(dir_frame, textvariable=self.output_dir, font=('Arial', 10)) dir_entry.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 10)) browse_btn = ttk.Button(dir_frame, text="Browse", command=self.browse_directory) browse_btn.grid(row=0, column=1) # Options frame options_frame = ttk.LabelFrame(main_frame, text="Download Options", padding="10") options_frame.grid(row=7, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 15)) options_frame.columnconfigure(1, weight=1) # Quality selection ttk.Label(options_frame, text="Video Quality:").grid(row=0, column=0, sticky=tk.W, pady=2) quality_combo = ttk.Combobox(options_frame, textvariable=self.quality, values=["best", "1080p", "720p", "480p", "worst"], state="readonly", width=15) quality_combo.grid(row=0, column=1, sticky=tk.W, pady=2, padx=(10, 0)) # Checkboxes ttk.Checkbutton(options_frame, text="Audio only", variable=self.audio_only).grid(row=1, column=0, sticky=tk.W, pady=2) ttk.Checkbutton(options_frame, text="Download thumbnails", variable=self.download_thumbnails).grid(row=1, column=1, sticky=tk.W, pady=2) ttk.Checkbutton(options_frame, text="Download metadata", variable=self.download_metadata).grid(row=2, column=0, sticky=tk.W, pady=2) # Manual install button (for when auto-install fails) self.manual_install_btn = ttk.Button(options_frame, text="Install yt-dlp Manually", command=self.show_manual_install_instructions) self.manual_install_btn.grid(row=2, column=1, sticky=tk.W, pady=2) self.manual_install_btn.grid_remove() # Hide initially # Download button self.download_btn = ttk.Button(main_frame, text="Download Channel", command=self.start_download, style="Accent.TButton") self.download_btn.grid(row=8, column=0, columnspan=3, pady=15) self.download_btn.state(['disabled']) # Disabled until dependencies are ready # Progress bar self.progress = ttk.Progressbar(main_frame, mode='determinate', maximum=100) self.progress.grid(row=9, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 5)) # Progress label self.progress_label = ttk.Label(main_frame, text="", font=('Arial', 9)) self.progress_label.grid(row=10, column=0, columnspan=3, pady=(0, 10)) # Output text area output_frame = ttk.LabelFrame(main_frame, text="Download Progress", padding="5") output_frame.grid(row=11, 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(11, weight=1) self.output_text = scrolledtext.ScrolledText(output_frame, height=10, width=80, font=('Consolas', 9)) 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="Checking system dependencies...") status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W) status_bar.grid(row=12, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(5, 0)) def paste_from_clipboard(self): """Paste URL from clipboard.""" try: clipboard_content = self.root.clipboard_get() if "youtube.com" in clipboard_content or "youtu.be" in clipboard_content: self.channel_url.set(clipboard_content.strip()) else: messagebox.showwarning("Invalid URL", "Clipboard doesn't contain a YouTube URL") except tk.TkError: messagebox.showwarning("Clipboard Error", "Could not access clipboard") 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 check_all_dependencies(self): """Check all system dependencies.""" self.log_output("=== System Dependency Check ===") # Check Python python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" self.log_output(f"Python {python_version} detected") if getattr(sys, 'frozen', False): self.log_output("Running as compiled executable") python_exe = self.get_python_executable() if python_exe: self.log_output(f"Found Python at: {python_exe}") self.python_status.config(text=f"Python: {python_version} [OK]", foreground="green") else: self.log_output("WARNING: Could not locate Python executable for package installation") self.python_status.config(text="Python: Limited functionality", foreground="orange") else: self.python_status.config(text=f"Python: {python_version} [OK]", foreground="green") # Check yt-dlp in background thread threading.Thread(target=self.check_ytdlp, daemon=True).start() def check_ytdlp(self): """Check if yt-dlp is available, install if needed.""" try: # Try to run yt-dlp result = subprocess.run(['yt-dlp', '--version'], capture_output=True, text=True, check=True, timeout=10) version = result.stdout.strip() # Update UI from main thread self.root.after(0, lambda: self.ytdlp_found(version)) except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired): # yt-dlp not found, try to install self.root.after(0, lambda: self.install_ytdlp()) def ytdlp_found(self, version): """Called when yt-dlp is found.""" self.log_output(f"yt-dlp {version} is available") self.ytdlp_status.config(text=f"yt-dlp: {version} [OK]", foreground="green") self.dependencies_ready() def install_ytdlp(self): """Install yt-dlp using pip.""" if getattr(sys, 'frozen', False): # Running as executable - need to find Python python_exe = self.get_python_executable() if not python_exe: self.log_output("Cannot install yt-dlp automatically - Python not found") self.ytdlp_status.config(text="yt-dlp: Manual installation required", foreground="red") self.manual_install_btn.grid() # Show manual install button self.show_manual_install_instructions() return else: python_exe = sys.executable self.log_output("yt-dlp not found. Installing...") self.ytdlp_status.config(text="yt-dlp: Installing...", foreground="orange") # Install in background thread threading.Thread(target=self.do_install_ytdlp, args=(python_exe,), daemon=True).start() def do_install_ytdlp(self, python_exe): """Actually install yt-dlp.""" try: # Install yt-dlp self.root.after(0, lambda: self.log_output(f"Installing yt-dlp using: {python_exe}")) result = subprocess.run([python_exe, '-m', 'pip', 'install', 'yt-dlp'], capture_output=True, text=True, check=True, timeout=120) self.root.after(0, lambda: self.log_output("Installation completed, verifying...")) # Try multiple methods to verify yt-dlp installation version = self.verify_ytdlp_installation(python_exe) if version: # Update UI from main thread self.root.after(0, lambda: self.ytdlp_installed(version)) else: # Installation succeeded but verification failed - that's still OK self.root.after(0, lambda: self.ytdlp_install_partial()) except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e: error_msg = str(e) if hasattr(e, 'stderr') and e.stderr: error_msg += f"\nError output: {e.stderr[:200]}" self.root.after(0, lambda: self.ytdlp_install_failed(error_msg)) def verify_ytdlp_installation(self, python_exe): """Try multiple methods to verify yt-dlp is installed and get its version.""" # Method 1: Try direct yt-dlp command try: result = subprocess.run(['yt-dlp', '--version'], capture_output=True, text=True, check=True, timeout=10) return result.stdout.strip() except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired): pass # Method 2: Try through Python module try: result = subprocess.run([python_exe, '-m', 'yt_dlp', '--version'], capture_output=True, text=True, check=True, timeout=10) return result.stdout.strip() except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired): pass # Method 3: Try to import and get version programmatically try: result = subprocess.run([python_exe, '-c', 'import yt_dlp; print(yt_dlp.version.__version__)'], capture_output=True, text=True, check=True, timeout=10) version = result.stdout.strip() if version: return version except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired): pass # Method 4: Check if module can be imported (basic verification) try: result = subprocess.run([python_exe, '-c', 'import yt_dlp; print("installed")'], capture_output=True, text=True, check=True, timeout=10) if 'installed' in result.stdout: return "installed (version unknown)" except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired): pass return None def ytdlp_install_partial(self): """Called when yt-dlp was installed but can't be verified normally.""" self.log_output("yt-dlp installed successfully!") self.log_output("Note: yt-dlp command may need to be accessed via 'python -m yt_dlp'") self.ytdlp_status.config(text="yt-dlp: Installed [OK]", foreground="green") self.dependencies_ready() def ytdlp_installed(self, version): """Called when yt-dlp is successfully installed.""" self.log_output(f"yt-dlp {version} installed successfully!") self.ytdlp_status.config(text=f"yt-dlp: {version} [OK]", foreground="green") self.dependencies_ready() def ytdlp_install_failed(self, error): """Called when yt-dlp installation fails.""" self.log_output(f"Failed to install yt-dlp: {error}") self.ytdlp_status.config(text="yt-dlp: Installation failed [ERROR]", foreground="red") self.status_var.set("Error: Could not install yt-dlp automatically") self.manual_install_btn.grid() # Show manual install button messagebox.showerror("Installation Error", "Could not install yt-dlp automatically.\n\n" "This can happen when running as an executable.\n" "Please click 'Install yt-dlp Manually' for instructions.") def show_manual_install_instructions(self): """Show manual installation instructions.""" instructions = """Manual yt-dlp Installation Instructions: 1. Open Command Prompt or PowerShell as Administrator 2. Run one of these commands: pip install yt-dlp OR python -m pip install yt-dlp OR py -m pip install yt-dlp 3. After installation, restart this application Alternative method: - Download yt-dlp.exe from: https://github.com/yt-dlp/yt-dlp/releases - Place it in the same folder as this application - Or place it in a folder that's in your system PATH The application will automatically detect yt-dlp once it's installed.""" # Create instruction window instruction_window = tk.Toplevel(self.root) instruction_window.title("Manual Installation Instructions") instruction_window.geometry("600x400") instruction_window.transient(self.root) instruction_window.grab_set() frame = ttk.Frame(instruction_window, padding="15") frame.pack(fill="both", expand=True) text_widget = scrolledtext.ScrolledText(frame, height=20, width=70, font=('Consolas', 10)) text_widget.pack(fill="both", expand=True) text_widget.insert(tk.END, instructions) text_widget.config(state=tk.DISABLED) button_frame = ttk.Frame(frame) button_frame.pack(fill="x", pady=(10, 0)) ttk.Button(button_frame, text="Recheck Dependencies", command=lambda: [instruction_window.destroy(), self.recheck_dependencies()]).pack(side="right", padx=(5, 0)) ttk.Button(button_frame, text="Close", command=instruction_window.destroy).pack(side="right") def recheck_dependencies(self): """Recheck dependencies after manual installation.""" self.dependencies_checked = False self.manual_install_btn.grid_remove() self.download_btn.state(['disabled']) self.status_var.set("Rechecking dependencies...") self.log_output("\n=== Rechecking Dependencies ===") threading.Thread(target=self.check_ytdlp, daemon=True).start() def dependencies_ready(self): """Called when all dependencies are ready.""" if not self.dependencies_checked: self.dependencies_checked = True self.download_btn.state(['!disabled']) self.manual_install_btn.grid_remove() self.status_var.set("Ready to download!") self.log_output("=== All dependencies ready! ===") def browse_directory(self): """Open directory browser.""" directory = filedialog.askdirectory(initialdir=self.output_dir.get()) if directory: self.output_dir.set(directory) def validate_inputs(self): """Validate user inputs.""" if not self.channel_url.get().strip(): messagebox.showerror("Error", "Please enter a YouTube channel URL") return False url = self.channel_url.get().strip() if "youtube.com" not in url and "youtu.be" not in url: messagebox.showerror("Error", "Please enter a valid YouTube 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.""" # Try to determine the best way to call yt-dlp cmd = self.get_ytdlp_command() # Output template output_path = Path(self.output_dir.get()) output_template = str(output_path / "%(uploader)s" / "%(upload_date)s - %(title)s.%(ext)s") cmd.extend(['-o', output_template]) # Quality settings if self.audio_only.get(): cmd.extend(['-f', 'bestaudio/best']) else: if self.quality.get() == 'best': cmd.extend(['-f', 'best']) elif self.quality.get() == 'worst': cmd.extend(['-f', 'worst']) else: # Custom quality quality_num = self.quality.get().replace('p', '') cmd.extend(['-f', f'best[height<={quality_num}]']) # Additional options cmd.extend([ '--ignore-errors', '--no-overwrites', '--continue', ]) # Archive file archive_file = output_path / 'download_archive.txt' cmd.extend(['--download-archive', str(archive_file)]) # Metadata and thumbnails if self.download_metadata.get(): cmd.extend(['--write-info-json', '--write-description']) if self.download_thumbnails.get(): cmd.extend(['--write-thumbnail']) # Add channel URL cmd.append(self.channel_url.get().strip()) return cmd def get_ytdlp_command(self): """Determine the best way to call yt-dlp based on what's available.""" # Method 1: Try direct yt-dlp command try: subprocess.run(['yt-dlp', '--version'], capture_output=True, text=True, check=True, timeout=5) return ['yt-dlp'] except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired): pass # Method 2: Try through Python module python_exe = self.get_python_executable() if python_exe: try: subprocess.run([python_exe, '-m', 'yt_dlp', '--version'], capture_output=True, text=True, check=True, timeout=5) return [python_exe, '-m', 'yt_dlp'] except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired): pass # Method 3: Fallback to direct command (might fail but user will see error) return ['yt-dlp'] def start_download(self): """Start the download process.""" if self.is_downloading: self.stop_download() return if not self.validate_inputs(): return # Clear previous output and reset progress self.clear_output() self.reset_progress() # Update UI self.is_downloading = True self.download_btn.config(text="Stop Download") self.progress.config(mode='indeterminate') self.progress.start() self.progress_label.config(text="Initializing download...") self.status_var.set("Downloading...") # Build command cmd = self.build_command() self.log_output(f"Starting download...") self.log_output(f"Channel: {self.channel_url.get()}") self.log_output(f"Output: {self.output_dir.get()}") self.log_output("-" * 50) # 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 reset_progress(self): """Reset progress tracking variables.""" self.total_videos = 0 self.downloaded_videos = 0 self.current_video = "" self.progress.config(value=0) self.progress_label.config(text="") def update_progress(self, downloaded, total, current_title=""): """Update the progress bar and label.""" self.downloaded_videos = downloaded self.total_videos = total self.current_video = current_title if total > 0: percentage = (downloaded / total) * 100 self.progress.config(mode='determinate', value=percentage) # Truncate long titles display_title = current_title[:50] + "..." if len(current_title) > 50 else current_title self.progress_label.config( text=f"Progress: {downloaded}/{total} videos ({percentage:.1f}%) - {display_title}" ) else: self.progress.config(mode='indeterminate') self.progress_label.config(text="Analyzing channel...") def parse_download_output(self, line): """Parse yt-dlp output to extract progress information.""" line = line.strip() # Look for playlist info: [download] Downloading video 5 of 23: Title if "[download] Downloading video" in line: try: # Extract: "Downloading video 5 of 23: Title" parts = line.split(":") if len(parts) >= 2: video_part = parts[1].strip() # "Downloading video 5 of 23" title_part = ":".join(parts[2:]).strip() if len(parts) > 2 else "" # Extract numbers: "5 of 23" if " of " in video_part: numbers = video_part.split(" of ") if len(numbers) == 2: try: current = int(numbers[0].split()[-1]) # Get last word before "of" total = int(numbers[1].split()[0]) # Get first word after "of" self.root.after(0, lambda: self.update_progress(current, total, title_part)) except (ValueError, IndexError): pass except Exception: pass # Look for single video download progress elif "[download]" in line and "%" in line: # Extract percentage from lines like: [download] 45.2% of 123.45MiB at 1.23MiB/s ETA 00:30 try: if "%" in line: percent_part = line.split("%")[0] percent_value = float(percent_part.split()[-1]) # If we don't have total videos info, show single video progress if self.total_videos == 0: self.root.after(0, lambda: self.update_single_video_progress(percent_value)) except (ValueError, IndexError): pass # Look for playlist detection elif "[youtube] Extracting playlist information" in line or "playlist" in line.lower(): self.root.after(0, lambda: self.update_progress(0, 0, "Analyzing playlist...")) def update_single_video_progress(self, percentage): """Update progress for single video download.""" self.progress.config(mode='determinate', value=percentage) self.progress_label.config(text=f"Download progress: {percentage:.1f}%") def show_completion_notification(self, success=True, message=""): """Show a popup notification when download completes.""" if success: title = "Download Complete!" icon = "info" msg = f"Channel download completed successfully!\n\n" if self.total_videos > 0: msg += f"Downloaded {self.downloaded_videos} out of {self.total_videos} videos.\n" msg += f"Files saved to:\n{self.output_dir.get()}" else: title = "Download Finished" icon = "warning" msg = f"Download finished with some issues.\n\n{message}\n\n" msg += f"Files saved to:\n{self.output_dir.get()}" # Create custom notification window notification = tk.Toplevel(self.root) notification.title(title) notification.geometry("450x250") notification.resizable(False, False) notification.transient(self.root) notification.grab_set() # Center the notification notification.update_idletasks() x = (notification.winfo_screenwidth() // 2) - (notification.winfo_width() // 2) y = (notification.winfo_screenheight() // 2) - (notification.winfo_height() // 2) notification.geometry(f"+{x}+{y}") # Configure notification content frame = ttk.Frame(notification, padding="20") frame.pack(fill="both", expand=True) # Title title_label = ttk.Label(frame, text=title, font=('Arial', 14, 'bold')) title_label.pack(pady=(0, 10)) # Message message_label = ttk.Label(frame, text=msg, wraplength=400, justify="center") message_label.pack(pady=(0, 15)) # Buttons button_frame = ttk.Frame(frame) button_frame.pack(fill="x", pady=(10, 0)) def open_folder(): """Open the download folder.""" try: if platform.system() == "Windows": os.startfile(self.output_dir.get()) elif platform.system() == "Darwin": # macOS subprocess.run(["open", self.output_dir.get()]) else: # Linux subprocess.run(["xdg-open", self.output_dir.get()]) notification.destroy() except Exception as e: messagebox.showerror("Error", f"Could not open folder: {e}") ttk.Button(button_frame, text="Open Folder", command=open_folder).pack(side="left", padx=(0, 10)) ttk.Button(button_frame, text="OK", command=notification.destroy).pack(side="right") # Auto-close after 30 seconds notification.after(30000, notification.destroy) # Play system notification sound try: if platform.system() == "Windows": import winsound winsound.PlaySound("SystemExclamation", winsound.SND_ALIAS) except ImportError: pass # No sound on non-Windows or if winsound not available 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, creationflags=subprocess.CREATE_NO_WINDOW if platform.system() == "Windows" else 0 ) # Read output line by line for line in iter(self.download_process.stdout.readline, ''): if not self.is_downloading: break # Parse the line for progress information self.parse_download_output(line) # Send line to output display 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(('success', 'Download completed successfully!')) else: self.output_queue.put(('warning', f'Download finished with some errors (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[WARNING] 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.progress.config(mode='determinate') self.status_var.set("Ready to download!") 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 == 'success': self.log_output(f"\n{message}") self.status_var.set(message) self.show_completion_notification(success=True) elif msg_type == 'warning': self.log_output(f"\n{message}") self.status_var.set(message) self.show_completion_notification(success=False, message=message) elif msg_type == 'error': self.log_output(f"\n[ERROR] {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(): # Prevent recursive calls when frozen if getattr(sys, 'frozen', False): # Running as PyInstaller executable import multiprocessing multiprocessing.freeze_support() # Set up the main window root = tk.Tk() # Configure styling style = ttk.Style() try: style.theme_use('vista') # Windows native theme except: try: style.theme_use('clam') # Fallback modern theme except: pass # Use default # Configure button style style.configure('Accent.TButton', font=('Arial', 10, 'bold')) # Create the application app = YouTubeArchiverStandalone(root) # Handle window closing def on_closing(): if app.is_downloading: if messagebox.askokcancel("Quit", "Download in progress. 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}") # Set minimum size root.minsize(600, 500) # Run the application root.mainloop() if __name__ == "__main__": main()