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)