fix: GPS extraction broken by exifr pick filter — use exifr.gps() separately

The pick option creates an allowlist for the merged EXIF output, which
drops latitude/longitude even when gps:true is set. Fixed by calling
exifr.gps() as a separate step (bypasses the pick filter entirely).

Also adds a startup backfill that re-extracts GPS for already-indexed
photos where gps_lat IS NULL, including reverse geocoding for place names.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-08-18 11:48:11 -04:00
1 parent eeab631ac9
commit 3c0cb2b68d
2 files changed
+48 -6

No files matched your search

+36
View File
@@ -2,8 +2,10 @@ import 'dotenv/config';
import chokidar from 'chokidar';
import path from 'path';
import fs from 'fs';
import exifr from 'exifr';
import { openDb } from './db.js';
import { ingestFile, isSupported } from './ingest.js';
import { reverseGeocode } from './geocode.js';
const PHOTO_DIR = process.env.PHOTO_DIR ?? null;
const DB_PATH = process.env.DB_PATH ?? './data/library.db';
@@ -107,6 +109,40 @@ function syncWatchFolders() {
syncWatchFolders();
setInterval(syncWatchFolders, 30_000);
// Backfill GPS for photos indexed before the exifr pick-filter bug was fixed
async function backfillGps() {
const rows = db.raw.prepare(
"SELECT id, filepath FROM photos WHERE gps_lat IS NULL"
).all().filter(r => fs.existsSync(r.filepath));
if (rows.length === 0) return;
console.log(`[worker] GPS backfill: checking ${rows.length} photos`);
const updateStmt = db.raw.prepare(
'UPDATE photos SET gps_lat = ?, gps_lon = ?, place_city = ?, place_country = ? WHERE id = ?'
);
let updated = 0;
for (const { id, filepath } of rows) {
try {
const gps = await exifr.gps(filepath);
if (gps?.latitude == null) continue;
let city = null, country = null;
try {
const place = await reverseGeocode(gps.latitude, gps.longitude);
city = place?.city ?? null;
country = place?.country ?? null;
} catch {}
updateStmt.run(gps.latitude, gps.longitude, city, country, id);
updated++;
} catch {}
}
if (updated > 0) console.log(`[worker] GPS backfill: updated ${updated} photos with location data`);
}
setTimeout(backfillGps, 5000);
process.on('SIGINT', () => {
console.log('\n[worker] shutting down');
db.setIngestStatus({ active: false, current: null, scanned, errors });
+12 -6
View File
@@ -44,14 +44,17 @@ async function ingestPhoto(filepath, db, posterDir, settings) {
try {
exif = await exifr.parse(filepath, {
pick: ['DateTimeOriginal', 'Make', 'Model', 'Orientation'],
gps: true,
}) ?? {};
} catch {}
const dateTaken = exif.DateTimeOriginal ?? null;
const cameraModel = [exif.Make, exif.Model].filter(Boolean).join(' ') || null;
const gpsLat = exif.latitude ?? null;
const gpsLon = exif.longitude ?? null;
let gpsLat = null, gpsLon = null;
try {
const gps = await exifr.gps(filepath);
if (gps?.latitude != null) { gpsLat = gps.latitude; gpsLon = gps.longitude; }
} catch {}
let placeCity = null, placeCountry = null;
if (gpsLat !== null && gpsLon !== null) {
@@ -142,12 +145,15 @@ async function ingestVideo(filepath, db, posterDir, settings) {
return;
}
// 2. exifr — GPS + more accurate date (best-effort)
// 2. exifr — more accurate date + GPS (best-effort, parsed separately to avoid pick filtering GPS)
let gpsLat = null, gpsLon = null;
try {
const exif = await exifr.parse(filepath, { gps: true, pick: ['DateTimeOriginal'] }) ?? {};
const exif = await exifr.parse(filepath, { pick: ['DateTimeOriginal'] }) ?? {};
if (exif.DateTimeOriginal) dateTaken = new Date(exif.DateTimeOriginal).toISOString();
if (exif.latitude != null) { gpsLat = exif.latitude; gpsLon = exif.longitude; }
} catch {}
try {
const gps = await exifr.gps(filepath);
if (gps?.latitude != null) { gpsLat = gps.latitude; gpsLon = gps.longitude; }
} catch {}
let placeCity = null, placeCountry = null;