181 lines
5.2 KiB
JavaScript
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);
|
|
});
|