Add frontend

This commit is contained in:
Kevin Thompson
2025-07-23 14:27:58 -05:00
parent 48ba590adb
commit b9041445ba
2 changed files with 331 additions and 0 deletions

331
youtube_archiver_gui.py Normal file
View File

@ -0,0 +1,331 @@
#!/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
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
self.create_widgets()
self.check_dependencies()
# Start checking queue for output updates
self.root.after(100, self.check_queue)
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)
# 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()