#!/usr/bin/env node /* Backfill direction & directionDegrees from *_Info.xml files into Recognition rows. Usage: node scripts/backfill-direction.js [PATH_TO_ROOT] */ const fs = require('fs'); const path = require('path'); const { PrismaClient } = require('@prisma/client'); const { XMLParser } = require('fast-xml-parser'); const prisma = new PrismaClient(); const parser = new XMLParser({ ignoreAttributes: false }); const ROOT = process.argv[2] || './data'; const CONCURRENCY = 8; // parallel verarbeitete Dateien // Hilfsfunktion: alle Dateien rekursiv einsammeln function walkFiles(dir, list = []) { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const e of entries) { const full = path.join(dir, e.name); if (e.isDirectory()) { walkFiles(full, list); } else if (e.isFile() && e.name.endsWith('_Info.xml')) { list.push(full); } } return list; } // XML -> Date aus {DateUTC, TimeUTC} / {DateLocal, TimeLocal} function toDate(dateObj, timeObj) { if (!dateObj || !timeObj) return null; const y = Number(dateObj.Year); const m = Number(dateObj.Month) - 1; // 0-based const d = Number(dateObj.Day); const hh = Number(timeObj.Hour); const mm = Number(timeObj.Min); const ss = Number(timeObj.Sec); const ms = Number(timeObj.Msec || 0); if ([y,m,d,hh,mm,ss].some(n => Number.isNaN(n))) return null; return new Date(y, m, d, hh, mm, ss, ms); } async function processXmlFile(file) { try { const xml = fs.readFileSync(file, 'utf8'); const json = parser.parse(xml); const data = json?.ReturnRecognitionData?.StandardMessage?.RecognitionData; if (!data) return { skipped: true, reason: 'no RecognitionData' }; const license = String(data.License || '').trim(); if (!license) return { skipped: true, reason: 'no license' }; const dirStr = data.Direction != null ? String(data.Direction).trim() : null; const degInt = data.DirectionDegrees != null ? parseInt(data.DirectionDegrees, 10) : null; // nichts zu backfillen? if ((!dirStr || dirStr.length === 0) && (!degInt || Number.isNaN(degInt))) { return { skipped: true, reason: 'no direction fields in XML' }; } // Zeiten const tsUTC = toDate(data.DateUTC, data.TimeUTC); const tsLocal = toDate(data.DateLocal, data.TimeLocal); if (!tsUTC && !tsLocal) { return { skipped: true, reason: 'no timestamps' }; } // Update-Bedingungen: // - entweder (license, timestampUTC) matcht // - oder (license, timestampLocal) matcht // - und direction/directionDegrees sind leer / null / 0 const where = { AND: [ { OR: [ ...(tsUTC ? [{ license, timestampUTC: tsUTC }] : []), ...(tsLocal ? [{ license, timestampLocal: tsLocal }] : []), ] }, { OR: [ { direction: null }, { direction: '' }, { directionDegrees: null }, { directionDegrees: 0 }, ] } ] }; // Nur setzen, wenn sie in XML sinnvolle Werte haben const dataUpdate = {}; if (dirStr && dirStr.length > 0) dataUpdate.direction = dirStr; if (Number.isInteger(degInt) && degInt >= 0) dataUpdate.directionDegrees = degInt; if (Object.keys(dataUpdate).length === 0) { return { skipped: true, reason: 'nothing to update' }; } const result = await prisma.recognition.updateMany({ where, data: dataUpdate, }); return { updated: result.count }; } catch (err) { return { error: err.message || String(err) }; } } // Einfache Parallelitätssteuerung async function run() { console.log(`🔎 Suche XMLs unter: ${path.resolve(ROOT)}`); const files = walkFiles(ROOT); console.log(`Gefundene *_Info.xml: ${files.length}`); let idx = 0; let active = 0; let done = 0; let updated = 0; let skipped = 0; let errors = 0; await new Promise((resolve) => { const pump = () => { while (active < CONCURRENCY && idx < files.length) { const file = files[idx++]; active++; processXmlFile(file) .then((res) => { done++; if (res?.updated) { updated += res.updated; console.log(`✅ ${file} → updated ${res.updated}`); } else if (res?.skipped) { skipped++; // optional leiser: // console.log(`⏭️ ${file} → skipped: ${res.reason}`); } else if (res?.error) { errors++; console.warn(`❌ ${file} → ${res.error}`); } else { // nichts } }) .catch((e) => { errors++; console.warn(`❌ ${file} → ${e.message || e}`); }) .finally(() => { active--; if (done === files.length) resolve(); else pump(); }); } }; pump(); }); console.log('—'.repeat(50)); console.log(`Fertig. Dateien: ${files.length}`); console.log(`✅ Updates: ${updated}`); console.log(`⏭️ Skipped: ${skipped}`); console.log(`❌ Errors : ${errors}`); await prisma.$disconnect(); } run().catch(async (e) => { console.error('Fatal:', e); await prisma.$disconnect(); process.exit(1); });