Commit Graph
17 Commits
Author SHA1 Message Date
nmemmertandClaude Sonnet 4.6 e58e6de125 fix: eliminate false-positive duplicate mega-groups
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>
2026-08-19 16:20:33 -04:00
nmemmertandClaude Sonnet 4.6 64cca095ea feat: paginate duplicates view — 20 per page with Load more
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>
2026-08-19 15:54:31 -04:00
nmemmertandClaude Sonnet 4.6 4f7dd3b49b 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>
2026-08-19 15:45:46 -04:00
nmemmertandClaude Sonnet 4.6 bc2eb7307f perf: indexes, query rewrites, caching — fix slow load at 30k+ photos
- Expression index on strftime('%m-%d', date_taken) makes Memories instant
- Composite indexes: (deleted, date_taken), (deleted, starred), (deleted, media_type),
  (deleted, place_city), (deleted, camera_model), (collection_id, photo_id)
- Albums: replace window-function queries with GROUP BY + MAX(id) CTE — avoids
  materialising 30k rows × 3 times just to pick cover photos
- Library stats + Albums: 30s/60s in-memory cache, invalidated on any write
- Videos: cursor-based pagination (100/page) with IntersectionObserver infinite scroll
- Fix build error: mixed ?? and || operators in MemoriesView track count display

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-18 15:34:10 -04:00
nmemmertandClaude Sonnet 4.6 6f4e90a1c8 feat: play real music from local library in Memories slideshow
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>
2026-08-18 15:26:18 -04:00
nmemmertandClaude Sonnet 4.6 d35836131a feat: recently added albums, timeline scrubber, soft delete & trash
- Smart albums: "Last 7 days" and "Last 30 days" filter by date_imported
- Timeline scrubber: fixed right-edge year bar with click-to-jump navigation
- Soft delete: trash photos from lightbox; restore or permanently delete from Trash view
- EXIF columns: iso, aperture, shutter_speed, focal_length, lens_model
- Bulk actions: star/unstar and add-to-album with multi-select
- Download original button in lightbox toolbar
- EXIF detail expansion in detail panel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-18 15:14:59 -04:00
nmemmertandClaude Sonnet 4.6 51c752602a feat: Albums view, auto albums, EXIF details, download, bulk select
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>
2026-08-18 15:02:52 -04:00
nmemmertandClaude Sonnet 4.6 eeab631ac9 feat: video sidebar with year/city filters like library view
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-18 11:38:44 -04:00
nmemmertandClaude Sonnet 4.6 6b4bd88943 feat: video support + Memories music choices
Videos:
- Worker ingests .mp4 .mov .m4v .avi .mkv .webm .3gp via ffmpeg/ffprobe
  (frame extract → sharp resize, same thumbnail pipeline as photos)
- DB: media_type + duration columns with migration
- GET /api/videos returns all indexed videos
- GET /api/photos/:id/video streams original file with Range support
  for browser seek/scrub
- VideosView: 16:9 thumbnail grid, play-button overlay, duration badge
- Full-screen player: native <video controls>, ESC to close
- Library tab now excludes videos (dedicated Videos tab)
- Dockerfile: adds ffmpeg to runtime stage

Memories music choices:
- Three styles: ⚡ Upbeat (G-major, default), 🌙 Dreamy (A-minor),
  🎬 Cinematic (D-minor, dual-oscillator richness)
- Shared Web Audio engine (reverb, LPF, pad + melody)
- Style picker in header; active style icon shown in slideshow bar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-18 11:01:30 -04:00
nmemmertandClaude Sonnet 4.6 0cba67e02e feat: Memories tab — slideshow of photos on this day with ambient music
- 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>
2026-08-18 10:42:00 -04:00
nmemmertandClaude Sonnet 4.6 e14919fe17 feat: sync all iCloud libraries (personal + shared + shared albums)
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>
2026-08-18 10:24:29 -04:00
nmemmertandClaude Sonnet 4.6 586b783239 fix: remove invalid --no-progress flag from icloudpd command
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-18 10:11:27 -04:00
nmemmertandClaude Sonnet 4.6 4ff5a6c8ca feat: photo sync (iCloud + Amazon import), larger gear icon, Luma branding
- Settings: iCloud Photos section with icloudpd-backed sync — Apple ID,
  password, download path, cookie dir, live log output via SSE
- Settings: Amazon Photos one-time import — path to extracted data export,
  Add & Index button wires it directly into Watch Folders
- scripts/amazon_sync.py: stdlib-only downloader using Netscape cookie file
- server: POST /api/sync/icloud, /api/sync/amazon, GET /api/sync/stream
  (SSE log streaming matching ingest stream pattern)
- db: icloud/amazon setting keys; getSettings handles bool + string fields
- Gear icon: fontSize 16→20, button 38×38, color #555→#888
- favicon.svg: 6-blade aperture icon in Luma blue
- Browser tab title: shpm → Luma
- Dockerfile: installs icloudpd via pip3, copies scripts/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-18 09:30:43 -04:00
nmemmertandClaude Sonnet 4.6 3429ec1d80 feat: places (reverse geocoding) + folder browser
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>
2026-08-18 08:27:41 -04:00
nmemmertandClaude Sonnet 4.6 d18c855571 feat: settings panel + multi-folder scanning
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>
2026-08-18 07:58:44 -04:00
nmemmertandClaude Sonnet 4.6 f0e237ab2b feat: filter sidebar, ingest progress, starred photos
- 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>
2026-08-17 17:04:01 -04:00
nmemmertandClaude Sonnet 4.6 e0f4e8ff0d feat: initial Luma implementation
Full self-hosted photo manager with:
- Ingestion worker (chokidar + sharp + exifr, dHash perceptual hashing)
- SQLite schema with WAL mode (photos, tags, collections, duplicate_pairs)
- Express API with keyset pagination, tag/collection/search/date filters
- React + Vite UI: timeline, lightbox, detail panel, tag/collection editors
- Duplicate detection (Union-Find grouping) with review + dismiss UI
- Map view (Leaflet + OpenStreetMap, marker clustering)
- Docker multi-stage build + Compose with inline env config
- GitHub Actions: CI build check + ghcr.io image publish on main/tags

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-17 16:33:55 -04:00