From 49dde844817937017f5bc7920515f26ff418ab02 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:17:30 +0200 Subject: [PATCH] Add progress signals and fake progress timer Emit progress updates during URL analysis and simulate gradual progress for long-running yt-dlp operations. Added QTimer import and a progress_update Signal on AnalysisThread, with emits at multiple analysis milestones (e.g. 15, 30, 60, 70, 85, 90, 92, 95, 100). In AnalysisMixin introduced _analysis_timer and _fake_progress, connected thread progress to _handle_analysis_progress, and implemented a _update_fake_progress timer that slowly advances the progress bar between real updates. Timers are stopped on real progress, completion or error and the UI progress is reset appropriately to improve UX during long extraction steps. --- ytsage/gui/ytsage_gui_analysis.py | 68 ++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/ytsage/gui/ytsage_gui_analysis.py b/ytsage/gui/ytsage_gui_analysis.py index 46604ce..8abf141 100644 --- a/ytsage/gui/ytsage_gui_analysis.py +++ b/ytsage/gui/ytsage_gui_analysis.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, cast import json import subprocess -from PySide6.QtCore import QMetaObject, Qt, Q_ARG, QThread, Signal +from PySide6.QtCore import QMetaObject, Qt, Q_ARG, QThread, Signal, QTimer from PySide6.QtWidgets import QMessageBox from ..core.ytsage_utils import validate_video_url, parse_yt_dlp_error @@ -22,6 +22,7 @@ class AnalysisThread(QThread): """ # Signals for status updates status_update = Signal(str) + progress_update = Signal(int) # Signal for progress bar updates # Signals for playlist UI playlist_info_visible = Signal(bool) @@ -63,6 +64,7 @@ class AnalysisThread(QThread): """Main thread execution - performs URL analysis.""" try: self.status_update.emit(_("main_ui.analyzing_extracting_basic")) + self.progress_update.emit(15) url = self.url # Clean up the URL to handle both playlist and video URLs @@ -107,6 +109,7 @@ class AnalysisThread(QThread): return self.status_update.emit(_("main_ui.analyzing_extracting_ytdlp")) + self.progress_update.emit(30) # This will trigger fake progress in UI # Build command for basic info extraction cmd = [yt_dlp_path, "--dump-single-json", "--flat-playlist", "--no-warnings", url] @@ -156,6 +159,7 @@ class AnalysisThread(QThread): return self.status_update.emit(_("main_ui.analyzing_processing_data")) + self.progress_update.emit(60) # Prepare result data result_data: Dict[str, Any] = { @@ -184,6 +188,7 @@ class AnalysisThread(QThread): # Fetch full info for the first video to get formats self.status_update.emit(_("main_ui.analyzing_fetching_first_video")) + self.progress_update.emit(70) first_video_entry = playlist_entries[0] first_video_url = first_video_entry.get("url") @@ -235,20 +240,24 @@ class AnalysisThread(QThread): return self.status_update.emit(_("main_ui.analyzing_processing_formats_ytdlp")) + self.progress_update.emit(85) result_data["all_formats"] = video_info.get("formats", []) # Get thumbnail URL self.status_update.emit(_("main_ui.analyzing_loading_thumbnail_ytdlp")) + self.progress_update.emit(90) playlist_info = result_data.get("playlist_info") or {} thumbnail_url = playlist_info.get("thumbnail") or video_info.get("thumbnail") result_data["thumbnail_url"] = thumbnail_url # Handle subtitles self.status_update.emit(_("main_ui.analyzing_processing_subtitles_ytdlp")) + self.progress_update.emit(92) result_data["available_subtitles"] = video_info.get("subtitles", {}) result_data["available_automatic_subtitles"] = video_info.get("automatic_captions", {}) self.status_update.emit(_("main_ui.analyzing_updating_table")) + self.progress_update.emit(95) # Emit all results at once self.analysis_complete.emit(result_data) @@ -259,6 +268,8 @@ class AnalysisMixin: # Track the current analysis thread _analysis_thread: Optional[AnalysisThread] = None + _analysis_timer: Optional[QTimer] = None + _fake_progress: int = 0 def analyze_url(self) -> None: """Start URL analysis in a background thread.""" @@ -293,11 +304,18 @@ class AnalysisMixin: self.toggle_analysis_dependent_controls(enabled=False) self.signals.update_status.emit(_("main_ui.analyzing_preparing")) + self.signals.update_progress.emit(0) # Reset progress self.is_analyzing = True + + # Stop any existing timer + if self._analysis_timer: + self._analysis_timer.stop() + self._analysis_timer = None # Create and configure the analysis thread self._analysis_thread = AnalysisThread( url=url, + # ... arguments will be filled by ... usage below ... cookie_file_path=self.cookie_file_path, browser_cookies_option=self.browser_cookies_option, proxy_url=self.proxy_url, @@ -307,6 +325,7 @@ class AnalysisMixin: # Connect signals to handlers self._analysis_thread.status_update.connect(self.signals.update_status.emit) + self._analysis_thread.progress_update.connect(self._handle_analysis_progress) self._analysis_thread.playlist_info_visible.connect(self.signals.playlist_info_label_visible.emit) self._analysis_thread.playlist_info_text.connect(self.signals.playlist_info_label_text.emit) self._analysis_thread.playlist_select_btn_visible.connect(self.signals.playlist_select_btn_visible.emit) @@ -318,10 +337,53 @@ class AnalysisMixin: # Start the thread self._analysis_thread.start() + def _handle_analysis_progress(self, value: int) -> None: + """Handle progress updates from analysis thread.""" + self = cast("YTSageApp", self) + + # Stop fake timer if running on any real update + if self._analysis_timer: + self._analysis_timer.stop() + self._analysis_timer = None + + self.signals.update_progress.emit(value) + + # If we hit the extraction phase (30%), start fake progress + if value == 30: + self._fake_progress = 30 + self._analysis_timer = QTimer(self) + self._analysis_timer.timeout.connect(self._update_fake_progress) + self._analysis_timer.start(200) # Every 200ms + + def _update_fake_progress(self) -> None: + """Increment progress bar slowly during long operations.""" + self = cast("YTSageApp", self) + + # Asymptotically approach 85% + if self._fake_progress < 85: + # Slow down as we get higher + increment = 1 + if self._fake_progress > 60: + if self._fake_progress % 3 == 0: # Slower + increment = 1 + else: + increment = 0 + + if increment > 0: + self._fake_progress += increment + self.signals.update_progress.emit(self._fake_progress) + def _on_analysis_complete(self, result_data: Dict[str, Any]) -> None: """Handle successful analysis completion - runs in main thread.""" self = cast("YTSageApp", self) + # Stop fake timer + if self._analysis_timer: + self._analysis_timer.stop() + self._analysis_timer = None + + self.signals.update_progress.emit(100) + # Update instance variables with results (safe - we're in main thread) self.is_playlist = result_data["is_playlist"] self.playlist_info = result_data["playlist_info"] @@ -362,6 +424,10 @@ class AnalysisMixin: def _on_analysis_error(self, error_message: str) -> None: """Handle analysis error - runs in main thread.""" self = cast("YTSageApp", self) + if self._analysis_timer: + self._analysis_timer.stop() + self._analysis_timer = None + self.signals.update_progress.emit(0) self.signals.update_status.emit(error_message) def _on_analysis_finished(self) -> None: