feat(scanner): treat reappeared content as a move, not a duplicate

When the SHA-256 dedup found identical content already in the library,
the scan skipped the file as a duplicate - and after files moved
between folders the old row kept its stale path, cycled missing ->
archived, and the new path never took. The archive feature turned the
old destructive move behavior into a stuck move instead.

Now the dedup branch stats the old location: if it is gone, the book
was MOVED, so the row is repointed (file_path, file_size) with archive
state cleared and reading history intact. 'Skip as duplicate' only
applies when the old path still exists (a true copy). Verified live: a
moved EPUB kept its single row, followed the file, and never entered
the missing/archive cycle.
This commit is contained in:
John O'Keefe
2026-09-13 12:00:19 -04:00
parent 17216e9cc7
commit 249b1dfe93
+33
View File
@@ -795,6 +795,39 @@ func (s *MediaScanner) processMediaFile(ctx context.Context, path string) (bool,
LibraryID: libraryID,
})
if err == nil && existingByHash.ID.Valid {
// Same content at a different path. If the old location is gone,
// the book was MOVED: repoint the row so history follows it and
// the archive pass doesn't cycle it into missing/archived. Only
// treat it as a duplicate copy when the old path still exists.
oldPathStillExists := false
for _, folder := range s.folders {
if _, statErr := os.Stat(filepath.Join(folder, existingByHash.FilePath)); statErr == nil {
oldPathStillExists = true
break
}
}
if !oldPathStillExists {
if err := s.db.MoveMediaItemFilePath(ctx, database.MoveMediaItemFilePathParams{
ID: existingByHash.ID,
FilePath: s.getRelativePath(path),
FileSize: pgtype.Int8{Int64: info.Size(), Valid: true},
}); err != nil {
fmt.Printf("Warning: failed to repoint moved media item %s -> %s: %v\n", existingByHash.FilePath, path, err)
} else {
fmt.Printf("[MOVE] Item content moved: %q -> %s (history preserved)\n", existingByHash.FilePath, path)
s.logger.LogDelete(fmt.Sprintf("[MOVE] Repointed item '%s' from %q to %s", existingByHash.Title, existingByHash.FilePath, path))
}
if s.forceRescan {
moved := existingByHash
moved.FilePath = s.getRelativePath(path)
moved.ArchivedAt = pgtype.Timestamptz{}
moved.MissingScanCount = 0
_ = s.updateMediaItem(ctx, moved, path)
}
return false, nil
}
fmt.Printf("Media item with same SHA-256 already exists in library (path %q), skipping duplicate: %s\n",
existingByHash.FilePath, path)
// Content returned (possibly at a new path): restore archived rows.