Files
yt-channel-archiver/youtube_archiver_gui.py

510 lines
20 KiB
Python

#!/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
import platform
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
# Check Python installation on Windows first
if not self.check_python_installation():
return # Exit if Python installation failed
self.create_widgets()
self.check_dependencies()
# Start checking queue for output updates
self.root.after(100, self.check_queue)
def check_python_installation(self):
"""Check if Python is properly installed on Windows."""
if platform.system() != "Windows":
return True # Not Windows, proceed normally
try:
# Try to run python and get version
result = subprocess.run([sys.executable, "--version"],
capture_output=True, text=True, check=True)
python_version = result.stdout.strip()
print(f"Python is available: {python_version}")
return True
except (subprocess.CalledProcessError, FileNotFoundError):
# Python not found, try to install it
return self.install_python_windows()
def install_python_windows(self):
"""Install Python on Windows using the provided MSIX installer."""
installer_path = Path("windows/python.msix")
if not installer_path.exists():
messagebox.showerror(
"Python Installation Error",
f"Python is not installed and the installer was not found at:\n{installer_path.absolute()}\n\n"
"Please install Python manually from python.org or place the installer at the expected location."
)
return False
# Show installation dialog
result = messagebox.askyesno(
"Python Installation Required",
"Python is not installed on this system. Would you like to install it now?\n\n"
f"The installer will be launched from:\n{installer_path.absolute()}"
)
if not result:
messagebox.showinfo(
"Installation Cancelled",
"Python installation cancelled. The application cannot run without Python."
)
return False
try:
# Create a temporary window to show installation progress
install_window = tk.Toplevel(self.root)
install_window.title("Installing Python")
install_window.geometry("400x200")
install_window.resizable(False, False)
install_window.transient(self.root)
install_window.grab_set()
# Center the window
install_window.update_idletasks()
x = (install_window.winfo_screenwidth() // 2) - (install_window.winfo_width() // 2)
y = (install_window.winfo_screenheight() // 2) - (install_window.winfo_height() // 2)
install_window.geometry(f"+{x}+{y}")
# Add progress indicator and message
frame = ttk.Frame(install_window, padding="20")
frame.pack(fill="both", expand=True)
ttk.Label(frame, text="Installing Python...",
font=('Arial', 12, 'bold')).pack(pady=(0, 10))
progress = ttk.Progressbar(frame, mode='indeterminate')
progress.pack(fill="x", pady=(0, 10))
progress.start()
status_label = ttk.Label(frame, text="Launching installer...", wraplength=350)
status_label.pack(pady=(0, 10))
# Update the window
install_window.update()
# Launch the MSIX installer
status_label.config(text="Running Python installer. Please follow the installation prompts.")
install_window.update()
# Use PowerShell to install the MSIX package
powershell_cmd = f'Add-AppxPackage -Path "{installer_path.absolute()}"'
result = subprocess.run([
"powershell", "-Command", powershell_cmd
], capture_output=True, text=True)
progress.stop()
if result.returncode == 0:
status_label.config(text="Installation completed successfully!")
install_window.update()
# Wait a moment and then verify installation
self.root.after(2000, lambda: self.verify_python_installation(install_window))
return True
else:
# Installation failed
error_msg = result.stderr or result.stdout or "Unknown error occurred"
status_label.config(text=f"Installation failed: {error_msg[:100]}...")
install_window.update()
messagebox.showerror(
"Installation Failed",
f"Python installation failed:\n{error_msg}\n\n"
"Please try installing Python manually from python.org"
)
install_window.destroy()
return False
except Exception as e:
messagebox.showerror(
"Installation Error",
f"An error occurred while installing Python:\n{str(e)}\n\n"
"Please try installing Python manually from python.org"
)
if 'install_window' in locals():
install_window.destroy()
return False
def verify_python_installation(self, install_window):
"""Verify that Python was installed successfully."""
try:
# Try to find Python in common locations
possible_python_paths = [
"python",
"python3",
"py",
os.path.expandvars(r"%LOCALAPPDATA%\Microsoft\WindowsApps\python.exe"),
os.path.expandvars(r"%LOCALAPPDATA%\Programs\Python\Python*\python.exe"),
]
python_found = False
for python_path in possible_python_paths:
try:
result = subprocess.run([python_path, "--version"],
capture_output=True, text=True, check=True)
python_version = result.stdout.strip()
print(f"Python found at {python_path}: {python_version}")
python_found = True
break
except (subprocess.CalledProcessError, FileNotFoundError):
continue
install_window.destroy()
if python_found:
messagebox.showinfo(
"Installation Successful",
f"Python has been installed successfully!\n{python_version}\n\n"
"The application will now continue."
)
return True
else:
messagebox.showwarning(
"Installation Verification Failed",
"Python installation completed, but Python could not be found in the expected locations.\n\n"
"You may need to restart the application or your system for the installation to take effect."
)
return False
except Exception as e:
install_window.destroy()
messagebox.showerror(
"Verification Error",
f"Could not verify Python installation:\n{str(e)}"
)
return False
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)
# Only proceed if Python installation check passed
if not hasattr(app, 'output_queue'):
root.destroy()
return
# 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()