Near-uniform images (sand close-ups, solid-color shots, blank frames)
have almost no gradient transitions so their dHash has very few set bits.
These match many unrelated photos because "low contrast ≈ low contrast."
- Skip photos whose pHash has <8 or >56 set bits out of 64 before
comparing — they can't be reliably compared by dHash
- Lower default threshold 5 → 3 (≤3 differing bits = genuinely near-
identical, e.g. same photo saved at different compression levels)
- Report "N compared" in scan result so skipped photos are visible
Run "Scan library" to replace existing pairs with clean results.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The union-find algorithm caused transitivity chains: A≈B and B≈C merged
A, B, C into one group even if A and C were completely different photos,
producing the 5000+ "duplicate" groups that were wrong.
- getGroups now returns individual pairs (2 photos each) — what was
actually flagged by the pHash comparison, no chaining
- scan() clears all existing pairs on each run so the result exactly
matches the current threshold (no stale loose pairs from prior scans)
- Default dedupe_threshold lowered from 10 → 5 (fewer false positives;
distance ≤5 means the images are genuinely near-identical)
Run "Scan library" once to replace the old results with clean pairs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Server returns page/limit/pages metadata; client fetches 20 groups at
a time and appends on "Load more". Shows remaining count in the button.
Dismiss and keep-best remove from the local list without a refetch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
Scans MUSIC_DIR (mounted at /music in Docker) with music-metadata to
read embedded BPM/genre tags and classify tracks as upbeat/cinematic/
dreamy. Falls back to untagged general pool when mood bucket is thin.
Slideshow plays real audio via <audio> element with range-request
streaming; Web Audio synth is kept as fallback when MUSIC_DIR is unset.
Shows now-playing title/artist in the slideshow bottom bar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Albums tab with manual album CRUD (create/delete, cover photo grid)
and Smart Albums auto-generated by year, city, and camera model using
SQLite window functions — no extra storage, computed from existing data.
Lightbox gets a download-original button (streams raw file with
Content-Disposition: attachment).
DetailPanel Camera section expands to show ISO, aperture, shutter speed,
focal length, and lens model extracted at ingest time; five new DB columns
added via safe ALTER TABLE migration.
Bulk selection: hover a photo to reveal a circle checkbox; selecting
any photo activates a floating action bar for star/unstar and add-to-album
across the whole selection. PhotoCard wrapped in React.memo to limit
re-renders to only affected cards.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New /api/memories/today endpoint queries photos by month+day (client
sends its local date to avoid UTC offset issues in Docker)
- MemoriesView shows photos grouped by year with a "Play Memories"
slideshow that crossfades between photos every 5 seconds
- Ambient music generated via Web Audio API — A-minor pad with slow
vibrato shimmer and a pentatonic melody with convolver reverb
- Mute toggle, keyboard nav (← → Esc), progress dots, year badge
- fix: map popup images now use explicit px dimensions (100% was
collapsing to 0 inside Leaflet's unsized popup content box)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Auto-discovers libraries via --list-libraries then runs icloudpd once
per library, each into its own subfolder under icloud_dest. Covers
PrimarySync, SharedSync-*, and SharedCollection-* automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Places:
- worker/geocode.js: Nominatim reverse geocoder, 1 req/sec rate limit,
in-memory cache keyed to 3-decimal-place coordinates
- Ingest calls reverseGeocode() for any photo with GPS data; stores
place_city and place_country on the photo row
- DB migrations add place_city / place_country columns + index on place_city
- Stats API returns cities grouped by count
- buildFilter() supports ?city= parameter
- FilterSidebar shows a Places section with city list + country sublabel
Folder browser:
- GET /api/fs/browse?path=… lists subdirectories (path-normalized to prevent traversal)
- Settings panel: 📁 button opens inline directory browser below the add
input — navigate with up arrow / directory clicks, Select fills the input
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Settings panel (⚙ in header):
- Watch Folders: add/remove paths, toggle enabled; worker polls every 30s
and calls watcher.add/unwatch without restart
- Ingestion: thumb/preview size and quality, applied per-file at ingest time
- Duplicate Detection: configurable Hamming distance threshold
- Danger Zone: clear all dismissed duplicate pairs
Multi-folder scanning:
- watch_folders table replaces single PHOTO_DIR env var
- PHOTO_DIR env var seeds the first entry on startup
- settings table with defaults for all tunable values
- Worker reads settings from DB on each drain cycle so changes are live
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Left filter sidebar: All Photos / Favorites / Date by Year / Tags / Collections
with counts, See more/less toggles, and blue active-row highlight
- SSE ingest badge in header: pulsing dot + filename + indexed count during scan;
auto-refreshes the library when a scan finishes
- Starred/Favorites: starred column on photos, star button in lightbox toolbar,
Favorites row in sidebar, ?starred=1 filter on /api/photos
- GET /api/library/stats endpoint (totals, years, tags, collections)
- GET /api/ingest/stream SSE endpoint (polls ingest_status table every 1.5s)
- Worker writes ingest_status (active, current file, scanned count) to SQLite
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>