From 2fcd20253231696dec48b832954383c944165494 Mon Sep 17 00:00:00 2001 From: Kevin Thompson Date: Sun, 10 Aug 2025 13:42:11 -0500 Subject: [PATCH] Show completion percentage and pop up when download is done --- ...=> youtube-channel-archiver-standalone.py} | 188 +++++++++++++++++- 1 file changed, 179 insertions(+), 9 deletions(-) rename windows/{youtube_archiver_standalone.py => youtube-channel-archiver-standalone.py} (77%) diff --git a/windows/youtube_archiver_standalone.py b/windows/youtube-channel-archiver-standalone.py similarity index 77% rename from windows/youtube_archiver_standalone.py rename to windows/youtube-channel-archiver-standalone.py index cecee67..f7341c3 100644 --- a/windows/youtube_archiver_standalone.py +++ b/windows/youtube-channel-archiver-standalone.py @@ -37,6 +37,9 @@ class YouTubeArchiverStandalone: self.download_process = None self.is_downloading = False self.dependencies_checked = False + self.total_videos = 0 + self.downloaded_videos = 0 + self.current_video = "" self.create_widgets() @@ -178,15 +181,19 @@ class YouTubeArchiverStandalone: self.download_btn.state(['disabled']) # Disabled until dependencies are ready # Progress bar - self.progress = ttk.Progressbar(main_frame, mode='indeterminate') - self.progress.grid(row=9, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10)) + self.progress = ttk.Progressbar(main_frame, mode='determinate', maximum=100) + self.progress.grid(row=9, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 5)) + + # Progress label + self.progress_label = ttk.Label(main_frame, text="", font=('Arial', 9)) + self.progress_label.grid(row=10, column=0, columnspan=3, pady=(0, 10)) # Output text area output_frame = ttk.LabelFrame(main_frame, text="Download Progress", padding="5") - output_frame.grid(row=10, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10)) + output_frame.grid(row=11, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10)) output_frame.columnconfigure(0, weight=1) output_frame.rowconfigure(0, weight=1) - main_frame.rowconfigure(10, weight=1) + main_frame.rowconfigure(11, weight=1) self.output_text = scrolledtext.ScrolledText(output_frame, height=10, width=80, font=('Consolas', 9)) @@ -196,7 +203,7 @@ class YouTubeArchiverStandalone: self.status_var = tk.StringVar(value="Checking system dependencies...") status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W) - status_bar.grid(row=11, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(5, 0)) + status_bar.grid(row=12, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(5, 0)) def paste_from_clipboard(self): """Paste URL from clipboard.""" @@ -552,13 +559,16 @@ The application will automatically detect yt-dlp once it's installed.""" if not self.validate_inputs(): return - # Clear previous output + # Clear previous output and reset progress self.clear_output() + self.reset_progress() # Update UI self.is_downloading = True self.download_btn.config(text="Stop Download") + self.progress.config(mode='indeterminate') self.progress.start() + self.progress_label.config(text="Initializing download...") self.status_var.set("Downloading...") # Build command @@ -573,6 +583,155 @@ The application will automatically detect yt-dlp once it's installed.""" self.download_thread.daemon = True self.download_thread.start() + def reset_progress(self): + """Reset progress tracking variables.""" + self.total_videos = 0 + self.downloaded_videos = 0 + self.current_video = "" + self.progress.config(value=0) + self.progress_label.config(text="") + + def update_progress(self, downloaded, total, current_title=""): + """Update the progress bar and label.""" + self.downloaded_videos = downloaded + self.total_videos = total + self.current_video = current_title + + if total > 0: + percentage = (downloaded / total) * 100 + self.progress.config(mode='determinate', value=percentage) + + # Truncate long titles + display_title = current_title[:50] + "..." if len(current_title) > 50 else current_title + + self.progress_label.config( + text=f"Progress: {downloaded}/{total} videos ({percentage:.1f}%) - {display_title}" + ) + else: + self.progress.config(mode='indeterminate') + self.progress_label.config(text="Analyzing channel...") + + def parse_download_output(self, line): + """Parse yt-dlp output to extract progress information.""" + line = line.strip() + + # Look for playlist info: [download] Downloading video 5 of 23: Title + if "[download] Downloading video" in line: + try: + # Extract: "Downloading video 5 of 23: Title" + parts = line.split(":") + if len(parts) >= 2: + video_part = parts[1].strip() # "Downloading video 5 of 23" + title_part = ":".join(parts[2:]).strip() if len(parts) > 2 else "" + + # Extract numbers: "5 of 23" + if " of " in video_part: + numbers = video_part.split(" of ") + if len(numbers) == 2: + try: + current = int(numbers[0].split()[-1]) # Get last word before "of" + total = int(numbers[1].split()[0]) # Get first word after "of" + self.root.after(0, lambda: self.update_progress(current, total, title_part)) + except (ValueError, IndexError): + pass + except Exception: + pass + + # Look for single video download progress + elif "[download]" in line and "%" in line: + # Extract percentage from lines like: [download] 45.2% of 123.45MiB at 1.23MiB/s ETA 00:30 + try: + if "%" in line: + percent_part = line.split("%")[0] + percent_value = float(percent_part.split()[-1]) + + # If we don't have total videos info, show single video progress + if self.total_videos == 0: + self.root.after(0, lambda: self.update_single_video_progress(percent_value)) + except (ValueError, IndexError): + pass + + # Look for playlist detection + elif "[youtube] Extracting playlist information" in line or "playlist" in line.lower(): + self.root.after(0, lambda: self.update_progress(0, 0, "Analyzing playlist...")) + + def update_single_video_progress(self, percentage): + """Update progress for single video download.""" + self.progress.config(mode='determinate', value=percentage) + self.progress_label.config(text=f"Download progress: {percentage:.1f}%") + + def show_completion_notification(self, success=True, message=""): + """Show a popup notification when download completes.""" + if success: + title = "Download Complete!" + icon = "info" + msg = f"Channel download completed successfully!\n\n" + if self.total_videos > 0: + msg += f"Downloaded {self.downloaded_videos} out of {self.total_videos} videos.\n" + msg += f"Files saved to:\n{self.output_dir.get()}" + else: + title = "Download Finished" + icon = "warning" + msg = f"Download finished with some issues.\n\n{message}\n\n" + msg += f"Files saved to:\n{self.output_dir.get()}" + + # Create custom notification window + notification = tk.Toplevel(self.root) + notification.title(title) + notification.geometry("450x250") + notification.resizable(False, False) + notification.transient(self.root) + notification.grab_set() + + # Center the notification + notification.update_idletasks() + x = (notification.winfo_screenwidth() // 2) - (notification.winfo_width() // 2) + y = (notification.winfo_screenheight() // 2) - (notification.winfo_height() // 2) + notification.geometry(f"+{x}+{y}") + + # Configure notification content + frame = ttk.Frame(notification, padding="20") + frame.pack(fill="both", expand=True) + + # Title + title_label = ttk.Label(frame, text=title, font=('Arial', 14, 'bold')) + title_label.pack(pady=(0, 10)) + + # Message + message_label = ttk.Label(frame, text=msg, wraplength=400, justify="center") + message_label.pack(pady=(0, 15)) + + # Buttons + button_frame = ttk.Frame(frame) + button_frame.pack(fill="x", pady=(10, 0)) + + def open_folder(): + """Open the download folder.""" + try: + if platform.system() == "Windows": + os.startfile(self.output_dir.get()) + elif platform.system() == "Darwin": # macOS + subprocess.run(["open", self.output_dir.get()]) + else: # Linux + subprocess.run(["xdg-open", self.output_dir.get()]) + notification.destroy() + except Exception as e: + messagebox.showerror("Error", f"Could not open folder: {e}") + + ttk.Button(button_frame, text="Open Folder", command=open_folder).pack(side="left", padx=(0, 10)) + ttk.Button(button_frame, text="OK", command=notification.destroy).pack(side="right") + + # Auto-close after 30 seconds + notification.after(30000, notification.destroy) + + # Play system notification sound + try: + if platform.system() == "Windows": + import winsound + winsound.PlaySound("SystemExclamation", winsound.SND_ALIAS) + except ImportError: + pass # No sound on non-Windows or if winsound not available + def run_download(self, cmd): """Run the download process.""" try: @@ -591,15 +750,20 @@ The application will automatically detect yt-dlp once it's installed.""" for line in iter(self.download_process.stdout.readline, ''): if not self.is_downloading: break + + # Parse the line for progress information + self.parse_download_output(line) + + # Send line to output display self.output_queue.put(('output', line.rstrip())) # Wait for process to complete return_code = self.download_process.wait() if return_code == 0: - self.output_queue.put(('status', 'Download completed successfully!')) + self.output_queue.put(('success', 'Download completed successfully!')) else: - self.output_queue.put(('status', f'Download finished with some errors (code {return_code})')) + self.output_queue.put(('warning', f'Download finished with some errors (code {return_code})')) except Exception as e: self.output_queue.put(('error', f'Error during download: {str(e)}')) @@ -619,6 +783,7 @@ The application will automatically detect yt-dlp once it's installed.""" self.is_downloading = False self.download_btn.config(text="Download Channel") self.progress.stop() + self.progress.config(mode='determinate') self.status_var.set("Ready to download!") def check_queue(self): @@ -629,9 +794,14 @@ The application will automatically detect yt-dlp once it's installed.""" if msg_type == 'output': self.log_output(message) - elif msg_type == 'status': + elif msg_type == 'success': self.log_output(f"\n{message}") self.status_var.set(message) + self.show_completion_notification(success=True) + elif msg_type == 'warning': + self.log_output(f"\n{message}") + self.status_var.set(message) + self.show_completion_notification(success=False, message=message) elif msg_type == 'error': self.log_output(f"\n[ERROR] {message}") messagebox.showerror("Download Error", message)