From f6c5c6780d118a5cb1bd98661fc7a84e1b6047ec Mon Sep 17 00:00:00 2001 From: pax Date: Thu, 9 Apr 2026 17:05:16 -0500 Subject: [PATCH] main_window: route batch download paths through save_post_file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fourth Phase 2 site migration. Extracts a shared _batch_download_to helper that owns the async loop with a per-batch in_flight set, then makes both _batch_download (the dialog-driven entry) and _batch_download_posts (the multi-select entry) thin wrappers that delegate to it. Fixes the latent v0.2.3 bug where batch downloads landing inside saved_dir() never wrote library_meta rows — _on_batch_done painted saved-dots from disk but the search index stayed empty. The library_meta write is now automatic via save_post_file's is_relative_to(saved_dir()) check, so any batch into a library folder gets indexed for free. Also picks up filename templates and sequential collision suffixes across batch downloads — collision-prone templates like %artist% on a page of same-artist posts now produce someartist.jpg, someartist_1.jpg, someartist_2.jpg instead of clobbering. --- booru_viewer/gui/main_window.py | 75 +++++++++++++++------------------ 1 file changed, 35 insertions(+), 40 deletions(-) diff --git a/booru_viewer/gui/main_window.py b/booru_viewer/gui/main_window.py index 23cc1f0..c9135ed 100644 --- a/booru_viewer/gui/main_window.py +++ b/booru_viewer/gui/main_window.py @@ -2671,24 +2671,10 @@ class BooruApp(QMainWindow): self._run_async(_fav) def _batch_download_posts(self, posts: list, dest: str) -> None: - # Same _batch_dest stash as _batch_download — _on_batch_progress - # incrementally lights saved dots when dest is inside the library. - self._batch_dest = Path(dest) - async def _batch(): - for i, post in enumerate(posts): - try: - path = await download_image(post.file_url) - ext = Path(path).suffix - target = Path(dest) / f"{post.id}{ext}" - if not target.exists(): - import shutil - shutil.copy2(path, target) - self._signals.batch_progress.emit(i + 1, len(posts), post.id) - except Exception as e: - log.warning(f"Batch #{post.id} failed: {e}") - self._signals.batch_done.emit(f"Downloaded {len(posts)} images to {dest}") - - self._run_async(_batch) + """Multi-select Download All entry point. Delegates to + _batch_download_to so the in_flight set, library_meta write, + and saved-dots refresh share one implementation.""" + self._batch_download_to(posts, Path(dest)) def _is_current_bookmarked(self, index: int) -> bool: site_id = self._site_combo.currentData() @@ -2827,6 +2813,36 @@ class BooruApp(QMainWindow): # -- Batch download -- + def _batch_download_to(self, posts: list[Post], dest_dir: Path) -> None: + """Download `posts` into `dest_dir`, routing each save through + save_post_file with a shared in_flight set so collision-prone + templates produce sequential _1, _2 suffixes within the batch. + + Stashes `dest_dir` on `self._batch_dest` so _on_batch_progress + and _on_batch_done can decide whether the destination is inside + the library and the saved-dots need refreshing. The library_meta + write happens automatically inside save_post_file when dest_dir + is inside saved_dir() — fixes the v0.2.3 latent bug where batch + downloads into a library folder left files unregistered. + """ + from ..core.library_save import save_post_file + + self._batch_dest = dest_dir + self._status.showMessage(f"Downloading {len(posts)} images...") + in_flight: set[str] = set() + + async def _batch(): + for i, post in enumerate(posts): + try: + src = Path(await download_image(post.file_url)) + save_post_file(src, post, dest_dir, self._db, in_flight) + self._signals.batch_progress.emit(i + 1, len(posts), post.id) + except Exception as e: + log.warning(f"Batch #{post.id} failed: {e}") + self._signals.batch_done.emit(f"Downloaded {len(posts)} images to {dest_dir}") + + self._run_async(_batch) + def _batch_download(self) -> None: if not self._posts: self._status.showMessage("No posts to download") @@ -2835,28 +2851,7 @@ class BooruApp(QMainWindow): dest = select_directory(self, "Download to folder") if not dest: return - - # Stash dest so _on_batch_done can decide whether the destination - # is inside the library and the saved-dots need refreshing. - self._batch_dest = Path(dest) - posts = list(self._posts) - self._status.showMessage(f"Downloading {len(posts)} images...") - - async def _batch(): - for i, post in enumerate(posts): - try: - path = await download_image(post.file_url) - ext = Path(path).suffix - target = Path(dest) / f"{post.id}{ext}" - if not target.exists(): - import shutil - shutil.copy2(path, target) - self._signals.batch_progress.emit(i + 1, len(posts), post.id) - except Exception as e: - log.warning(f"Batch #{post.id} failed: {e}") - self._signals.batch_done.emit(f"Downloaded {len(posts)} images to {dest}") - - self._run_async(_batch) + self._batch_download_to(list(self._posts), Path(dest)) def _on_batch_progress(self, current: int, total: int, post_id: int) -> None: self._status.showMessage(f"Downloading {current}/{total}...")