332 lines
12 KiB
Python
332 lines
12 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
|
|
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()
|