const path = require("path"); const fsp = require("fs/promises"); const ExifReader = require("exifreader"); const sharp = require("sharp"); const { sha256, inferMimeFromExt, parseExifDateUtc } = require("./utils"); const { extractGpsFromExif, extractGpsWithExiftool } = require("./gps"); const { createVideoThumbnail, createThumbnails } = require("./thumbs"); const { probeVideo } = require("./video"); const { WEB_ROOT, PATH_FULL } = require("../config"); async function processFile(userName, cartella, fileRelPath, absPath, ext, st) { const isVideo = [".mp4", ".mov", ".m4v"].includes(ext); const thumbBase = path.join( WEB_ROOT, "photos", userName, "thumbs", cartella, path.dirname(fileRelPath), ); await fsp.mkdir(thumbBase, { recursive: true }); // --- Nome file + estensione (dal path ASSOLUTO, sempre corretto) --- const parsed = path.parse(absPath); const baseName = parsed.name; // IMG_0249 const extName = parsed.ext; // .JPG const absThumbMin = path.join(thumbBase, `${baseName}_min.jpg`); const absThumbAvg = path.join(thumbBase, `${baseName}_avg.jpg`); const absThumbCast = path.join(thumbBase, `${baseName}_cast.jpg`); if (isVideo) { await createVideoThumbnail(absPath, absThumbMin, absThumbAvg); } else { await createThumbnails(absPath, absThumbMin, absThumbAvg, absThumbCast); } // --- EXIF --- let tags = {}; try { tags = await ExifReader.load(absPath, { expanded: true }); } catch {} let timeRaw = tags?.exif?.DateTimeOriginal?.value?.[0] || null; let takenAtIso = parseExifDateUtc(timeRaw); // Fallback per i video if (isVideo) { const info = await probeVideo(absPath); // Cerca la data nei punti standard const creationFormat = info.format?.tags?.creation_time || null; const creationStream0 = info.streams?.[0]?.tags?.creation_time || null; const creationStream1 = info.streams?.[1]?.tags?.creation_time || null; // Scegli la prima disponibile const videoDate = creationFormat || creationStream0 || creationStream1 || null; if (videoDate) { // NON convertire, NON usare new Date() takenAtIso = videoDate; timeRaw = videoDate; } else { // Fallback finale: mtime del file const fallback = new Date(st.mtimeMs).toISOString(); takenAtIso = fallback; timeRaw = fallback; } } // --- GPS --- let gps = null; if (isVideo) { // i video usano exiftool gps = await extractGpsWithExiftool(absPath); } else { // le foto usano exifreader gps = extractGpsFromExif(tags); } // L'altitudine EXIF viene mantenuta. // Se assente, sarà recuperata dal geocode worker. // --- DIMENSIONI & ROTAZIONE --- let width = null, height = null, duration = null, duration_ms = null, rotation = 0; if (isVideo) { const info = await probeVideo(absPath); const stream = info.streams?.find((s) => s.width && s.height); if (stream) { width = stream.width; height = stream.height; rotation = 0; if (stream?.tags?.rotate) { rotation = Number(stream.tags.rotate); } const sdl = stream?.side_data_list; if (sdl && Array.isArray(sdl)) { const rotEntry = sdl.find((d) => d.rotation !== undefined); if (rotEntry) rotation = Number(rotEntry.rotation); const matrixEntry = sdl.find( (d) => typeof d.displaymatrix === "string", ); if (matrixEntry) { const match = matrixEntry.displaymatrix.match( /rotation of ([\-0-9]+) degrees/i, ); if (match) rotation = Number(match[1]); } } rotation = ((rotation % 360) + 360) % 360; } duration = info.format?.duration || null; duration_ms = duration ? Math.round(duration * 1000) : null; } else { try { const meta = await sharp(absPath).metadata(); width = meta.width || null; height = meta.height || null; } catch {} try { const raw = tags?.exif?.Orientation?.value ?? tags?.image?.Orientation?.value ?? tags?.ifd0?.Orientation?.value ?? null; const val = Array.isArray(raw) ? raw[0] : raw; const map = { 1: 0, 3: 180, 6: 90, 8: 270 }; rotation = map[val] ?? 0; } catch {} } const mime_type = inferMimeFromExt(ext); const id = sha256(`${userName}/${cartella}/${fileRelPath}`); // Reverse geocoding eseguito in modo asincrono. const location = null; // // --- GESTIONE PATH FULL / RELATIVI --- // const relPath = fileRelPath; const relThub1 = fileRelPath.replace(/\.[^.]+$/, "_min.jpg"); const relThub2 = fileRelPath.replace(/\.[^.]+$/, "_avg.jpg"); const relCast = isVideo ? "" : fileRelPath.replace(/\.[^.]+$/, "_cast.jpg"); const fullPath = PATH_FULL ? path.posix.join("/photos", userName, cartella, fileRelPath) : relPath; const fullThub1 = PATH_FULL ? path.posix.join("/photos", userName, "thumbs", cartella, relThub1) : relThub1; const fullThub2 = PATH_FULL ? path.posix.join("/photos", userName, "thumbs", cartella, relThub2) : relThub2; const fullCast = isVideo ? "" : PATH_FULL ? path.posix.join("/photos", userName, "thumbs", cartella, relCast) : relCast; return { id, user: userName, cartella, name: baseName + extName, // 👈 SEMPRE CORRETTO path: fullPath, thub1: fullThub1, thub2: fullThub2, cast: fullCast, gps, data: timeRaw, taken_at: takenAtIso, mime_type, width, height, rotation, size_bytes: st.size, mtimeMs: st.mtimeMs, duration_ms: isVideo ? duration_ms : null, location, }; } module.exports = processFile;