import fs from 'fs'; import path from 'path'; import sharp from 'sharp'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const UPLOAD_DIR = process.env.PICUPPER_UPLOAD_DIR || path.join(__dirname, 'uploads'); async function processDirectory(dir) { const entries = fs.readdirSync(dir, { withFileTypes: true }); // Check if we are in a leaf "day" directory (has images) const images = entries.filter(e => e.isFile() && /\.(jpg|jpeg|png|webp)$/i.test(e.name) && !e.name.includes('_thumb')); if (images.length > 0) { console.log(`Processing directory: ${dir}`); const statsFile = path.join(dir, 'stats.json'); let stats = []; if (fs.existsSync(statsFile)) { try { stats = JSON.parse(fs.readFileSync(statsFile, 'utf8')); } catch (e) { console.error(`Error reading ${statsFile}`, e.message); } } // Create a set of existing filenames for fast lookup const processedFiles = new Set(stats.map(s => s.filename)); let updated = false; for (const img of images) { if (processedFiles.has(img.name)) continue; const imgPath = path.join(dir, img.name); console.log(` Computing brightness for ${img.name}...`); try { const image = sharp(imgPath); const s = await image.stats(); const brightness = Math.round((s.channels[0].mean + s.channels[1].mean + s.channels[2].mean) / 3); // Try to extract timestamp from filename: {cameraId}_{timestamp}.{ext} // Timestamp in filename is usually ISO like 2024-12-19T14-00-00-000Z but with - instead of : // We need a valid ISO string for consistency. // Filename format: {cameraId}_YYYY-MM-DDTHH-mm-ss-sssZ.{ext} // extract YYYY-MM-DDTHH-mm-ss-sssZ let timestamp = new Date().toISOString(); const match = img.name.match(/_(\d{4}-\d{2}-\d{2}T[\d-]+Z)\./); if (match) { // Replace - with : in time part for standard ISO parseability if needed, // or just store as is if that's what we did in server.js. // In server.js: new Date().toISOString().replace(/[:.]/g, '-') was used for filename. // For the stats entry, server.js uses new Date().toISOString() (standard ISO with colons). // Reconstruct standard ISO // stored format: 2025-12-19T14-25-00-123Z // target: 2025-12-19T14:25:00.123Z const rawTs = match[1]; const parts = rawTs.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z/); if (parts) { timestamp = `${parts[1]}-${parts[2]}-${parts[3]}T${parts[4]}:${parts[5]}:${parts[6]}.${parts[7]}Z`; } else { // Fallback for older formats if any timestamp = rawTs; } } else { // Fallback to file mtime const fstats = fs.statSync(imgPath); timestamp = fstats.mtime.toISOString(); } stats.push({ timestamp: timestamp, filename: img.name, brightness: brightness }); updated = true; } catch (err) { console.error(` Failed to process ${img.name}:`, err.message); } } if (updated) { // Sort by timestamp stats.sort((a, b) => a.timestamp.localeCompare(b.timestamp)); fs.writeFileSync(statsFile, JSON.stringify(stats, null, 2)); console.log(` Updated ${statsFile} with new entries.`); } else { console.log(` No new images to process.`); } } // Recurse into subdirectories for (const entry of entries) { if (entry.isDirectory()) { await processDirectory(path.join(dir, entry.name)); } } } // Start console.log(`Starting brightness backfill in ${UPLOAD_DIR}...`); processDirectory(UPLOAD_DIR).then(() => { console.log('Done!'); }).catch(err => { console.error('Fatal error:', err); });