2026-02-07 00:05:45 +00:00
|
|
|
/**
|
|
|
|
|
* Helper: Converts [Degrees, Minutes, Seconds] to Decimal Degrees
|
|
|
|
|
*/
|
|
|
|
|
const convertDMSToDD = (dms: number[] | undefined, ref: string | undefined): number | null => {
|
|
|
|
|
if (!dms || dms.length < 3) return null;
|
|
|
|
|
const [degrees, minutes, seconds] = dms;
|
|
|
|
|
let dd = degrees + (minutes / 60) + (seconds / 3600);
|
|
|
|
|
|
|
|
|
|
if (ref === 'S' || ref === 'W') {
|
|
|
|
|
dd = dd * -1;
|
|
|
|
|
}
|
|
|
|
|
return parseFloat(dd.toFixed(6));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* MAPPER: Transforms the raw database JSON into a structured GUI object.
|
|
|
|
|
* Designed to handle the lowercase keys generated by the sanitizer.
|
|
|
|
|
*/
|
|
|
|
|
export const mapImageMetadata = (metadata: any) => {
|
|
|
|
|
if (!metadata || !metadata.details) {
|
|
|
|
|
return { title: metadata?.title || "Unknown File" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { details, dimensions, title } = metadata;
|
|
|
|
|
const { image = {}, photo = {}, gpsInfo = {} } = details;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
fileName: title,
|
|
|
|
|
device: `${image.make || ''} ${image.model || ''}`.trim() || 'Unknown Device',
|
|
|
|
|
|
|
|
|
|
// Dates from EXIF are strings after sanitization
|
|
|
|
|
timestamp: photo.dateTimeOriginal ? new Date(photo.dateTimeOriginal) : null,
|
|
|
|
|
|
|
|
|
|
resolution: {
|
|
|
|
|
width: photo.pixelXDimension || dimensions?.split('x')[0],
|
|
|
|
|
height: photo.pixelYDimension || dimensions?.split('x')[1],
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
settings: {
|
|
|
|
|
aperture: photo.fNumber ? `f/${photo.fNumber}` : 'N/A',
|
|
|
|
|
shutterSpeed: photo.exposureTime
|
|
|
|
|
? (photo.exposureTime < 1
|
|
|
|
|
? `1/${Math.round(1 / photo.exposureTime)}s`
|
|
|
|
|
: `${photo.exposureTime}s`)
|
|
|
|
|
: 'N/A',
|
|
|
|
|
iso: photo.iSOSpeedRatings || 'N/A',
|
|
|
|
|
focalLength: photo.focalLength ? `${photo.focalLength}mm` : 'N/A',
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
location: {
|
|
|
|
|
latitude: convertDMSToDD(gpsInfo.gpsLatitude, gpsInfo.gpsLatitudeRef),
|
|
|
|
|
longitude: convertDMSToDD(gpsInfo.gpsLongitude, gpsInfo.gpsLongitudeRef),
|
|
|
|
|
altitude: gpsInfo.gpsAltitude ? Math.round(gpsInfo.gpsAltitude) : null,
|
|
|
|
|
mapUrl: (gpsInfo.gpsLatitude && gpsInfo.gpsLongitude)
|
|
|
|
|
? `https://www.google.com/maps?q=${convertDMSToDD(gpsInfo.gpsLatitude, gpsInfo.gpsLatitudeRef)},${convertDMSToDD(gpsInfo.gpsLongitude, gpsInfo.gpsLongitudeRef)}`
|
|
|
|
|
: null
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-02-12 11:55:43 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// src/lib/transformers.ts
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Transforms the raw database FileNode into a structured object for the UI.
|
|
|
|
|
*/
|
|
|
|
|
export const mapMetadata = (node: any) => {
|
|
|
|
|
if (!node) return { title: "Unknown File" };
|
|
|
|
|
|
|
|
|
|
const meta = node.metadata || {};
|
|
|
|
|
const fileName = node.name;
|
|
|
|
|
const extension = fileName.split('.').pop()?.toLowerCase();
|
|
|
|
|
|
|
|
|
|
// --- PDF Logic ---
|
|
|
|
|
if (extension === 'pdf') {
|
|
|
|
|
return {
|
|
|
|
|
fileName,
|
|
|
|
|
type: 'PDF Document',
|
|
|
|
|
title: meta.title || fileName,
|
|
|
|
|
author: meta.author || 'Unknown Author',
|
|
|
|
|
subject: meta.subject || 'N/A',
|
|
|
|
|
pageCount: meta.pageCount || 0,
|
|
|
|
|
keywords: meta.keywords || 'None',
|
|
|
|
|
textPreview: node.description || meta.textPreview || '', // description field often stores the preview
|
|
|
|
|
creator: meta.creator || 'N/A',
|
|
|
|
|
producer: meta.producer || 'N/A'
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Image Logic (Sharp/Exif) ---
|
|
|
|
|
if (['jpg', 'jpeg', 'png', 'webp'].includes(extension || '')) {
|
|
|
|
|
// Note: your extractor lowercase keys via the sanitizer
|
|
|
|
|
return {
|
|
|
|
|
fileName,
|
|
|
|
|
type: 'Image',
|
|
|
|
|
device: `${meta.image?.make || ''} ${meta.image?.model || ''}`.trim() || 'Unknown Device',
|
|
|
|
|
timestamp: meta.photo?.dateTimeOriginal ? new Date(meta.photo.dateTimeOriginal) : null,
|
|
|
|
|
settings: {
|
|
|
|
|
aperture: meta.photo?.fNumber ? `f/${meta.photo.fNumber}` : 'N/A',
|
|
|
|
|
iso: meta.photo?.iSOSpeedRatings || 'N/A',
|
|
|
|
|
focalLength: meta.photo?.focalLength ? `${meta.photo.focalLength}mm` : 'N/A',
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Default Fallback ---
|
|
|
|
|
return {
|
|
|
|
|
fileName,
|
|
|
|
|
type: extension?.toUpperCase() || 'File',
|
|
|
|
|
details: meta
|
|
|
|
|
};
|
2026-02-07 00:05:45 +00:00
|
|
|
};
|