diff --git a/downloaderyoutube/_version.py b/downloaderyoutube/_version.py new file mode 100644 index 0000000..904bf58 --- /dev/null +++ b/downloaderyoutube/_version.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# File generated by PyInstaller GUI Wrapper. DO NOT EDIT MANUALLY. +# Contains build-time information scraped from Git (if available) +# and a helper function to format version strings. + +import re + +# --- Version Data (Generated) --- +__version__ = "v.0.0.0.2-0-gd985bc9-dirty" +GIT_COMMIT_HASH = "d985bc91cd936473742143d3532f603a59d2e395" +GIT_BRANCH = "master" +BUILD_TIMESTAMP = "2025-09-15T11:47:35.974283+00:00" +IS_GIT_REPO = True + +# --- Default Values (for comparison or fallback) --- +DEFAULT_VERSION = "0.0.0+unknown" +DEFAULT_COMMIT = "Unknown" +DEFAULT_BRANCH = "Unknown" + +# --- Helper Function --- +def get_version_string(format_string=None): + """ + Returns a formatted string based on the build version information. + + Args: + format_string (str, optional): A format string using placeholders. + Defaults to "{version} ({branch}/{commit_short})" if None. + Placeholders: + {{version}}: Full version string (e.g., 'v1.0.0-5-gabcdef-dirty') + {{tag}}: Clean tag part if exists (e.g., 'v1.0.0'), else DEFAULT_VERSION. + {{commit}}: Full Git commit hash. + {{commit_short}}: Short Git commit hash (7 chars). + {{branch}}: Git branch name. + {{dirty}}: '-dirty' if the repo was dirty, empty otherwise. + {{timestamp}}: Full build timestamp (ISO 8601 UTC). + {{timestamp_short}}: Build date only (YYYY-MM-DD). + {{is_git}}: 'Git' if IS_GIT_REPO is True, 'Unknown' otherwise. + + Returns: + str: The formatted version string, or an error message if formatting fails. + """ + if format_string is None: + format_string = "{version} ({branch}/{commit_short})" # Default format + + replacements = {} + try: + replacements['version'] = __version__ if __version__ else DEFAULT_VERSION + replacements['commit'] = GIT_COMMIT_HASH if GIT_COMMIT_HASH else DEFAULT_COMMIT + replacements['commit_short'] = GIT_COMMIT_HASH[:7] if GIT_COMMIT_HASH and len(GIT_COMMIT_HASH) >= 7 else DEFAULT_COMMIT + replacements['branch'] = GIT_BRANCH if GIT_BRANCH else DEFAULT_BRANCH + replacements['timestamp'] = BUILD_TIMESTAMP if BUILD_TIMESTAMP else "Unknown" + replacements['timestamp_short'] = BUILD_TIMESTAMP.split('T')[0] if BUILD_TIMESTAMP and 'T' in BUILD_TIMESTAMP else "Unknown" + replacements['is_git'] = "Git" if IS_GIT_REPO else "Unknown" + replacements['dirty'] = "-dirty" if __version__ and __version__.endswith('-dirty') else "" + + tag = DEFAULT_VERSION + if __version__ and IS_GIT_REPO: + match = re.match(r'^(v?([0-9]+(?:\.[0-9]+)*))', __version__) + if match: + tag = match.group(1) + replacements['tag'] = tag + + output_string = format_string + for placeholder, value in replacements.items(): + pattern = re.compile(r'{{\s*' + re.escape(placeholder) + r'\s*}}') + output_string = pattern.sub(str(value), output_string) + + if re.search(r'{\s*\w+\s*}', output_string): + pass # Or log a warning: print(f"Warning: Unreplaced placeholders found: {output_string}") + + return output_string + + except Exception as e: + return f"[Formatting Error: {e}]" diff --git a/downloaderyoutube/gui/gui.py b/downloaderyoutube/gui/gui.py index c960777..d251df6 100644 --- a/downloaderyoutube/gui/gui.py +++ b/downloaderyoutube/gui/gui.py @@ -1,10 +1,11 @@ - 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 @@ -20,6 +21,27 @@ LOGGING_CONFIG = { "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) @@ -29,11 +51,11 @@ class App(tk.Frame): 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.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("YouTube Downloader") + 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) @@ -47,8 +69,8 @@ class App(tk.Frame): 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.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) @@ -60,6 +82,10 @@ class App(tk.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) @@ -77,10 +103,10 @@ class App(tk.Frame): 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 = ttk.Button(url_frame, text="Analyze URL", command=self._analyze_url) self.analyze_button.pack(side=tk.LEFT) - info_frame = ttk.LabelFrame(tab, text="Informazioni Video") + 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) @@ -88,7 +114,7 @@ class App(tk.Frame): format_frame = ttk.Frame(tab) format_frame.pack(fill=tk.X, pady=5) - ttk.Label(format_frame, text="Formato:").pack(side=tk.LEFT) + 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") @@ -100,16 +126,16 @@ class App(tk.Frame): 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 = 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", "Stato"), show="headings") + self.batch_tree = ttk.Treeview(tree_frame, columns=("URL", "Status"), show="headings") self.batch_tree.heading("URL", text="URL") - self.batch_tree.heading("Stato", text="Stato") + self.batch_tree.heading("Status", text="Status") self.batch_tree.column("URL", width=400) - self.batch_tree.column("Stato", width=100, anchor="center") + 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") @@ -121,18 +147,18 @@ class App(tk.Frame): 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)"]) + 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="Scarica Tutto", command=self.start_batch_download) + 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, "In attesa")) + 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) @@ -146,6 +172,7 @@ class App(tk.Frame): 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: @@ -156,6 +183,21 @@ class App(tk.Frame): 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" @@ -164,6 +206,7 @@ class App(tk.Frame): 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") @@ -172,9 +215,9 @@ class App(tk.Frame): 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.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("") @@ -186,7 +229,7 @@ class App(tk.Frame): url = self.url_entry.get().strip() if not url: return self._set_ui_state(False) - self.format_combobox.set("Analisi in corso...") + self.format_combobox.set("Analyzing...") self.downloader.get_video_formats(url, self._on_formats_received) def _on_formats_received(self, video_info, formats): @@ -194,16 +237,16 @@ class App(tk.Frame): 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.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("Analisi fallita.") + self.format_combobox.set("Analysis failed.") # ALWAYS update UI state at the end self._set_ui_state(True) @@ -226,28 +269,28 @@ class App(tk.Frame): 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") + 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, "Completato", "completed") + 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, "Errore", "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, "Stato", status) + 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 completato con successo!") + 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, "Download multiplo completato!") + 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()