Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ border-radius: 128px;
- Redesigned the Admin Tagging Status table to be more informative.

- Fixes
- Tag writes rename each comic before writing it, and the database follows
the file immediately. Bookmarks and read progress now survive a rename or
a CBR conversion even when a library scan lands in the middle of one.
- Editing a comic's tags twice in a row no longer fails the second edit with
a "no such file" error when renaming is on.
- Comics are never deleted from the database while their files are still on
disk, so a misread filesystem event can no longer take a comic's bookmarks
and read progress with it.
Expand Down
40 changes: 0 additions & 40 deletions codex/librarian/scribe/importer/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,6 @@
from codex.librarian.scribe.importer.tasks import ImportTask
from codex.librarian.scribe.search.status import SearchIndexCleanStatus
from codex.librarian.scribe.status import UpdateCollectionTimestampsStatus
from codex.librarian.scribe.tagwrite_moves import (
get_pending_tag_write_paths,
release_tag_write_moves,
)
from codex.librarian.worker import WorkerStatusBase
from codex.models import Library
from codex.settings import LOGLEVEL
Expand Down Expand Up @@ -162,41 +158,6 @@ def timed_step(self, name: str, method: Callable[[], Any]) -> Any:
self.phase_times[name] = self.phase_times.get(name, 0.0) + elapsed
return result

def _defer_pending_tag_write_moves(self) -> None:
"""
Leave paths a tag-write batch is still moving to that batch.

A scan that lands during a long tag write reports the conversion
it is watching as an unrelated delete plus create, and its task
outranks the tag writer's end-of-batch move by enqueue time.
Dropping those paths here makes the scan a no-op for them, so the
move still finds its source row — and its bookmarks — in place.

A task that carries a registered move reconciles it, so it both
releases that guard and is exempt from it — the tag writer's own
task keeps the re-read it asked for. The exemption is computed
from the task rather than from the release so a move that later
turns out to be unappliable can't cost the task its own paths.
Runs before the write wait and the status init so neither counts
a deferred path.
"""
release_tag_write_moves(self.task.files_moved)
own = frozenset(self.task.files_moved) | frozenset(
self.task.files_moved.values()
)
pending = get_pending_tag_write_paths() - own
if not pending:
return
deferred = pending & (
self.task.files_deleted | self.task.files_created | self.task.files_modified
)
if not deferred:
return
self.task.files_deleted -= pending
self.task.files_created -= pending
self.task.files_modified -= pending
self.log.info(f"Deferred {len(deferred)} path(s) to an in-flight tag write.")

def _wait_for_filesystem_ops_to_finish(self) -> bool:
"""Watcher sends events before filesystem events finish, so wait for them."""
started_checking = time()
Expand Down Expand Up @@ -423,7 +384,6 @@ def _init_librarian_status(self, path) -> None:
def init_apply(self) -> None:
"""Initialize the library and status flags."""
self.start_time = now()
self._defer_pending_tag_write_moves()
self.library.start_update()
if self._wait_for_filesystem_ops_to_finish():
# The import runs anyway: abandoning the task would drop these
Expand Down
Loading