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>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-08-18 08:27:41 -04:00
1 parent 64f7906b22
commit 3429ec1d80
8 files changed
+264 -25

No files matched your search

+11 -1
View File
@@ -4,6 +4,7 @@ import crypto from 'crypto';
import sharp from 'sharp';
import exifr from 'exifr';
import { computeDHash } from './hash.js';
import { reverseGeocode } from './geocode.js';
const SUPPORTED = new Set([
'.jpg', '.jpeg', '.png', '.webp', '.heic', '.heif',
@@ -37,6 +38,13 @@ export async function ingestFile(filepath, db, posterDir, settings = {}) {
const gpsLat = exif.latitude ?? null;
const gpsLon = exif.longitude ?? null;
let placeCity = null, placeCountry = null;
if (gpsLat !== null && gpsLon !== null) {
const place = await reverseGeocode(gpsLat, gpsLon);
placeCity = place?.city ?? null;
placeCountry = place?.country ?? null;
}
const { dir, thumb, preview } = posterPaths(filepath, posterDir, dateTaken ?? stat.mtime);
fs.mkdirSync(dir, { recursive: true });
@@ -78,7 +86,9 @@ export async function ingestFile(filepath, db, posterDir, settings = {}) {
phash,
width,
height,
filesize: stat.size,
filesize: stat.size,
place_city: placeCity,
place_country: placeCountry,
});
console.log(`[ingest] ${path.basename(filepath)}`);