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 loc = require('../geo.js'); 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 }); const baseName = path.parse(fileRelPath).name; const absThumbMin = path.join(thumbBase, `${baseName}_min.jpg`); const absThumbAvg = path.join(thumbBase, `${baseName}_avg.jpg`); if (isVideo) { await createVideoThumbnail(absPath, absThumbMin, absThumbAvg); } else { await createThumbnails(absPath, absThumbMin, absThumbAvg); } // --- EXIF --- let tags = {}; try { tags = await ExifReader.load(absPath, { expanded: true }); } catch {} const timeRaw = tags?.exif?.DateTimeOriginal?.value?.[0] || null; const takenAtIso = parseExifDateUtc(timeRaw); // --- GPS --- let gps = isVideo ? await extractGpsWithExiftool(absPath) : extractGpsFromExif(tags); // --- DIMENSIONI & ROTAZIONE --- let width = null, height = null, duration = 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; // --- ROTAZIONE VIDEO (robusta, compatibile Xiaomi) --- rotation = 0; // 1) rotate standard if (stream?.tags?.rotate) { rotation = Number(stream.tags.rotate); } // 2) side_data_list rotation 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); } // 3) Xiaomi: displaymatrix string 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]); } } } // Normalizza (Xiaomi usa -90 → 270) rotation = ((rotation % 360) + 360) % 360; } duration = info.format?.duration || null; } else { // FOTO try { const meta = await sharp(absPath).metadata(); width = meta.width || null; height = meta.height || null; } catch {} // --- ROTAZIONE EXIF FOTO --- 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}`); const location = gps ? await loc(gps.lng, gps.lat) : null; // // --- GESTIONE PATH FULL / RELATIVI --- // const relPath = fileRelPath; const relThub1 = fileRelPath.replace(/\.[^.]+$/, '_min.jpg'); const relThub2 = fileRelPath.replace(/\.[^.]+$/, '_avg.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; return { id, user: userName, cartella, path: fullPath, thub1: fullThub1, thub2: fullThub2, gps, data: timeRaw, taken_at: takenAtIso, mime_type, width, height, rotation, size_bytes: st.size, mtimeMs: st.mtimeMs, duration: isVideo ? duration : null, location }; } module.exports = processFile;