Add frontend
This commit is contained in:
331
youtube_archiver_gui.py
Normal file
331
youtube_archiver_gui.py
Normal 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()
|
Reference in New Issue
Block a user