kennzeichen/backend/prisma/update-direction.js
2025-10-23 12:11:41 +02:00

181 lines
5.2 KiB
JavaScript

#!/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);
});