]*>.*?]*>.*?${icao}.*?<\/span>.*?<\/tr>`, 's'));
+ if (!rowMatch) return null;
+ const cells = rowMatch.match(/| ]*>.*?<\/td>/g) || [];
+ return {
+ country: cells[1]?.replace(/<[^>]*>/g, '').trim() || null,
+ registration: cells[2]?.replace(/<[^>]*>/g, '').trim() || null,
+ aircraft: cells[3]?.replace(/<[^>]*>/g, '').trim() || null,
+ operator: cells[4]?.replace(/<[^>]*>/g, '').trim() || null,
+ serialNumber: cells[6]?.replace(/<[^>]*>/g, '').trim() || null,
+ };
+ } catch (e) {
+ return null;
+ }
+}
+
+async function fetchHexDb(icao) {
+ const resp = await fetchWithTimeout(`https://hexdb.io/api/v1/aircraft/${icao}`);
+ if (!resp.ok) return null;
+ const found = await resp.json();
+ return {
+ registration: found.Registration || null,
+ manufacturer: found.Manufacturer || null,
+ aircraft: found.ICAOTypeCode || null,
+ model: found.Type || null,
+ operator: found.RegisteredOwners || null,
+ };
+}
+
+function backfillFromSimilar(aircraft) {
+ if (!aircraft) return {};
+ const similar = db.prepare(`
+ SELECT manufacturer, model, engines, categoryDescription, class
+ FROM aircraft
+ WHERE aircraft = ?
+ AND (manufacturer IS NOT NULL OR model IS NOT NULL OR engines IS NOT NULL)
+ LIMIT 1
+ `).get(aircraft);
+ if (!similar) return {};
+ return {
+ manufacturer: similar.manufacturer || null,
+ model: similar.model || null,
+ engines: similar.engines || null,
+ categoryDescription: similar.categoryDescription || null,
+ class: similar.class || null,
+ };
+}
+
export async function enrichAircraft(a) {
if (!a.hex) return a;
- const row = db.prepare('SELECT * FROM aircraft WHERE icao24 = ?').get(a.hex.toUpperCase());
- if (!row) {
- const resp = await fetch(`https://hexdb.io/api/v1/aircraft/${a.hex.toUpperCase()}`);
- if (resp.ok) {
- const found = await resp.json();
- const insert = db.prepare(`
- INSERT OR IGNORE INTO aircraft (icao24, registration, manufacturer, aircraft, model, operator)
- VALUES (@icao24, @registration, @manufacturer, @aircraft, @model, @operator)
- `);
- const mapped = {
- icao24: a.hex.toUpperCase(),
- registration: found.Registration || null,
- manufacturer: found.Manufacturer || null,
- aircraft: found.ICAOTypeCode || null,
- model: found.Type || null,
- operator: found.RegisteredOwners || null,
- };
- insert.run(mapped);
- return { ...a, ...mapped, type: classifyAircraft(mapped) };
- }
- return { ...a, type: isIcaoInMilitaryRange(a.hex) ? 'military' : 'unknown' };
+ const icao = a.hex.toUpperCase();
+
+ // 1. Check own DB first
+ const row = db.prepare('SELECT * FROM aircraft WHERE icao24 = ?').get(icao);
+ if (row?.manufacturer && row?.model) {
+ return { ...a, ...row, type: classifyAircraft(row) };
}
- return { ...a, ...row, type: classifyAircraft(row) };
+
+ // 2. Race the two external sources
+ let found = await Promise.any([
+ fetchHexDb(icao),
+ scrapeHexDatabase(icao),
+ ]).catch(() => null);
+
+ // Merge with any partial DB row we already have
+ const merged = { ...row, ...found };
+
+ // 3. Backfill manufacturer/model/engines from similar aircraft type in DB
+ if (merged.aircraft && (!merged.manufacturer || !merged.model)) {
+ const similar = backfillFromSimilar(merged.aircraft);
+ Object.assign(merged, { ...similar, ...merged }); // don't overwrite fetched data
+ }
+
+ // 4. Save back to DB
+ if (found) {
+ db.prepare(`
+ INSERT INTO aircraft (icao24, registration, manufacturer, aircraft, model, operator, country, serialNumber, engines, categoryDescription, class)
+ VALUES (@icao24, @registration, @manufacturer, @aircraft, @model, @operator, @country, @serialNumber, @engines, @categoryDescription, @class)
+ ON CONFLICT(icao24) DO UPDATE SET
+ registration = COALESCE(excluded.registration, registration),
+ manufacturer = COALESCE(excluded.manufacturer, manufacturer),
+ aircraft = COALESCE(excluded.aircraft, aircraft),
+ model = COALESCE(excluded.model, model),
+ operator = COALESCE(excluded.operator, operator),
+ country = COALESCE(excluded.country, country),
+ serialNumber = COALESCE(excluded.serialNumber, serialNumber),
+ engines = COALESCE(excluded.engines, engines),
+ categoryDescription = COALESCE(excluded.categoryDescription, categoryDescription),
+ class = COALESCE(excluded.class, class)
+ `).run({
+ icao24: icao,
+ registration: merged.registration || null,
+ manufacturer: merged.manufacturer || null,
+ aircraft: merged.aircraft || null,
+ model: merged.model || null,
+ operator: merged.operator || null,
+ country: merged.country || null,
+ serialNumber: merged.serialNumber || null,
+ engines: merged.engines || null,
+ categoryDescription: merged.categoryDescription || null,
+ class: merged.class || null,
+ });
+ }
+
+ const result = { icao24: icao, ...merged };
+ return { ...a, ...result, type: classifyAircraft(result) };
}
export async function getADSB() {
|