From ee123b93d43004c9651a21b0fdd8d5b4632c76e2 Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Mon, 15 Sep 2025 13:07:35 +0200 Subject: [PATCH] add multiple download --- DownloaderYouTube.ico | Bin 0 -> 47803 bytes downloaderyoutube/core/core.py | 142 +++++++-------- downloaderyoutube/gui/gui.py | 312 ++++++++++++++++++--------------- 3 files changed, 228 insertions(+), 226 deletions(-) diff --git a/DownloaderYouTube.ico b/DownloaderYouTube.ico index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..c17cd84d9ae0f8eb7bdc8d1ca889121f89371a62 100644 GIT binary patch literal 47803 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 z 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 x@60Dz str: - """User-friendly string representation for the combobox.""" size_mb = f"{self.filesize / (1024 * 1024):.2f} MB" if self.filesize else "N/A" return f"{self.resolution} ({self.ext}) - {size_mb} - {self.note}" - class Downloader: - """ - Handles video operations using yt-dlp in separate threads to avoid - blocking the GUI. - """ - def __init__(self): + # Callbacks for single/overall progress self.progress_callback: Optional[Callable[[int], None]] = None self.completion_callback: Optional[Callable[[Optional[str]], None]] = None + # Callbacks for single video analysis self.formats_callback: Optional[Callable[[Optional[Dict[str, Any]], Optional[List[VideoFormat]]], None]] = None + # Callbacks for per-video status in a batch + self.video_started_callback: Optional[Callable[[str], None]] = None + self.video_completed_callback: Optional[Callable[[str], None]] = None + self.video_error_callback: Optional[Callable[[str, str], None]] = None def _progress_hook(self, d: Dict[str, Any]): - if d["status"] == "downloading": + if d["status"] == "downloading" and self.progress_callback: if d.get("total_bytes") and d.get("downloaded_bytes"): - percentage = (d["downloaded_bytes"] / d["total_bytes"]) * 100 - if self.progress_callback: - self.progress_callback(int(percentage)) - elif d["status"] == "finished": - logger.info("yt-dlp finished downloading.") + self.progress_callback(int((d["downloaded_bytes"] / d["total_bytes"]) * 100)) def _get_formats_task(self, url: str): - """Task to fetch video formats and info in a thread.""" try: - logger.info(f"Fetching formats for URL: {url}") - ydl_opts = {"noplaylist": True} - with yt_dlp.YoutubeDL(ydl_opts) as ydl: + with yt_dlp.YoutubeDL({"noplaylist": True, "quiet": True}) as ydl: info = ydl.extract_info(url, download=False) - - video_info = { - "title": info.get("title", "N/A"), - "uploader": info.get("uploader", "N/A"), - "duration_string": info.get("duration_string", "N/A"), - } - - formats = [] - for f in info.get("formats", []): - if f.get("vcodec", "none") != "none" and f.get("ext") == "mp4": - note = "Progressive" if f.get("acodec", "none") != "none" else "Video Only" - formats.append(VideoFormat( - format_id=f["format_id"], - resolution=f.get("format_note", f.get("resolution", "N/A")), - ext=f["ext"], - filesize=f.get("filesize"), - note=note - )) - + video_info = {"title": info.get("title", "N/A"), "uploader": info.get("uploader", "N/A"), "duration_string": info.get("duration_string", "N/A")} + formats = [VideoFormat(f["format_id"], f.get("format_note", f.get("resolution", "N/A")), f["ext"], f.get("filesize"), "Progressive" if f.get("acodec") != "none" else "Video Only") for f in info.get("formats", []) if f.get("vcodec", "none") != "none" and f.get("ext") == "mp4"] formats.sort(key=lambda x: (x.note != "Progressive", -int(x.resolution.replace("p", "")) if x.resolution.replace("p", "").isdigit() else 0)) - - logger.info(f"Found {len(formats)} suitable formats.") if self.formats_callback: self.formats_callback(video_info, formats) - except Exception as e: - error_message = f"Failed to fetch formats: {e}" - logger.error(error_message) - logger.debug(traceback.format_exc()) + logger.error(f"Failed to fetch formats: {e}") if self.formats_callback: - self.formats_callback(None, None) # Indicate failure + self.formats_callback(None, None) - def get_video_formats(self, url: str, formats_callback: Callable[[Optional[Dict[str, Any]], Optional[List[VideoFormat]]], None]): - """Starts the format fetching process in a new thread.""" + def get_video_formats(self, url: str, formats_callback: Callable): self.formats_callback = formats_callback - thread = Thread(target=self._get_formats_task, args=(url,), daemon=True) - thread.start() + Thread(target=self._get_formats_task, args=(url,), daemon=True).start() def _download_task(self, url: str, download_path: str, format_id: str): - """The actual download logic that runs in a separate thread.""" try: - logger.info(f"Starting download for URL: {url} with format: {format_id}") - ydl_opts = { - "format": format_id, - "outtmpl": f"{download_path}/%(uploader)s_%(title)s_%(format_note)s.%(ext)s", - "progress_hooks": [self._progress_hook], - "logger": logger, - "noplaylist": True, - } - + ydl_opts = {"format": format_id, "outtmpl": f"{download_path}/%(uploader)s_%(title)s_%(format_note)s.%(ext)s", "progress_hooks": [self._progress_hook], "logger": logger, "noplaylist": True} with yt_dlp.YoutubeDL(ydl_opts) as ydl: ydl.download([url]) - - logger.info(f"Successfully downloaded video from URL: {url}") if self.completion_callback: self.completion_callback(None) - except Exception as e: - error_message = f"An unexpected error occurred: {e}" - logger.error(error_message) - logger.debug(traceback.format_exc()) + logger.error(f"An unexpected error occurred: {e}") if self.completion_callback: - self.completion_callback(error_message) + self.completion_callback(str(e)) - def download_video( - self, - url: str, - download_path: str, - format_id: str, - progress_callback: Callable[[int], None], - completion_callback: Callable[[Optional[str]], None], - ): - """Starts the video download in a new thread.""" + def download_video(self, url: str, download_path: str, format_id: str, progress_callback: Callable, completion_callback: Callable): self.progress_callback = progress_callback self.completion_callback = completion_callback - thread = Thread( - target=self._download_task, args=(url, download_path, format_id), daemon=True - ) - thread.start() \ No newline at end of file + Thread(target=self._download_task, args=(url, download_path, format_id), daemon=True).start() + + def _get_format_from_profile(self, profile: str) -> str: + if "720p" in profile: + return "bestvideo[height<=720][ext=mp4]+bestaudio[ext=m4a]/best[height<=720][ext=mp4]/best" + elif "480p" in profile: + return "bestvideo[height<=480][ext=mp4]+bestaudio[ext=m4a]/best[height<=480][ext=mp4]/best" + else: + return "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best" + + def _batch_download_task(self, urls: List[str], download_path: str, quality_profile: str): + format_string = self._get_format_from_profile(quality_profile) + logger.info(f"Starting batch download for {len(urls)} videos with profile: {quality_profile}") + for i, url in enumerate(urls): + if not url.strip(): continue + if self.video_started_callback: + self.video_started_callback(url) + self.progress_callback(0) + try: + ydl_opts = {"format": format_string, "outtmpl": f"{download_path}/%(uploader)s_%(title)s_%(format_note)s.%(ext)s", "progress_hooks": [self._progress_hook], "logger": logger, "noplaylist": True} + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + ydl.download([url]) + if self.video_completed_callback: + self.video_completed_callback(url) + except Exception as e: + error_msg = str(e) + logger.error(f"Failed to download {url}. Reason: {error_msg}") + if self.video_error_callback: + self.video_error_callback(url, error_msg) + logger.info("--- Batch download finished ---") + if self.completion_callback: + self.completion_callback(None) + + def download_batch(self, urls: List[str], download_path: str, quality_profile: str, progress_callback: Callable, completion_callback: Callable, video_started_callback: Callable, video_completed_callback: Callable, video_error_callback: Callable): + self.progress_callback = progress_callback + self.completion_callback = completion_callback + self.video_started_callback = video_started_callback + self.video_completed_callback = video_completed_callback + self.video_error_callback = video_error_callback + Thread(target=self._batch_download_task, args=(urls, download_path, quality_profile), daemon=True).start() \ No newline at end of file diff --git a/downloaderyoutube/gui/gui.py b/downloaderyoutube/gui/gui.py index c48181b..c960777 100644 --- a/downloaderyoutube/gui/gui.py +++ b/downloaderyoutube/gui/gui.py @@ -1,3 +1,4 @@ + import logging import tkinter as tk from tkinter import ttk, filedialog, messagebox @@ -8,227 +9,250 @@ import os from downloaderyoutube.core.core import Downloader, VideoFormat from downloaderyoutube.utils.logger import add_tkinter_handler, get_logger -# Get a logger instance for this module logger = get_logger(__name__) -# --- Constants --- CONFIG_FILE_NAME = ".last_download_path" - -# A basic logging config dictionary for the Tkinter handler LOGGING_CONFIG = { "colors": { - logging.DEBUG: "gray", - logging.INFO: "black", - logging.WARNING: "orange", - logging.ERROR: "red", - logging.CRITICAL: "red", + logging.DEBUG: "gray", logging.INFO: "black", + logging.WARNING: "orange", logging.ERROR: "red", logging.CRITICAL: "red", }, "queue_poll_interval_ms": 100, } - class App(tk.Frame): - """ - The main graphical user interface for the Downloader application. - """ - 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] = {} - # StringVars for video info labels 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("800x650") - - self.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + 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): - """Creates and lays out the widgets in the application window.""" - # --- Top Frame for URL Input --- - url_frame = ttk.Frame(self) - url_frame.pack(fill=tk.X, pady=(0, 5)) + self.notebook = ttk.Notebook(self) + self.notebook.pack(fill=tk.BOTH, expand=True) - ttk.Label(url_frame, text="Video URL:").pack(side=tk.LEFT, padx=(0, 5)) - 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, padx=(5, 0)) + 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") - # --- Video Info Panel --- - info_frame = ttk.LabelFrame(self, text="Informazioni Video") - info_frame.pack(fill=tk.X, pady=5, padx=2) + self._create_single_download_tab(single_tab) + self._create_batch_download_tab(batch_tab) - 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) - - # --- Path Frame --- - path_frame = ttk.Frame(self) + 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 = 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) - # --- Format Selection Frame --- - format_frame = ttk.Frame(self) - format_frame.pack(fill=tk.X, pady=5) + self.progress_bar = ttk.Progressbar(common_frame, orient="horizontal", mode="determinate") + self.progress_bar.pack(fill=tk.X, pady=5) - ttk.Label(format_frame, text="Formato:").pack(side=tk.LEFT, padx=(0, 5)) - self.format_combobox = ttk.Combobox(format_frame, state="readonly") - 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_download, state="disabled" - ) - self.download_button.pack(side=tk.LEFT, padx=(5, 0)) - - # --- Progress Bar --- - self.progress_bar = ttk.Progressbar( - self, orient="horizontal", length=100, mode="determinate" - ) - self.progress_bar.pack(fill=tk.X, pady=(10, 5)) - - # --- Log Viewer --- log_frame = ttk.LabelFrame(self, text="Log") - log_frame.pack(fill=tk.BOTH, expand=True) - 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, padx=5, pady=5) + 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): - logger.info("Setting up GUI logging handler.") - add_tkinter_handler( - gui_log_widget=self.log_widget, - root_tk_instance_for_gui_handler=self.master, - logging_config_dict=LOGGING_CONFIG, - ) - logger.info("GUI logging handler configured.") + add_tkinter_handler(self.log_widget, self.master, LOGGING_CONFIG) def _load_last_path(self): - """Loads the last used download path from the config file.""" - try: - 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}") - logger.info(f"Loaded last download path: {path}") - except Exception as e: - logger.warning(f"Could not load last download path: {e}") + 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): - """Saves the current download path to the config file.""" if self.download_path: - try: - with open(CONFIG_FILE_NAME, "w") as f: - f.write(self.download_path) - logger.info(f"Saved last download path: {self.download_path}") - except Exception as e: - logger.warning(f"Could not save last download path: {e}") + 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}") - logger.info(f"Download path set 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() - if not url: - messagebox.showerror("Error", "Please enter a YouTube URL.") - return - - logger.info(f"Analyze button clicked. Fetching formats for {url}") - self.analyze_button.config(state="disabled") - self.download_button.config(state="disabled") + url = self.url_entry.get().strip() + if not url: return + self._set_ui_state(False) self.format_combobox.set("Analisi in corso...") - self.title_var.set("Titolo: Analisi in corso...") - self.uploader_var.set("Autore: Analisi in corso...") - self.duration_var.set("Durata: Analisi in corso...") self.downloader.get_video_formats(url, self._on_formats_received) - def _on_formats_received(self, video_info: Optional[Dict[str, Any]], formats: Optional[List[VideoFormat]]): + 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: Optional[Dict[str, Any]], formats: Optional[List[VideoFormat]]): - self.analyze_button.config(state="normal") + def _update_ui_after_analysis(self, video_info, formats): if video_info and formats: - logger.info("Successfully fetched 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") - self.download_button.config(state="normal") else: - logger.error("Failed to fetch video info or formats.") - messagebox.showerror("Error", "Could not retrieve video information. Check the URL and logs.") + messagebox.showerror("Error", "Could not retrieve video information.") self.format_combobox.set("Analisi fallita.") - self.format_combobox["values"] = [] - self.title_var.set("Titolo: N/A") - self.uploader_var.set("Autore: N/A") - self.duration_var.set("Durata: N/A") + # ALWAYS update UI state at the end + self._set_ui_state(True) - def start_download(self): - url = self.url_entry.get() - if not self.download_path: - messagebox.showerror("Error", "Please select a download folder.") - return - - selected_index = self.format_combobox.current() - if selected_index < 0: - messagebox.showerror("Error", "Please select a format to download.") - return - - selected_format = self.available_formats[selected_index] - logger.info(f"Download button clicked for format: {selected_format.format_id}") - - self.download_button.config(state="disabled") - self.analyze_button.config(state="disabled") + 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) - self.downloader.download_video( - url=url, - download_path=self.download_path, - format_id=selected_format.format_id, - progress_callback=self._on_download_progress, - completion_callback=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) + self.master.after(0, self._finalize_download, error_message, "Download completato con successo!") - def _finalize_download(self, error_message: Optional[str]): - self.progress_bar["value"] = 0 - self.download_button.config(state="normal") - self.analyze_button.config(state="normal") + 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", error_message) + messagebox.showerror("Download Failed", f"A problem occurred: {error_message}") else: - messagebox.showinfo("Success", "Video downloaded successfully!") \ No newline at end of file + messagebox.showinfo("Success", success_message)