Scope cleanup routines to files created by the current download

cleanup_partial_files() deleted every *.part and *.fNNN.* file in the
whole download directory, and cleanup_subtitle_files() deleted any new
.vtt/.srt under it recursively - including files belonging to other
applications (e.g. a browser's own .part downloads in ~/Downloads).

Track every destination path yt-dlp reports for this download and
restrict both cleanup passes to those files and their .part/.ytdl
siblings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-07-25 01:22:05 +02:00
parent c54574224a
commit a1d1f87b65
+21 -6
View File
@@ -112,18 +112,23 @@ class DownloadThread(QThread):
self.last_file_path: Optional[str] = None # Initialize full file path storage self.last_file_path: Optional[str] = None # Initialize full file path storage
self.subtitle_files: List[str] = [] # Track subtitle files that are created self.subtitle_files: List[str] = [] # Track subtitle files that are created
self.initial_subtitle_files: Set[Path] = set() # Track initial subtitle files before download self.initial_subtitle_files: Set[Path] = set() # Track initial subtitle files before download
self.download_files: Set[Path] = set() # Every destination path this download wrote to
def cleanup_partial_files(self) -> None: def cleanup_partial_files(self) -> None:
"""Delete any partial files including .part and unmerged format-specific files""" """Delete partial files (.part/.ytdl and unmerged .fNNN. streams), but only
those belonging to destinations this download actually wrote — the download
directory may contain unrelated files from other applications."""
try: try:
pattern = re.compile(r"\.f\d+\.") # Pattern to match format codes like .f243. pattern = re.compile(r"\.f\d+\.") # Pattern to match format codes like .f243.
for file_path in self.path.iterdir(): for dest in self.download_files:
if file_path.suffix == ".part" or pattern.search(file_path.name): candidates = [dest.with_name(dest.name + ".part"), dest.with_name(dest.name + ".ytdl")]
if dest.suffix == ".part" or pattern.search(dest.name):
candidates.append(dest)
for file_path in candidates:
if file_path.exists():
self._safe_delete_with_retry(file_path) self._safe_delete_with_retry(file_path)
except Exception as e: except Exception as e:
logger.exception(f"Error cleaning partial files: {e}") logger.exception(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 = 5, delay: float = 2.0) -> None: def _safe_delete_with_retry(self, file_path: Path, max_retries: int = 5, delay: float = 2.0) -> None:
"""Safely delete a file with retry mechanism for file locking issues across platforms""" """Safely delete a file with retry mechanism for file locking issues across platforms"""
@@ -219,8 +224,15 @@ class DownloadThread(QThread):
logger.debug(f"Deleted {deleted_count[0]} of {len(self.subtitle_files)} tracked subtitle files") logger.debug(f"Deleted {deleted_count[0]} of {len(self.subtitle_files)} tracked subtitle files")
# --- Method 2: Delete new subtitle files not in initial set --- # --- Method 2: Delete new subtitle files not in initial set ---
# Only touch subtitles belonging to files this download wrote; the
# directory may contain subtitle files from other processes.
download_stems = {p.stem for p in self.download_files} | {Path(f).stem for f in self.subtitle_files or []}
new_subtitle_files: Set[Path] = { new_subtitle_files: Set[Path] = {
f for f in Path(self.path).rglob("*") if f.suffix in [".vtt", ".srt"] and f not in self.initial_subtitle_files f
for f in Path(self.path).rglob("*")
if f.suffix in [".vtt", ".srt"]
and f not in self.initial_subtitle_files
and any(f.name.startswith(stem) for stem in download_stems if stem)
} }
for subtitle_file in new_subtitle_files: for subtitle_file in new_subtitle_files:
deleted_count[1] += safe_delete(path=subtitle_file) deleted_count[1] += safe_delete(path=subtitle_file)
@@ -619,6 +631,7 @@ class DownloadThread(QThread):
filepath = dest_match.group(1).strip() filepath = dest_match.group(1).strip()
self.current_filename = Path(filepath).name self.current_filename = Path(filepath).name
self.last_file_path = filepath # Store the full path for later cleanup self.last_file_path = filepath # Store the full path for later cleanup
self.download_files.add(Path(filepath))
logger.debug(f"Extracted filename: {self.current_filename}") # DEBUG logger.debug(f"Extracted filename: {self.current_filename}") # DEBUG
# Check if this is an audio-only download by looking in the previous lines # Check if this is an audio-only download by looking in the previous lines
@@ -711,6 +724,7 @@ class DownloadThread(QThread):
dest_path = match.group(1).strip() dest_path = match.group(1).strip()
self.current_filename = Path(dest_path).name self.current_filename = Path(dest_path).name
self.last_file_path = dest_path self.last_file_path = dest_path
self.download_files.add(Path(dest_path))
logger.debug(f"Captured destination filename: {self.current_filename}") logger.debug(f"Captured destination filename: {self.current_filename}")
elif "Downloading API JSON" in line: elif "Downloading API JSON" in line:
self.status_signal.emit(_("download.processing_playlist")) self.status_signal.emit(_("download.processing_playlist"))
@@ -771,6 +785,7 @@ class DownloadThread(QThread):
merged_filepath = merger_match.group(1).strip() merged_filepath = merger_match.group(1).strip()
self.current_filename = Path(merged_filepath).name self.current_filename = Path(merged_filepath).name
self.last_file_path = merged_filepath self.last_file_path = merged_filepath
self.download_files.add(Path(merged_filepath))
logger.debug(f"Updated to merged filename: {self.current_filename}") logger.debug(f"Updated to merged filename: {self.current_filename}")
elif "SponsorBlock" in line: elif "SponsorBlock" in line:
self.status_signal.emit(_("download.removing_sponsor_segments")) self.status_signal.emit(_("download.removing_sponsor_segments"))