Fixed Windows yt-dlp verification

This commit is contained in:
2025-08-10 13:35:51 -05:00
parent fdb44d4011
commit cd8476b506
2 changed files with 222 additions and 509 deletions

View File

@@ -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()