feat: extract and store GPS coords

This commit is contained in:
William Valentin
2026-02-04 15:51:47 -08:00
parent d4a3bb3c42
commit 4180e7866c

View File

@@ -271,6 +271,75 @@ function parseExifDate(dateStr: string | undefined): Date | null {
return isNaN(date.getTime()) ? null : date; return isNaN(date.getTime()) ? null : date;
} }
function parseGpsParts(parts: number[]): number | null {
if (parts.length === 0 || !Number.isFinite(parts[0])) return null;
const [deg, min, sec] = parts;
const sign = deg < 0 ? -1 : 1;
let value = Math.abs(deg);
if (Number.isFinite(min)) value += Math.abs(min) / 60;
if (Number.isFinite(sec)) value += Math.abs(sec) / 3600;
return sign * value;
}
function parseGpsValue(value: unknown): number | null {
if (typeof value === "number") {
return Number.isFinite(value) ? value : null;
}
if (typeof value === "string") {
const trimmed = value.trim();
if (!trimmed) return null;
const direct = Number(trimmed);
if (!Number.isNaN(direct)) return direct;
const parts = trimmed.match(/-?\d+(?:\.\d+)?/g);
if (!parts) return null;
return parseGpsParts(parts.map((part) => Number(part)).filter(Number.isFinite));
}
if (Array.isArray(value)) {
const parts = value
.map((part) => {
if (typeof part === "number") return part;
if (typeof part === "string") return Number(part);
return NaN;
})
.filter(Number.isFinite);
return parseGpsParts(parts);
}
return null;
}
function applyRefSign(value: number, ref: unknown): number {
if (typeof ref !== "string") return value;
const normalized = ref.trim().toUpperCase();
if (normalized === "S" || normalized === "W") return -Math.abs(value);
if (normalized === "N" || normalized === "E") return Math.abs(value);
return value;
}
function parseGpsCoord(
value: unknown,
ref: unknown,
kind: "lat" | "lon",
): number | null {
const parsed = parseGpsValue(value);
if (parsed === null) return null;
const signed = applyRefSign(parsed, ref);
if (!Number.isFinite(signed)) return null;
if (kind === "lat") {
return signed >= -90 && signed <= 90 ? signed : null;
}
return signed >= -180 && signed <= 180 ? signed : null;
}
function extractGps(tags: Record<string, unknown>) {
const lat = parseGpsCoord(tags.GPSLatitude, tags.GPSLatitudeRef, "lat");
const lon = parseGpsCoord(tags.GPSLongitude, tags.GPSLongitudeRef, "lon");
if (lat === null || lon === null) return null;
return { lat, lon };
}
function isPlausibleCaptureTs(date: Date) { function isPlausibleCaptureTs(date: Date) {
const ts = date.getTime(); const ts = date.getTime();
if (!Number.isFinite(ts)) return false; if (!Number.isFinite(ts)) return false;
@@ -351,7 +420,9 @@ export async function handleProcessAsset(raw: unknown) {
thumb_small_key: null, thumb_small_key: null,
thumb_med_key: null, thumb_med_key: null,
poster_key: null, poster_key: null,
raw_tags_json: null raw_tags_json: null,
gps_lat: null,
gps_lon: null
}; };
let rawTags: Record<string, unknown> = {}; let rawTags: Record<string, unknown> = {};
let captureTs: Date | null = null; let captureTs: Date | null = null;
@@ -425,6 +496,11 @@ export async function handleProcessAsset(raw: unknown) {
if (asset.media_type === "image") { if (asset.media_type === "image") {
rawTags = await tryReadExifTags(); rawTags = await tryReadExifTags();
maybeSetCaptureDateFromTags(rawTags); maybeSetCaptureDateFromTags(rawTags);
const gps = extractGps(rawTags);
if (gps) {
updates.gps_lat = gps.lat;
updates.gps_lon = gps.lon;
}
await applyObjectMtimeFallback(); await applyObjectMtimeFallback();
@@ -470,6 +546,11 @@ export async function handleProcessAsset(raw: unknown) {
} else if (asset.media_type === "video") { } else if (asset.media_type === "video") {
rawTags = await tryReadExifTags(); rawTags = await tryReadExifTags();
maybeSetCaptureDateFromTags(rawTags); maybeSetCaptureDateFromTags(rawTags);
const gps = extractGps(rawTags);
if (gps) {
updates.gps_lat = gps.lat;
updates.gps_lon = gps.lon;
}
const ffprobeOutput = await runCommand("ffprobe", [ const ffprobeOutput = await runCommand("ffprobe", [
"-v", "-v",
@@ -583,7 +664,9 @@ export async function handleProcessAsset(raw: unknown) {
"thumb_small_key", "thumb_small_key",
"thumb_med_key", "thumb_med_key",
"poster_key", "poster_key",
"raw_tags_json" "raw_tags_json",
"gps_lat",
"gps_lon"
)}, status = 'ready', error_message = null )}, status = 'ready', error_message = null
where id = ${asset.id} where id = ${asset.id}
`; `;