galleria_photo_face_rknn_op.../api_v1/scanner/scanPhotoSingle.js
2026-10-03 12:16:58 +02:00

419 lines
9 KiB
JavaScript

// api_v1/scanner/scanPhotoSingle.js
const path = require('path');
const fsp = require('fs/promises');
const processFile = require('./processFile');
const {
enqueueGeocodeJob,
} = require('./geocodeQueue');
const { sha256 } = require('./utils');
const { log } = require('./logger');
const { WEB_ROOT } = require('../config');
function normCountryCode(value) {
if (!value) return undefined;
const s = String(value).trim().toUpperCase();
return s.length ? s : undefined;
}
function normalizeLocation(location) {
if (!location) return null;
const countryCode = normCountryCode(
location.country_code ||
location.countryCode ||
location.isoCountryCode
);
const countyCode =
location.county_code ||
location.countyCode ||
undefined;
return {
...location,
// Paese
country: location.country || undefined,
country_code: countryCode,
// County / contea
county: location.county || undefined,
county_code: countyCode,
// Campi già esistenti
region: location.region || undefined,
city: location.city || undefined,
postcode: location.postcode || undefined,
address: location.address || undefined,
timezone: location.timezone || undefined,
time: location.time || undefined,
};
}
// ---------------------------------------------------------
// Controlla se un file esiste fisicamente
// ---------------------------------------------------------
async function fileExists(filePath) {
try {
await fsp.access(filePath);
return true;
} catch {
return false;
}
}
// ---------------------------------------------------------
// Controlla i thumbnail fisici
//
// FOTO -> min + avg + cast
// VIDEO -> min + avg
// ---------------------------------------------------------
async function thumbnailsExist(user, cartella, f) {
const thumbBase = path.join(
WEB_ROOT,
'photos',
user,
'thumbs',
cartella,
path.dirname(f.relPath)
);
const parsed = path.parse(f.name);
const baseName = parsed.name;
const thumbMin = path.join(
thumbBase,
`${baseName}_min.jpg`
);
const thumbAvg = path.join(
thumbBase,
`${baseName}_avg.jpg`
);
const isVideo = [
'.mp4',
'.mov',
'.m4v'
].includes(f.ext.toLowerCase());
// I video NON hanno cast
if (isVideo) {
return (
await fileExists(thumbMin) &&
await fileExists(thumbAvg)
);
}
// Le foto hanno min + avg + cast
const thumbCast = path.join(
thumbBase,
`${baseName}_cast.jpg`
);
return (
await fileExists(thumbMin) &&
await fileExists(thumbAvg) &&
await fileExists(thumbCast)
);
}
async function scanPhotoSingle(
db,
user,
cartella,
f,
newFiles,
prefix = ''
) {
const fileName = f.name;
const id = f.id;
const st = f.stat;
// Recupero precedente SENZA contentHash
let prev = await db('photos')
.select(
'id',
'size_bytes',
'mtimeMs',
'fast_hash',
'path'
)
.where({ id })
.first();
// Hash veloce basato su size + mtime
const fastHash = sha256(
`${st.size}-${st.mtimeMs}`
);
// ---------------------------------------------------------
// PATH CAMBIATO = NUOVO FILE
// ---------------------------------------------------------
if (prev && prev.path !== f.path) {
log(
`${prefix} 🟢 Nuovo/Modificato (path changed): ${fileName}`
);
prev = null;
}
// ---------------------------------------------------------
// NUOVO FILE
// ---------------------------------------------------------
if (!prev) {
log(`${prefix} 🟢 Nuovo: ${fileName}`);
const meta = await processFile(
user,
cartella,
f.relPath,
f.absPath,
f.ext,
st
);
meta.id = id;
meta.path = f.path;
// Normalizza location prima di salvarla e inviarla
meta.location = normalizeLocation(
meta.location
);
log(
`🌍 [LOCATION NEW] ${fileName} ` +
`location=${JSON.stringify(meta.location)}`
);
await db('photos').insert({
id: meta.id,
user,
cartella,
name: meta.name,
path: meta.path,
thub1: meta.thub1,
thub2: meta.thub2,
cast: meta.cast,
mime_type: meta.mime_type,
width: meta.width,
height: meta.height,
rotation: meta.rotation,
size_bytes: meta.size_bytes,
mtimeMs: meta.mtimeMs,
duration_ms: meta.duration_ms,
taken_at: meta.taken_at,
data: meta.data,
lat: meta.gps?.lat ?? null,
lon: meta.gps?.lng ?? null,
alt: meta.gps?.alt ?? null,
// Qui ora il JSON può contenere:
// country, country_code, county, county_code,
// region, city, ecc.
location: meta.location
? JSON.stringify(meta.location)
: null,
fast_hash: fastHash,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
});
await enqueueGeocodeJob({
entityType: 'photo',
entityId: meta.id,
lat: meta.gps?.lat,
lon: meta.gps?.lng,
});
newFiles.push(meta);
return meta;
}
// ---------------------------------------------------------
// IDENTICO (fastHash uguale)
//
// Se il file è identico:
// - se tutti i thumbnail esistono -> non fare niente
// - se manca anche un solo thumbnail -> riprocessa
//
// FOTO -> min + avg + cast
// VIDEO -> min + avg
// ---------------------------------------------------------
if (prev.fast_hash === fastHash) {
const thumbsOk = await thumbnailsExist(
user,
cartella,
f
);
if (thumbsOk) {
await db('photos')
.where({ id })
.update({
path: f.path,
size_bytes: st.size,
mtimeMs: st.mtimeMs,
fast_hash: fastHash,
// Anche un file identico aggiorna updated_at
updated_at: new Date().toISOString(),
});
return null;
}
// -------------------------------------------------------
// File identico ma thumbnail mancanti
// -------------------------------------------------------
log(
`${prefix} 🟡 File identico ma thumbnail mancanti: ${fileName}`
);
const meta = await processFile(
user,
cartella,
f.relPath,
f.absPath,
f.ext,
st
);
meta.id = id;
meta.path = f.path;
// Normalizza location
meta.location = normalizeLocation(
meta.location
);
log(
`🌍 [LOCATION THUMBS REBUILD] ${fileName} ` +
`location=${JSON.stringify(meta.location)}`
);
await db('photos')
.where({ id })
.update({
name: meta.name,
path: meta.path,
thub1: meta.thub1,
thub2: meta.thub2,
cast: meta.cast,
mime_type: meta.mime_type,
width: meta.width,
height: meta.height,
rotation: meta.rotation,
size_bytes: meta.size_bytes,
mtimeMs: meta.mtimeMs,
duration_ms: meta.duration_ms,
taken_at: meta.taken_at,
data: meta.data,
lat: meta.gps?.lat ?? null,
lon: meta.gps?.lng ?? null,
alt: meta.gps?.alt ?? null,
location: meta.location
? JSON.stringify(meta.location)
: null,
fast_hash: fastHash,
updated_at: new Date().toISOString(),
});
await enqueueGeocodeJob({
entityType: 'photo',
entityId: meta.id,
lat: meta.gps?.lat,
lon: meta.gps?.lng,
});
newFiles.push(meta);
return meta;
}
// ---------------------------------------------------------
// MODIFICATO (fastHash diverso)
// ---------------------------------------------------------
log(`${prefix} 🟠 Modificato: ${fileName}`);
const meta = await processFile(
user,
cartella,
f.relPath,
f.absPath,
f.ext,
st
);
meta.id = id;
meta.path = f.path;
// Normalizza location prima di salvarla e inviarla
meta.location = normalizeLocation(
meta.location
);
log(
`🌍 [LOCATION UPDATED] ${fileName} ` +
`location=${JSON.stringify(meta.location)}`
);
await db('photos')
.where({ id })
.update({
name: meta.name,
path: meta.path,
thub1: meta.thub1,
thub2: meta.thub2,
mime_type: meta.mime_type,
width: meta.width,
height: meta.height,
rotation: meta.rotation,
size_bytes: meta.size_bytes,
mtimeMs: meta.mtimeMs,
duration_ms: meta.duration_ms,
taken_at: meta.taken_at,
data: meta.data,
lat: meta.gps?.lat ?? null,
lon: meta.gps?.lng ?? null,
alt: meta.gps?.alt ?? null,
// Qui ora il JSON può contenere:
// country, country_code, county, county_code,
// region, city, ecc.
location: meta.location
? JSON.stringify(meta.location)
: null,
fast_hash: fastHash,
updated_at: new Date().toISOString(),
});
await enqueueGeocodeJob({
entityType: 'photo',
entityId: meta.id,
lat: meta.gps?.lat,
lon: meta.gps?.lng,
});
newFiles.push(meta);
return meta;
}
module.exports = scanPhotoSingle;