SXXXXXXX_DownloaderYouTube/downloaderyoutube/gui/gui.py
2025-09-15 14:20:35 +02:00

302 lines
14 KiB
Python

import logging
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from tkinter.scrolledtext import ScrolledText
from typing import Optional, List, Dict, Any
import os
import subprocess
import sys
from downloaderyoutube.core.core import Downloader, VideoFormat
from downloaderyoutube.utils.logger import add_tkinter_handler, get_logger
logger = get_logger(__name__)
CONFIG_FILE_NAME = ".last_download_path"
LOGGING_CONFIG = {
"colors": {
logging.DEBUG: "gray", logging.INFO: "black",
logging.WARNING: "orange", logging.ERROR: "red", logging.CRITICAL: "red",
},
"queue_poll_interval_ms": 100,
}
# --- Import Version Info FOR THE WRAPPER ITSELF ---
try:
# Use absolute import based on package name
from downloaderyoutube import _version as wrapper_version
WRAPPER_APP_VERSION_STRING = f"{wrapper_version.__version__} ({wrapper_version.GIT_BRANCH}/{wrapper_version.GIT_COMMIT_HASH[:7]})"
WRAPPER_BUILD_INFO = f"Wrapper Built: {wrapper_version.BUILD_TIMESTAMP}"
except ImportError:
# This might happen if you run the wrapper directly from source
# without generating its _version.py first (if you use that approach for the wrapper itself)
WRAPPER_APP_VERSION_STRING = "(Dev Wrapper)"
WRAPPER_BUILD_INFO = "Wrapper build time unknown"
# --- End Import Version Info ---
# --- Constants for Version Generation ---
DEFAULT_VERSION = "0.0.0+unknown"
DEFAULT_COMMIT = "Unknown"
DEFAULT_BRANCH = "Unknown"
# --- End Constants ---
class App(tk.Frame):
def __init__(self, master: tk.Tk):
super().__init__(master)
self.master = master
self.downloader = Downloader()
self.download_path: Optional[str] = None
self.available_formats: List[VideoFormat] = []
self.url_to_tree_item: Dict[str, str] = {}
self.title_var = tk.StringVar(value="Title: N/A")
self.uploader_var = tk.StringVar(value="Author: N/A")
self.duration_var = tk.StringVar(value="Duration: N/A")
self.master.title(f"YouTube Downloader- {WRAPPER_APP_VERSION_STRING}")
self.master.geometry("800x750")
self.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self._create_widgets()
self._setup_logging_handler()
self._load_last_path()
def _create_widgets(self):
self.notebook = ttk.Notebook(self)
self.notebook.pack(fill=tk.BOTH, expand=True)
single_tab = ttk.Frame(self.notebook, padding=10)
batch_tab = ttk.Frame(self.notebook, padding=10)
self.notebook.add(single_tab, text="Single Download")
self.notebook.add(batch_tab, text="Batch Download")
self._create_single_download_tab(single_tab)
self._create_batch_download_tab(batch_tab)
common_frame = ttk.Frame(self)
common_frame.pack(fill=tk.X, padx=5, pady=5)
path_frame = ttk.Frame(common_frame)
path_frame.pack(fill=tk.X, pady=5)
self.path_button = ttk.Button(path_frame, text="Choose Folder...", command=self.select_download_path)
self.path_button.pack(side=tk.LEFT)
self.open_folder_button = ttk.Button(path_frame, text="Open", command=self.open_download_folder, state="disabled")
self.open_folder_button.pack(side=tk.RIGHT)
self.path_label = ttk.Label(path_frame, text="No download folder selected.")
self.path_label.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=10)
self.progress_bar = ttk.Progressbar(common_frame, orient="horizontal", mode="determinate")
self.progress_bar.pack(fill=tk.X, pady=5)
log_frame = ttk.LabelFrame(self, text="Log")
log_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=(5, 0))
self.log_widget = ScrolledText(log_frame, state=tk.DISABLED, wrap=tk.WORD, font=("Courier New", 9))
self.log_widget.pack(fill=tk.BOTH, expand=True)
def _create_single_download_tab(self, tab: ttk.Frame):
url_frame = ttk.Frame(tab)
url_frame.pack(fill=tk.X, pady=(0, 5))
ttk.Label(url_frame, text="Video URL:").pack(side=tk.LEFT)
self.url_entry = ttk.Entry(url_frame)
self.url_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
self.analyze_button = ttk.Button(url_frame, text="Analyze URL", command=self._analyze_url)
self.analyze_button.pack(side=tk.LEFT)
info_frame = ttk.LabelFrame(tab, text="Video Information")
info_frame.pack(fill=tk.X, pady=5, expand=True)
ttk.Label(info_frame, textvariable=self.title_var).pack(anchor="w", padx=5)
ttk.Label(info_frame, textvariable=self.uploader_var).pack(anchor="w", padx=5)
ttk.Label(info_frame, textvariable=self.duration_var).pack(anchor="w", padx=5)
format_frame = ttk.Frame(tab)
format_frame.pack(fill=tk.X, pady=5)
ttk.Label(format_frame, text="Format:").pack(side=tk.LEFT)
self.format_combobox = ttk.Combobox(format_frame, state="disabled")
self.format_combobox.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
self.download_button = ttk.Button(format_frame, text="Download", command=self.start_single_download, state="disabled")
self.download_button.pack(side=tk.LEFT)
def _create_batch_download_tab(self, tab: ttk.Frame):
add_url_frame = ttk.Frame(tab)
add_url_frame.pack(fill=tk.X)
ttk.Label(add_url_frame, text="URL:").pack(side=tk.LEFT)
self.batch_url_entry = ttk.Entry(add_url_frame)
self.batch_url_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
self.add_url_button = ttk.Button(add_url_frame, text="Add", command=self._add_url_to_tree)
self.add_url_button.pack(side=tk.LEFT)
tree_frame = ttk.Frame(tab)
tree_frame.pack(fill=tk.BOTH, expand=True, pady=5)
self.batch_tree = ttk.Treeview(tree_frame, columns=("URL", "Status"), show="headings")
self.batch_tree.heading("URL", text="URL")
self.batch_tree.heading("Status", text="Status")
self.batch_tree.column("URL", width=400)
self.batch_tree.column("Status", width=100, anchor="center")
self.batch_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
tree_scrollbar = ttk.Scrollbar(tree_frame, orient="vertical", command=self.batch_tree.yview)
tree_scrollbar.pack(side=tk.RIGHT, fill="y")
self.batch_tree.config(yscrollcommand=tree_scrollbar.set)
self.batch_tree.tag_configure("downloading", foreground="blue")
self.batch_tree.tag_configure("completed", foreground="green")
self.batch_tree.tag_configure("error", foreground="red")
controls_frame = ttk.Frame(tab)
controls_frame.pack(fill=tk.X, pady=5)
ttk.Label(controls_frame, text="Quality Profile:").pack(side=tk.LEFT)
self.quality_profile_combobox = ttk.Combobox(controls_frame, state="readonly", values=['''Maximum Quality (1080p+, requires FFmpeg)[''', '''High Quality (720p)[''', '''Medium Quality (480p)['''])
self.quality_profile_combobox.current(1)
self.quality_profile_combobox.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
self.batch_download_button = ttk.Button(controls_frame, text="Download All", command=self.start_batch_download)
self.batch_download_button.pack(side=tk.LEFT)
def _add_url_to_tree(self):
url = self.batch_url_entry.get().strip()
if not url: return
if url not in self.url_to_tree_item:
item_id = self.batch_tree.insert("", tk.END, values=(url, "Waiting"))
self.url_to_tree_item[url] = item_id
self.batch_url_entry.delete(0, tk.END)
def _setup_logging_handler(self):
add_tkinter_handler(self.log_widget, self.master, LOGGING_CONFIG)
def _load_last_path(self):
if os.path.exists(CONFIG_FILE_NAME):
with open(CONFIG_FILE_NAME, "r") as f:
path = f.read().strip()
if os.path.isdir(path):
self.download_path = path
self.path_label.config(text=f"Saving to: {self.download_path}")
self.open_folder_button.config(state="normal")
def save_last_path(self):
if self.download_path:
with open(CONFIG_FILE_NAME, "w") as f: f.write(self.download_path)
def select_download_path(self):
path = filedialog.askdirectory()
if path:
self.download_path = path
self.path_label.config(text=f"Saving to: {self.download_path}")
self.open_folder_button.config(state="normal")
def open_download_folder(self):
if not self.download_path:
return
try:
if sys.platform == "win32":
os.startfile(self.download_path)
elif sys.platform == "darwin": # macOS
subprocess.run(["open", self.download_path])
else: # Linux and other UNIX-like
subprocess.run(["xdg-open", self.download_path])
except Exception as e:
logger.error(f"Failed to open folder: {e}")
messagebox.showerror("Error", f"Could not open the folder: {self.download_path}")
def _set_ui_state(self, enabled: bool):
state = "normal" if enabled else "disabled"
self.analyze_button.config(state=state)
self.download_button.config(state=state if self.available_formats and enabled else "disabled")
self.batch_download_button.config(state=state)
self.add_url_button.config(state=state)
self.path_button.config(state=state)
self.open_folder_button.config(state=state if self.download_path and enabled else "disabled")
for entry in [self.url_entry, self.batch_url_entry]:
entry.config(state="normal" if enabled else "disabled")
self.quality_profile_combobox.config(state="readonly" if enabled else "disabled")
def _reset_forms(self):
logger.info("Resetting forms to initial state.")
self.progress_bar["value"] = 0
self.url_entry.delete(0, tk.END)
self.title_var.set("Title: N/A")
self.uploader_var.set("Author: N/A")
self.duration_var.set("Duration: N/A")
self.available_formats = []
self.format_combobox["values"] = []
self.format_combobox.set("")
for item in self.batch_tree.get_children():
self.batch_tree.delete(item)
self.url_to_tree_item.clear()
def _analyze_url(self):
url = self.url_entry.get().strip()
if not url: return
self._set_ui_state(False)
self.format_combobox.set("Analyzing...")
self.downloader.get_video_formats(url, self._on_formats_received)
def _on_formats_received(self, video_info, formats):
self.master.after(0, self._update_ui_after_analysis, video_info, formats)
def _update_ui_after_analysis(self, video_info, formats):
if video_info and formats:
self.title_var.set(f"Title: {video_info['title']}")
self.uploader_var.set(f"Author: {video_info['uploader']}")
self.duration_var.set(f"Duration: {video_info['duration_string']}")
self.available_formats = formats
self.format_combobox["values"] = [str(f) for f in formats]
self.format_combobox.current(0)
self.format_combobox.config(state="readonly")
else:
messagebox.showerror("Error", "Could not retrieve video information.")
self.format_combobox.set("Analysis failed.")
# ALWAYS update UI state at the end
self._set_ui_state(True)
def start_single_download(self):
if not self.download_path: messagebox.showerror("Error", "Please select a download folder."); return
idx = self.format_combobox.current()
if idx < 0: messagebox.showerror("Error", "Please select a format."); return
self._set_ui_state(False)
self.progress_bar["value"] = 0
url = self.url_entry.get().strip()
self.downloader.download_video(url, self.download_path, self.available_formats[idx].format_id, self._on_download_progress, self._on_download_complete)
def start_batch_download(self):
if not self.download_path: messagebox.showerror("Error", "Please select a download folder."); return
urls = [self.batch_tree.item(item, "values")[0] for item in self.batch_tree.get_children()]
if not urls: messagebox.showerror("Error", "Please add at least one URL to the list."); return
self._set_ui_state(False)
self.progress_bar["value"] = 0
quality = self.quality_profile_combobox.get()
self.downloader.download_batch(urls, self.download_path, quality, self._on_download_progress, self._on_batch_complete, self._on_video_started, self._on_video_completed, self._on_video_error)
def _on_video_started(self, url):
self.master.after(0, self._update_batch_item, url, "Downloading...", "downloading")
def _on_video_completed(self, url):
self.master.after(0, self._update_batch_item, url, "Completed", "completed")
def _on_video_error(self, url, error):
self.master.after(0, self._update_batch_item, url, "Error", "error")
def _update_batch_item(self, url, status, tag):
if url in self.url_to_tree_item:
item_id = self.url_to_tree_item[url]
self.batch_tree.set(item_id, "Status", status)
self.batch_tree.item(item_id, tags=(tag,))
def _on_download_progress(self, percentage: int):
self.master.after(0, self.progress_bar.config, {"value": percentage})
def _on_download_complete(self, error_message: Optional[str]):
self.master.after(0, self._finalize_download, error_message, "Download completed successfully!")
def _on_batch_complete(self, error_message: Optional[str]):
self.master.after(0, self._finalize_download, error_message, "Batch download completed!")
def _finalize_download(self, error_message: Optional[str], success_message: str):
self._reset_forms()
self._set_ui_state(True)
if error_message:
messagebox.showerror("Download Failed", f"A problem occurred: {error_message}")
else:
messagebox.showinfo("Success", success_message)