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.
This commit is contained in:
@@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, cast
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import subprocess
|
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 PySide6.QtWidgets import QMessageBox
|
||||||
|
|
||||||
from ..core.ytsage_utils import validate_video_url, parse_yt_dlp_error
|
from ..core.ytsage_utils import validate_video_url, parse_yt_dlp_error
|
||||||
@@ -22,6 +22,7 @@ class AnalysisThread(QThread):
|
|||||||
"""
|
"""
|
||||||
# Signals for status updates
|
# Signals for status updates
|
||||||
status_update = Signal(str)
|
status_update = Signal(str)
|
||||||
|
progress_update = Signal(int) # Signal for progress bar updates
|
||||||
|
|
||||||
# Signals for playlist UI
|
# Signals for playlist UI
|
||||||
playlist_info_visible = Signal(bool)
|
playlist_info_visible = Signal(bool)
|
||||||
@@ -63,6 +64,7 @@ class AnalysisThread(QThread):
|
|||||||
"""Main thread execution - performs URL analysis."""
|
"""Main thread execution - performs URL analysis."""
|
||||||
try:
|
try:
|
||||||
self.status_update.emit(_("main_ui.analyzing_extracting_basic"))
|
self.status_update.emit(_("main_ui.analyzing_extracting_basic"))
|
||||||
|
self.progress_update.emit(15)
|
||||||
|
|
||||||
url = self.url
|
url = self.url
|
||||||
# Clean up the URL to handle both playlist and video URLs
|
# Clean up the URL to handle both playlist and video URLs
|
||||||
@@ -107,6 +109,7 @@ class AnalysisThread(QThread):
|
|||||||
return
|
return
|
||||||
|
|
||||||
self.status_update.emit(_("main_ui.analyzing_extracting_ytdlp"))
|
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
|
# Build command for basic info extraction
|
||||||
cmd = [yt_dlp_path, "--dump-single-json", "--flat-playlist", "--no-warnings", url]
|
cmd = [yt_dlp_path, "--dump-single-json", "--flat-playlist", "--no-warnings", url]
|
||||||
@@ -156,6 +159,7 @@ class AnalysisThread(QThread):
|
|||||||
return
|
return
|
||||||
|
|
||||||
self.status_update.emit(_("main_ui.analyzing_processing_data"))
|
self.status_update.emit(_("main_ui.analyzing_processing_data"))
|
||||||
|
self.progress_update.emit(60)
|
||||||
|
|
||||||
# Prepare result data
|
# Prepare result data
|
||||||
result_data: Dict[str, Any] = {
|
result_data: Dict[str, Any] = {
|
||||||
@@ -184,6 +188,7 @@ class AnalysisThread(QThread):
|
|||||||
|
|
||||||
# Fetch full info for the first video to get formats
|
# Fetch full info for the first video to get formats
|
||||||
self.status_update.emit(_("main_ui.analyzing_fetching_first_video"))
|
self.status_update.emit(_("main_ui.analyzing_fetching_first_video"))
|
||||||
|
self.progress_update.emit(70)
|
||||||
first_video_entry = playlist_entries[0]
|
first_video_entry = playlist_entries[0]
|
||||||
first_video_url = first_video_entry.get("url")
|
first_video_url = first_video_entry.get("url")
|
||||||
|
|
||||||
@@ -235,20 +240,24 @@ class AnalysisThread(QThread):
|
|||||||
return
|
return
|
||||||
|
|
||||||
self.status_update.emit(_("main_ui.analyzing_processing_formats_ytdlp"))
|
self.status_update.emit(_("main_ui.analyzing_processing_formats_ytdlp"))
|
||||||
|
self.progress_update.emit(85)
|
||||||
result_data["all_formats"] = video_info.get("formats", [])
|
result_data["all_formats"] = video_info.get("formats", [])
|
||||||
|
|
||||||
# Get thumbnail URL
|
# Get thumbnail URL
|
||||||
self.status_update.emit(_("main_ui.analyzing_loading_thumbnail_ytdlp"))
|
self.status_update.emit(_("main_ui.analyzing_loading_thumbnail_ytdlp"))
|
||||||
|
self.progress_update.emit(90)
|
||||||
playlist_info = result_data.get("playlist_info") or {}
|
playlist_info = result_data.get("playlist_info") or {}
|
||||||
thumbnail_url = playlist_info.get("thumbnail") or video_info.get("thumbnail")
|
thumbnail_url = playlist_info.get("thumbnail") or video_info.get("thumbnail")
|
||||||
result_data["thumbnail_url"] = thumbnail_url
|
result_data["thumbnail_url"] = thumbnail_url
|
||||||
|
|
||||||
# Handle subtitles
|
# Handle subtitles
|
||||||
self.status_update.emit(_("main_ui.analyzing_processing_subtitles_ytdlp"))
|
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_subtitles"] = video_info.get("subtitles", {})
|
||||||
result_data["available_automatic_subtitles"] = video_info.get("automatic_captions", {})
|
result_data["available_automatic_subtitles"] = video_info.get("automatic_captions", {})
|
||||||
|
|
||||||
self.status_update.emit(_("main_ui.analyzing_updating_table"))
|
self.status_update.emit(_("main_ui.analyzing_updating_table"))
|
||||||
|
self.progress_update.emit(95)
|
||||||
|
|
||||||
# Emit all results at once
|
# Emit all results at once
|
||||||
self.analysis_complete.emit(result_data)
|
self.analysis_complete.emit(result_data)
|
||||||
@@ -259,6 +268,8 @@ class AnalysisMixin:
|
|||||||
|
|
||||||
# Track the current analysis thread
|
# Track the current analysis thread
|
||||||
_analysis_thread: Optional[AnalysisThread] = None
|
_analysis_thread: Optional[AnalysisThread] = None
|
||||||
|
_analysis_timer: Optional[QTimer] = None
|
||||||
|
_fake_progress: int = 0
|
||||||
|
|
||||||
def analyze_url(self) -> None:
|
def analyze_url(self) -> None:
|
||||||
"""Start URL analysis in a background thread."""
|
"""Start URL analysis in a background thread."""
|
||||||
@@ -293,11 +304,18 @@ class AnalysisMixin:
|
|||||||
self.toggle_analysis_dependent_controls(enabled=False)
|
self.toggle_analysis_dependent_controls(enabled=False)
|
||||||
|
|
||||||
self.signals.update_status.emit(_("main_ui.analyzing_preparing"))
|
self.signals.update_status.emit(_("main_ui.analyzing_preparing"))
|
||||||
|
self.signals.update_progress.emit(0) # Reset progress
|
||||||
self.is_analyzing = True
|
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
|
# Create and configure the analysis thread
|
||||||
self._analysis_thread = AnalysisThread(
|
self._analysis_thread = AnalysisThread(
|
||||||
url=url,
|
url=url,
|
||||||
|
# ... arguments will be filled by ... usage below ...
|
||||||
cookie_file_path=self.cookie_file_path,
|
cookie_file_path=self.cookie_file_path,
|
||||||
browser_cookies_option=self.browser_cookies_option,
|
browser_cookies_option=self.browser_cookies_option,
|
||||||
proxy_url=self.proxy_url,
|
proxy_url=self.proxy_url,
|
||||||
@@ -307,6 +325,7 @@ class AnalysisMixin:
|
|||||||
|
|
||||||
# Connect signals to handlers
|
# Connect signals to handlers
|
||||||
self._analysis_thread.status_update.connect(self.signals.update_status.emit)
|
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_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_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)
|
self._analysis_thread.playlist_select_btn_visible.connect(self.signals.playlist_select_btn_visible.emit)
|
||||||
@@ -318,10 +337,53 @@ class AnalysisMixin:
|
|||||||
# Start the thread
|
# Start the thread
|
||||||
self._analysis_thread.start()
|
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:
|
def _on_analysis_complete(self, result_data: Dict[str, Any]) -> None:
|
||||||
"""Handle successful analysis completion - runs in main thread."""
|
"""Handle successful analysis completion - runs in main thread."""
|
||||||
self = cast("YTSageApp", self)
|
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)
|
# Update instance variables with results (safe - we're in main thread)
|
||||||
self.is_playlist = result_data["is_playlist"]
|
self.is_playlist = result_data["is_playlist"]
|
||||||
self.playlist_info = result_data["playlist_info"]
|
self.playlist_info = result_data["playlist_info"]
|
||||||
@@ -362,6 +424,10 @@ class AnalysisMixin:
|
|||||||
def _on_analysis_error(self, error_message: str) -> None:
|
def _on_analysis_error(self, error_message: str) -> None:
|
||||||
"""Handle analysis error - runs in main thread."""
|
"""Handle analysis error - runs in main thread."""
|
||||||
self = cast("YTSageApp", self)
|
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)
|
self.signals.update_status.emit(error_message)
|
||||||
|
|
||||||
def _on_analysis_finished(self) -> None:
|
def _on_analysis_finished(self) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user