fix: dedupe scan 500 error, SQLite limit crash, add keep-best action

- Replace BigInt hammingDistance with 32-bit popcount — safer (no throw
  on malformed hashes) and ~10× faster
- Make scan() async with setImmediate chunking so the event loop stays
  responsive on large libraries
- Batch getGroups() photo lookup in slices of 900 to stay under SQLite's
  999-variable limit
- Add POST /api/duplicates/keep-best endpoint: scores by resolution then
  filesize, soft-deletes the losers, dismisses the pair
- DuplicateGroup: green "Keep" badge on the best photo, green "Keep best"
  button in footer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-08-19 15:45:46 -04:00
1 parent 93e16561ec
commit 4f7dd3b49b
6 files changed
+205 -78

No files matched your search

+12 -7
View File
@@ -20,12 +20,17 @@ export async function computeDHash(filepath) {
return hash.toString(16).padStart(16, '0');
}
function popcount32(n) {
n = n >>> 0;
n -= (n >> 1) & 0x55555555;
n = (n & 0x33333333) + ((n >> 2) & 0x33333333);
n = (n + (n >> 4)) & 0x0f0f0f0f;
return Math.imul(n, 0x01010101) >>> 24;
}
export function hammingDistance(a, b) {
let xor = BigInt('0x' + a) ^ BigInt('0x' + b);
let dist = 0;
while (xor > 0n) {
dist += Number(xor & 1n);
xor >>= 1n;
}
return dist;
if (!a || !b || a.length !== 16 || b.length !== 16) return 64;
const xorHi = (parseInt(a.slice(0, 8), 16) ^ parseInt(b.slice(0, 8), 16)) >>> 0;
const xorLo = (parseInt(a.slice(8), 16) ^ parseInt(b.slice(8), 16)) >>> 0;
return popcount32(xorHi) + popcount32(xorLo);
}