Fixed Windows yt-dlp verification
This commit is contained in:
@@ -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()
|
||||
|
||||
|
Reference in New Issue
Block a user