main_window: route _save_to_library through save_post_file
First Phase 2 site migration. _save_to_library shrinks from ~80 lines to ~30 by delegating to core.library_save.save_post_file. The "find existing copy and rename across folders" block is gone — same- post idempotency is now handled by the DB-backed filename column via _same_post_on_disk inside save_post_file. The thumbnail-copy block is extracted as a new _copy_library_thumb helper so _bulk_save (Phase 2.2) can call it too. behavior change from v0.2.3: cross-folder re-save is now copy, not move. Old folder's copy is preserved. The atomic-rename-move was a workaround for not having a DB-backed filename column; with _same_post_on_disk the workaround is unnecessary. Users who want move semantics can manually delete the old copy. Net diff: -52 lines.
This commit is contained in:
parent
9248dd77aa
commit
38937528ef
@ -2732,105 +2732,53 @@ class BooruApp(QMainWindow):
|
|||||||
else:
|
else:
|
||||||
self._status.showMessage("Image not cached yet — double-click to download first")
|
self._status.showMessage("Image not cached yet — double-click to download first")
|
||||||
|
|
||||||
def _save_to_library(self, post: Post, folder: str | None) -> None:
|
def _copy_library_thumb(self, post: Post) -> None:
|
||||||
"""Save (or relocate) an image in the library folder structure.
|
"""Copy a post's browse thumbnail into the library thumbnail
|
||||||
|
cache so the Library tab can paint it without re-downloading.
|
||||||
|
No-op if there's no preview_url or the source thumb isn't cached."""
|
||||||
|
if not post.preview_url:
|
||||||
|
return
|
||||||
|
from ..core.config import thumbnails_dir
|
||||||
|
from ..core.cache import cached_path_for
|
||||||
|
thumb_src = cached_path_for(post.preview_url, thumbnails_dir())
|
||||||
|
if not thumb_src.exists():
|
||||||
|
return
|
||||||
|
lib_thumb_dir = thumbnails_dir() / "library"
|
||||||
|
lib_thumb_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
lib_thumb = lib_thumb_dir / f"{post.id}.jpg"
|
||||||
|
if not lib_thumb.exists():
|
||||||
|
import shutil
|
||||||
|
shutil.copy2(thumb_src, lib_thumb)
|
||||||
|
|
||||||
If the post is already saved somewhere in the library, the existing
|
def _save_to_library(self, post: Post, folder: str | None) -> None:
|
||||||
file is renamed into the target folder rather than re-downloading.
|
"""Save a post into the library, optionally inside a subfolder.
|
||||||
This is what makes "Save to Library → SomeFolder" act like a move
|
|
||||||
when the post is already in Unfiled (or another folder), instead
|
Routes through the unified save_post_file flow so the filename
|
||||||
of producing a duplicate. rename() is atomic on the same filesystem
|
template, sequential collision suffixes, same-post idempotency,
|
||||||
so a crash mid-move can never leave both copies behind.
|
and library_meta write are all handled in one place. Re-saving
|
||||||
|
the same post into the same folder is a no-op (idempotent);
|
||||||
|
saving into a different folder produces a second copy without
|
||||||
|
touching the first.
|
||||||
"""
|
"""
|
||||||
from ..core.config import saved_dir, saved_folder_dir, MEDIA_EXTENSIONS
|
from ..core.config import saved_dir, saved_folder_dir
|
||||||
|
from ..core.library_save import save_post_file
|
||||||
|
|
||||||
self._status.showMessage(f"Saving #{post.id} to library...")
|
self._status.showMessage(f"Saving #{post.id} to library...")
|
||||||
|
|
||||||
# Resolve destination synchronously — saved_folder_dir() does
|
|
||||||
# the path-traversal check and may raise ValueError. Surface
|
|
||||||
# that error here rather than from inside the async closure.
|
|
||||||
try:
|
try:
|
||||||
if folder:
|
dest_dir = saved_folder_dir(folder) if folder else saved_dir()
|
||||||
dest_dir = saved_folder_dir(folder)
|
|
||||||
else:
|
|
||||||
dest_dir = saved_dir()
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
self._status.showMessage(f"Invalid folder name: {e}")
|
self._status.showMessage(f"Invalid folder name: {e}")
|
||||||
return
|
return
|
||||||
dest_resolved = dest_dir.resolve()
|
|
||||||
|
|
||||||
# Look for an existing copy of this post anywhere in the library.
|
|
||||||
# The library is shallow (root + one level of subdirectories) so
|
|
||||||
# this is cheap — at most one iterdir per top-level entry.
|
|
||||||
existing: Path | None = None
|
|
||||||
root = saved_dir()
|
|
||||||
if root.is_dir():
|
|
||||||
stem = str(post.id)
|
|
||||||
for entry in root.iterdir():
|
|
||||||
if entry.is_file() and entry.stem == stem and entry.suffix.lower() in MEDIA_EXTENSIONS:
|
|
||||||
existing = entry
|
|
||||||
break
|
|
||||||
if entry.is_dir():
|
|
||||||
for sub in entry.iterdir():
|
|
||||||
if sub.is_file() and sub.stem == stem and sub.suffix.lower() in MEDIA_EXTENSIONS:
|
|
||||||
existing = sub
|
|
||||||
break
|
|
||||||
if existing is not None:
|
|
||||||
break
|
|
||||||
|
|
||||||
async def _save():
|
async def _save():
|
||||||
try:
|
try:
|
||||||
if existing is not None:
|
src = Path(await download_image(post.file_url))
|
||||||
# Already in the library — relocate instead of re-saving.
|
save_post_file(src, post, dest_dir, self._db)
|
||||||
if existing.parent.resolve() != dest_resolved:
|
self._copy_library_thumb(post)
|
||||||
target = dest_dir / existing.name
|
|
||||||
if target.exists():
|
|
||||||
# Destination already has a file with the same
|
|
||||||
# name (matched by post id, so it's the same
|
|
||||||
# post). Drop the source to collapse the
|
|
||||||
# duplicate rather than leaving both behind.
|
|
||||||
existing.unlink()
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
existing.rename(target)
|
|
||||||
except OSError:
|
|
||||||
# Cross-device rename — fall back to copy+unlink.
|
|
||||||
import shutil as _sh
|
|
||||||
_sh.move(str(existing), str(target))
|
|
||||||
else:
|
|
||||||
# Not in the library yet — pull from cache and copy in.
|
|
||||||
path = await download_image(post.file_url)
|
|
||||||
ext = Path(path).suffix
|
|
||||||
dest = dest_dir / f"{post.id}{ext}"
|
|
||||||
if not dest.exists():
|
|
||||||
import shutil
|
|
||||||
shutil.copy2(path, dest)
|
|
||||||
|
|
||||||
# Copy browse thumbnail to library thumbnail cache
|
|
||||||
if post.preview_url:
|
|
||||||
from ..core.config import thumbnails_dir
|
|
||||||
from ..core.cache import cached_path_for as _cpf
|
|
||||||
thumb_src = _cpf(post.preview_url, thumbnails_dir())
|
|
||||||
if thumb_src.exists():
|
|
||||||
lib_thumb_dir = thumbnails_dir() / "library"
|
|
||||||
lib_thumb_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
lib_thumb = lib_thumb_dir / f"{post.id}.jpg"
|
|
||||||
if not lib_thumb.exists():
|
|
||||||
import shutil as _sh
|
|
||||||
_sh.copy2(thumb_src, lib_thumb)
|
|
||||||
|
|
||||||
# Store metadata for library search
|
|
||||||
self._db.save_library_meta(
|
|
||||||
post_id=post.id, tags=post.tags,
|
|
||||||
tag_categories=post.tag_categories,
|
|
||||||
score=post.score, rating=post.rating,
|
|
||||||
source=post.source, file_url=post.file_url,
|
|
||||||
)
|
|
||||||
|
|
||||||
where = folder or "Unfiled"
|
where = folder or "Unfiled"
|
||||||
self._signals.bookmark_done.emit(
|
self._signals.bookmark_done.emit(
|
||||||
self._grid.selected_index,
|
self._grid.selected_index,
|
||||||
f"Saved #{post.id} to {where}"
|
f"Saved #{post.id} to {where}",
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._signals.bookmark_error.emit(str(e))
|
self._signals.bookmark_error.emit(str(e))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user