// 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;