Previously the scanner's SHA-256 dedup was library-scoped: moving a book
between libraries created a duplicate row (new ID) while the old row went
missing and archived after two scans, orphaning reading progress and
annotations from the file that users still see.
processMediaFile now falls through to cross-library detection when the
same-library hash lookup misses:
- GetMediaItemsBySHA256AndSameLibraryType returns candidates in other
same-type libraries; each candidate's file is stat'd through ITS OWN
library's folders (the old existence check stat'd against the current
scan's folder tree, which is meaningless across libraries).
- File gone -> it is a move: the candidate must pass the target library's
type rules (ValidateMediaItemForLibrary), then the row is repointed via
MoveMediaItemToLibrary with the recomputed relative path. Reading
history, annotations, and collections follow automatically because the
row keeps its ID. Incompatible formats (e.g. a reflowable EPUB into a
manga library) are rejected with a processing issue instead of being
force-imported; the upsert on (media_item_id, issue_type) keeps
repeated scans from spamming duplicates.
- File still present -> deliberate multi-library copy: fall through to
normal import so both libraries keep independent rows.
Candidate selection is factored into selectMoveCandidate (pure function,
unit-tested in media_scanner_move_test.go): input arrives pre-ordered
(archived first, then most missing scans, then oldest) and only
candidates whose file is verifiably gone qualify, so deliberate copies
are never repointed.