diff --git a/server/src/adsb.mjs b/server/src/adsb.mjs index 0986944..84bd6d4 100644 --- a/server/src/adsb.mjs +++ b/server/src/adsb.mjs @@ -1,12 +1,12 @@ import {cfg} from './config.mjs'; import Database from 'better-sqlite3'; import {createWriteStream, existsSync, mkdirSync} from 'fs'; +import {promises as fsp} from 'fs'; import {resolve, dirname} from 'path'; import {fileURLToPath} from 'url'; import {pipeline} from 'stream/promises'; import {createGunzip} from 'zlib'; import AdmZip from 'adm-zip'; -import * as fs from 'node:fs'; const DIR = dirname(fileURLToPath(import.meta.url)); const DATA = resolve(DIR, '../data'); @@ -14,69 +14,15 @@ const DB_PATH = resolve(DATA, 'aircraft.db'); const DB_ZIP = resolve(DATA, 'aircraft_db.zip'); const TY_GZ = resolve(DATA, 'aircraft_types.gz'); const SHAPES_PATH = resolve(DATA, 'shapes.json'); -const history = new Map(); // icao -> [{lat, lon, alt, ts}] -const MAX_HISTORY = 500; const DB_URL = 'https://www.mictronics.de/aircraft-database/aircraft_db.php'; const TY_URL = 'https://www.mictronics.de/aircraft-database/aircraft_types.php'; +const history = new Map(); +const MAX_HISTORY = 500; + let db; -async function downloadFile(url, dest) { - const res = await fetch(url); - if(!res.ok) throw new Error(`Download failed ${url}: ${res.status}`); - await pipeline(res.body, createWriteStream(dest)); -} - -async function downloadTypes() { - await downloadFile(TY_URL, TY_GZ); - const gunzip = createGunzip(); - const chunks = []; - await new Promise((res, rej) => { - const gz = fs.createReadStream(TY_GZ); - gz.pipe(gunzip); - gunzip.on('data', c => chunks.push(c)); - gunzip.on('end', res); - gunzip.on('error', rej); - }); - return JSON.parse(Buffer.concat(chunks).toString('utf8')); -} - -async function importTypes(types) { - const insert = db.prepare(`INSERT OR IGNORE INTO aircraft_types(type_code, desc, wtc, icon) VALUES (?,?,?,?)`); - const run = db.transaction(() => { - for(const [code, t] of Object.entries(types)) { - insert.run(code, t.desc || null, t.wtc || null, t.icon || null); - } - }); - run(); - console.log(`✈️ Types imported: ${Object.keys(types).length}`); -} - -async function importAircraftDb() { - await downloadFile(DB_URL, DB_ZIP); - const zip = new AdmZip(DB_ZIP); - const entries = zip.getEntries(); - - const insert = db.prepare(`INSERT OR IGNORE INTO aircraft(icao, reg, type_code) VALUES (?,?,?)`); - - let total = 0; - const run = db.transaction((rows) => { - for(const [icao, a] of Object.entries(rows)) { - insert.run(icao.toUpperCase(), a.r || null, a.t || null); - total++; - } - }); - - for(const entry of entries) { - if(!entry.entryName.endsWith('.json')) continue; - const rows = JSON.parse(entry.getData().toString('utf8')); - run(rows); - } - - console.log(`✈️ Aircraft imported: ${total}`); -} - function initSchema() { db.exec(` CREATE TABLE IF NOT EXISTS aircraft ( @@ -93,45 +39,94 @@ function initSchema() { `); } +async function downloadFile(url, dest) { + const res = await fetch(url); + if(!res.ok) throw new Error(`Download failed ${url}: ${res.status}`); + await pipeline(res.body, createWriteStream(dest)); +} + +async function downloadTypes() { + await downloadFile(TY_URL, TY_GZ); + const chunks = []; + await new Promise((res, rej) => { + const gz = fsp.open(TY_GZ).then(f => f.createReadStream()); + const gunzip = createGunzip(); + gz.then(stream => { + stream.pipe(gunzip); + gunzip.on('data', c => chunks.push(c)); + gunzip.on('end', res); + gunzip.on('error', rej); + }); + }); + return JSON.parse(Buffer.concat(chunks).toString('utf8')); +} + +async function importTypes(types) { + const insert = db.prepare(`INSERT OR REPLACE INTO aircraft_types(type_code, desc, wtc, icon) VALUES (?,?,?,?)`); + db.transaction(() => { + for(const [code, t] of Object.entries(types)) + insert.run(code, t.desc || null, t.wtc || null, t.icon || null); + })(); + console.log(`✈️ Types upserted: ${Object.keys(types).length}`); +} + +async function importAircraftDb() { + await downloadFile(DB_URL, DB_ZIP); + const zip = new AdmZip(DB_ZIP); + const entries = zip.getEntries(); + const insert = db.prepare(`INSERT OR REPLACE INTO aircraft(icao, reg, type_code) VALUES (?,?,?)`); + + let total = 0; + for(const entry of entries) { + if(!entry.entryName.endsWith('.json')) continue; + const rows = JSON.parse(entry.getData().toString('utf8')); + db.transaction(() => { + for(const [icao, a] of Object.entries(rows)) { + insert.run(icao.toUpperCase(), a.r || null, a.t || null); + total++; + } + })(); + } + console.log(`✈️ Aircraft upserted: ${total}`); +} + export async function initAircraftDb() { if(!existsSync(DATA)) mkdirSync(DATA, {recursive: true}); db = new Database(DB_PATH); initSchema(); - const count = db.prepare('SELECT COUNT(*) as n FROM aircraft').get(); - if(count.n > 0) { - console.log(`✈️ Aircraft DB already loaded (${count.n} rows) — skipping download`); - return; - } - - console.log('✈️ Downloading aircraft DB...'); - await Promise.all([importAircraftDb(), downloadTypes().then(t => importTypes(t))]); - await Promise.all([fs.promises.rm(DB_ZIP), fs.promises.rm(TY_GZ),]); + console.log('✈️ Syncing aircraft DB...'); + await Promise.all([ + importAircraftDb(), + downloadTypes().then(importTypes) + ]); + await Promise.all([fsp.rm(DB_ZIP), fsp.rm(TY_GZ)]); + console.log('✈️ Aircraft DB sync complete'); } const lookupAircraft = (icao) => db?.prepare(` SELECT a.icao, a.reg, a.type_code, t.desc, t.wtc, t.icon FROM aircraft a - LEFT JOIN aircraft_types t ON a.type_code = t.type_code + LEFT JOIN aircraft_types t ON a.type_code = t.type_code WHERE a.icao = ? - `).get(icao.toUpperCase()); + `).get(icao.toUpperCase()); export function enrichAircraft(a) { if(!a.hex) return a; const row = lookupAircraft(a.hex); return { ...a, - registration: row?.reg || a.r || null, - type_code: row?.type_code || a.t || null, - type_desc: row?.desc || null, - wtc: row?.wtc || null, - icon: row?.icon || null, + registration: row?.reg || a.r || null, + type_code: row?.type_code || a.t || null, + type_desc: row?.desc || null, + wtc: row?.wtc || null, + icon: row?.icon || null, }; } export async function getShapes() { - return JSON.parse(await fs.promises.readFile(SHAPES_PATH, 'utf8')); + return JSON.parse(await fsp.readFile(SHAPES_PATH, 'utf8')); } export async function getADSB() {