Show completion percentage and pop up when download is done
This commit is contained in:
@@ -37,6 +37,9 @@ class YouTubeArchiverStandalone:
|
|||||||
self.download_process = None
|
self.download_process = None
|
||||||
self.is_downloading = False
|
self.is_downloading = False
|
||||||
self.dependencies_checked = False
|
self.dependencies_checked = False
|
||||||
|
self.total_videos = 0
|
||||||
|
self.downloaded_videos = 0
|
||||||
|
self.current_video = ""
|
||||||
|
|
||||||
self.create_widgets()
|
self.create_widgets()
|
||||||
|
|
||||||
@@ -178,15 +181,19 @@ class YouTubeArchiverStandalone:
|
|||||||
self.download_btn.state(['disabled']) # Disabled until dependencies are ready
|
self.download_btn.state(['disabled']) # Disabled until dependencies are ready
|
||||||
|
|
||||||
# Progress bar
|
# Progress bar
|
||||||
self.progress = ttk.Progressbar(main_frame, mode='indeterminate')
|
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, 10))
|
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 text area
|
||||||
output_frame = ttk.LabelFrame(main_frame, text="Download Progress", padding="5")
|
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.columnconfigure(0, weight=1)
|
||||||
output_frame.rowconfigure(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,
|
self.output_text = scrolledtext.ScrolledText(output_frame, height=10, width=80,
|
||||||
font=('Consolas', 9))
|
font=('Consolas', 9))
|
||||||
@@ -196,7 +203,7 @@ class YouTubeArchiverStandalone:
|
|||||||
self.status_var = tk.StringVar(value="Checking system dependencies...")
|
self.status_var = tk.StringVar(value="Checking system dependencies...")
|
||||||
status_bar = ttk.Label(main_frame, textvariable=self.status_var,
|
status_bar = ttk.Label(main_frame, textvariable=self.status_var,
|
||||||
relief=tk.SUNKEN, anchor=tk.W)
|
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):
|
def paste_from_clipboard(self):
|
||||||
"""Paste URL from clipboard."""
|
"""Paste URL from clipboard."""
|
||||||
@@ -552,13 +559,16 @@ The application will automatically detect yt-dlp once it's installed."""
|
|||||||
if not self.validate_inputs():
|
if not self.validate_inputs():
|
||||||
return
|
return
|
||||||
|
|
||||||
# Clear previous output
|
# Clear previous output and reset progress
|
||||||
self.clear_output()
|
self.clear_output()
|
||||||
|
self.reset_progress()
|
||||||
|
|
||||||
# Update UI
|
# Update UI
|
||||||
self.is_downloading = True
|
self.is_downloading = True
|
||||||
self.download_btn.config(text="Stop Download")
|
self.download_btn.config(text="Stop Download")
|
||||||
|
self.progress.config(mode='indeterminate')
|
||||||
self.progress.start()
|
self.progress.start()
|
||||||
|
self.progress_label.config(text="Initializing download...")
|
||||||
self.status_var.set("Downloading...")
|
self.status_var.set("Downloading...")
|
||||||
|
|
||||||
# Build command
|
# 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.daemon = True
|
||||||
self.download_thread.start()
|
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):
|
def run_download(self, cmd):
|
||||||
"""Run the download process."""
|
"""Run the download process."""
|
||||||
try:
|
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, ''):
|
for line in iter(self.download_process.stdout.readline, ''):
|
||||||
if not self.is_downloading:
|
if not self.is_downloading:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Parse the line for progress information
|
||||||
|
self.parse_download_output(line)
|
||||||
|
|
||||||
|
# Send line to output display
|
||||||
self.output_queue.put(('output', line.rstrip()))
|
self.output_queue.put(('output', line.rstrip()))
|
||||||
|
|
||||||
# Wait for process to complete
|
# Wait for process to complete
|
||||||
return_code = self.download_process.wait()
|
return_code = self.download_process.wait()
|
||||||
|
|
||||||
if return_code == 0:
|
if return_code == 0:
|
||||||
self.output_queue.put(('status', 'Download completed successfully!'))
|
self.output_queue.put(('success', 'Download completed successfully!'))
|
||||||
else:
|
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:
|
except Exception as e:
|
||||||
self.output_queue.put(('error', f'Error during download: {str(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.is_downloading = False
|
||||||
self.download_btn.config(text="Download Channel")
|
self.download_btn.config(text="Download Channel")
|
||||||
self.progress.stop()
|
self.progress.stop()
|
||||||
|
self.progress.config(mode='determinate')
|
||||||
self.status_var.set("Ready to download!")
|
self.status_var.set("Ready to download!")
|
||||||
|
|
||||||
def check_queue(self):
|
def check_queue(self):
|
||||||
@@ -629,9 +794,14 @@ The application will automatically detect yt-dlp once it's installed."""
|
|||||||
|
|
||||||
if msg_type == 'output':
|
if msg_type == 'output':
|
||||||
self.log_output(message)
|
self.log_output(message)
|
||||||
elif msg_type == 'status':
|
elif msg_type == 'success':
|
||||||
self.log_output(f"\n{message}")
|
self.log_output(f"\n{message}")
|
||||||
self.status_var.set(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':
|
elif msg_type == 'error':
|
||||||
self.log_output(f"\n[ERROR] {message}")
|
self.log_output(f"\n[ERROR] {message}")
|
||||||
messagebox.showerror("Download Error", message)
|
messagebox.showerror("Download Error", message)
|
Reference in New Issue
Block a user