feat: extract and store GPS coords
This commit is contained in:
@@ -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}
|
||||||
`;
|
`;
|
||||||
|
|||||||
Reference in New Issue
Block a user