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 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, } 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="Titolo: N/A") self.uploader_var = tk.StringVar(value="Autore: N/A") self.duration_var = tk.StringVar(value="Durata: N/A") self.master.title("YouTube Downloader") 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="Download Singolo") self.notebook.add(batch_tab, text="Download Multiplo") 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.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="Analizza URL", command=self._analyze_url) self.analyze_button.pack(side=tk.LEFT) info_frame = ttk.LabelFrame(tab, text="Informazioni Video") 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="Formato:").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="Aggiungi", 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", "Stato"), show="headings") self.batch_tree.heading("URL", text="URL") self.batch_tree.heading("Stato", text="Stato") self.batch_tree.column("URL", width=400) self.batch_tree.column("Stato", 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="Profilo Qualità:").pack(side=tk.LEFT) self.quality_profile_combobox = ttk.Combobox(controls_frame, state="readonly", values=["Qualità Massima (1080p+, richiede FFmpeg)", "Alta Qualità (720p)", "Qualità Media (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="Scarica Tutto", 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, "In attesa")) 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}") 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}") 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) 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("Titolo: N/A") self.uploader_var.set("Autore: N/A") self.duration_var.set("Durata: 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("Analisi in corso...") 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"Titolo: {video_info['title']}") self.uploader_var.set(f"Autore: {video_info['uploader']}") self.duration_var.set(f"Durata: {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("Analisi fallita.") # 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, "In corso...", "downloading") def _on_video_completed(self, url): self.master.after(0, self._update_batch_item, url, "Completato", "completed") def _on_video_error(self, url, error): self.master.after(0, self._update_batch_item, url, "Errore", "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, "Stato", 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 completato con successo!") def _on_batch_complete(self, error_message: Optional[str]): self.master.after(0, self._finalize_download, error_message, "Download multiplo completato!") 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)