diff --git a/windows/youtube_archiver_standalone.py b/windows/youtube_archiver_standalone.py index 44c389d..cecee67 100644 --- a/windows/youtube_archiver_standalone.py +++ b/windows/youtube_archiver_standalone.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ YouTube Channel Archiver - Standalone Windows Executable -Complete self-contained application with dependency management. +Fixed version that avoids subprocess recursion issues with PyInstaller. """ import tkinter as tk @@ -46,6 +46,35 @@ class YouTubeArchiverStandalone: # 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") @@ -135,6 +164,12 @@ class YouTubeArchiverStandalone: 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, @@ -191,7 +226,18 @@ class YouTubeArchiverStandalone: # 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") - self.python_status.config(text=f"Python: {python_version} [OK]", foreground="green") + + 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() @@ -219,29 +265,97 @@ class YouTubeArchiverStandalone: 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, daemon=True).start() + threading.Thread(target=self.do_install_ytdlp, args=(python_exe,), daemon=True).start() - def do_install_ytdlp(self): + def do_install_ytdlp(self, python_exe): """Actually install yt-dlp.""" try: # Install yt-dlp - result = subprocess.run([sys.executable, '-m', 'pip', 'install', 'yt-dlp'], - capture_output=True, text=True, check=True, timeout=60) + self.root.after(0, lambda: self.log_output(f"Installing yt-dlp using: {python_exe}")) - # Verify installation - result = subprocess.run(['yt-dlp', '--version'], - capture_output=True, text=True, check=True, timeout=10) - version = result.stdout.strip() + result = subprocess.run([python_exe, '-m', 'pip', 'install', 'yt-dlp'], + capture_output=True, text=True, check=True, timeout=120) - # Update UI from main thread - self.root.after(0, lambda: self.ytdlp_installed(version)) + 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: - self.root.after(0, lambda: self.ytdlp_install_failed(str(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.""" @@ -253,18 +367,78 @@ class YouTubeArchiverStandalone: """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. Check internet connection.") + 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" - "Please check your internet connection and try again.\n" - "You can also install it manually with: pip install yt-dlp") + "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! ===") @@ -301,7 +475,8 @@ class YouTubeArchiverStandalone: def build_command(self): """Build the yt-dlp command based on GUI settings.""" - cmd = ['yt-dlp'] + # Try to determine the best way to call yt-dlp + cmd = self.get_ytdlp_command() # Output template output_path = Path(self.output_dir.get()) @@ -344,6 +519,30 @@ class YouTubeArchiverStandalone: 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: @@ -446,6 +645,12 @@ class YouTubeArchiverStandalone: 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() diff --git a/youtube_archiver_standalone.py b/youtube_archiver_standalone.py deleted file mode 100644 index 44c389d..0000000 --- a/youtube_archiver_standalone.py +++ /dev/null @@ -1,492 +0,0 @@ -#!/usr/bin/env python3 -""" -YouTube Channel Archiver - Standalone Windows Executable -Complete self-contained application with dependency management. -""" - -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.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 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) - - # 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='indeterminate') - self.progress.grid(row=9, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10)) - - # Output text area - output_frame = ttk.LabelFrame(main_frame, text="Download Progress", padding="5") - output_frame.grid(row=10, 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(10, 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=11, 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") - 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.""" - 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, daemon=True).start() - - def do_install_ytdlp(self): - """Actually install yt-dlp.""" - try: - # Install yt-dlp - result = subprocess.run([sys.executable, '-m', 'pip', 'install', 'yt-dlp'], - capture_output=True, text=True, check=True, timeout=60) - - # Verify installation - 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_installed(version)) - - except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e: - self.root.after(0, lambda: self.ytdlp_install_failed(str(e))) - - 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. Check internet connection.") - - messagebox.showerror("Installation Error", - "Could not install yt-dlp automatically.\n\n" - "Please check your internet connection and try again.\n" - "You can also install it manually with: pip install yt-dlp") - - 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.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.""" - cmd = ['yt-dlp'] - - # 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 start_download(self): - """Start the download process.""" - 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...") - 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 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 - 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 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.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 == 'status': - self.log_output(f"\n{message}") - self.status_var.set(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(): - # 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()