From 377157d4158886c9a9e69566d7b30623b7bff8b6 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 22 Sep 2025 15:16:34 +0300 Subject: [PATCH] Improve partial file cleanup with retry and delays Added a _safe_delete_with_retry method to handle Windows file locking issues when deleting partial files. Introduced delays before cleanup after process termination to ensure file handles are released, reducing errors during file deletion. Error signal emission for cleanup failures is suppressed to avoid thread crashes. --- src/core/ytsage_downloader.py | 43 +++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/src/core/ytsage_downloader.py b/src/core/ytsage_downloader.py index d78f66c..029b119 100644 --- a/src/core/ytsage_downloader.py +++ b/src/core/ytsage_downloader.py @@ -99,13 +99,31 @@ class DownloadThread(QThread): pattern = re.compile(r"\.f\d+\.") # Pattern to match format codes like .f243. for file_path in self.path.iterdir(): if file_path.suffix == ".part" or pattern.search(file_path.name): - try: - file_path.unlink(missing_ok=True) - except Exception as e: - logger.exception(f"Error deleting {file_path.name}: {e}") + self._safe_delete_with_retry(file_path) except Exception as e: logger.exception(f"Error cleaning partial files: {e}") - self.error_signal.emit(f"Error cleaning partial files: {e}") + # Don't emit error signal for cleanup issues to avoid crashing the thread + logger.error(f"Error cleaning partial files: {e}") + + def _safe_delete_with_retry(self, file_path: Path, max_retries: int = 3, delay: float = 1.0) -> None: + """Safely delete a file with retry mechanism for Windows file locking issues""" + for attempt in range(max_retries): + try: + if file_path.exists(): + file_path.unlink(missing_ok=True) + logger.info(f"Successfully deleted {file_path.name}") + return + except PermissionError as e: + if "being used by another process" in str(e) and attempt < max_retries - 1: + logger.warning(f"File {file_path.name} is locked, retrying in {delay} seconds... (attempt {attempt + 1}/{max_retries})") + time.sleep(delay) + delay *= 1.5 # Exponential backoff + else: + logger.error(f"Failed to delete {file_path.name} after {max_retries} attempts: {e}") + return + except Exception as e: + logger.error(f"Error deleting {file_path.name}: {e}") + return def cleanup_subtitle_files(self) -> None: """Delete subtitle files after they have been merged into the video file""" @@ -425,6 +443,16 @@ class DownloadThread(QThread): for line in iter(self.process.stdout.readline, ""): # type: ignore if self.cancelled: self.process.terminate() + # Wait for process to actually terminate before cleaning up files + try: + self.process.wait(timeout=5) # Wait up to 5 seconds + except subprocess.TimeoutExpired: + logger.warning("Process didn't terminate gracefully, forcing kill") + self.process.kill() + self.process.wait() + + # Add delay before cleanup to allow file handles to be released + time.sleep(1) self.cleanup_partial_files() self.status_signal.emit("Download cancelled") return @@ -473,11 +501,16 @@ class DownloadThread(QThread): ) else: self.error_signal.emit(f"Download failed with return code {return_code}") + + # Add delay before cleanup to allow file handles to be released + time.sleep(1) self.cleanup_partial_files() except Exception as e: logger.exception(f"Error in direct command: {e}") self.error_signal.emit(f"Error in direct command: {e}") + # Add delay before cleanup to allow file handles to be released + time.sleep(1) self.cleanup_partial_files() def _parse_output_line(self, line) -> None: