wave3.1: ADS-B OSINT layers (emergency squawks, rare-type hunter, shadow, radius focus)
All FREE via adsb.lol. New generic pieces:
- serve.py proxy/adsb/<path>: regex-allowlisted (SSRF-safe) adsb.lol v2 proxy
with per-path 120s cache + upstream call-spacing (adsb.lol 429s on bursts;
reservation slots space call *starts* without serializing network time),
12s fail-fast timeout, stale-on-error
- js/layers/adsb-layer.js: generic {ac:[...]} billboard factory (parallel
allSettled fetch, live/scrub/visibility gating, optional client-side filter)
- js/adsb-registry.js: emergency (sqk 7700/7600/7500, default-on, alerts red),
rare-type hunter (filters the shared mil feed by ICAO type — 1 cached call,
no per-type rate-limit), shadow (LADD+PIA, purple/pink)
- js/layers/radius.js: camera-driven 'all traffic within 250nm of view'
- verified live: all four render, worst-case simultaneous-enable ok, 0 errors
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
eb3d3cca96
commit
60ceb4a261
69
js/adsb-registry.js
Normal file
69
js/adsb-registry.js
Normal file
@ -0,0 +1,69 @@
|
||||
// ADS-B OSINT layer specs (Wave 3.1), rendered by createAdsbLayer. All feeds are
|
||||
// FREE via adsb.lol (proxy/adsb/<path>) — no key, works forever. Emergency is on
|
||||
// by default (usually empty = cheap, dramatic when not); the hunters are off.
|
||||
|
||||
// ICAO type designator → readable name (rare-type hunter).
|
||||
const TYPE_NAMES = {
|
||||
U2: 'U-2 Dragon Lady (spyplane)',
|
||||
GHWK: 'RQ-4 Global Hawk (drone)',
|
||||
E3TF: 'E-3 Sentry (AWACS)',
|
||||
E3CF: 'E-3 Sentry (AWACS)',
|
||||
E6: 'E-6 Mercury (TACAMO)',
|
||||
E4: 'E-4B Nightwatch ("doomsday")',
|
||||
RC135: 'RC-135 Rivet Joint (recon)',
|
||||
B52: 'B-52 Stratofortress',
|
||||
P8: 'P-8 Poseidon (sub-hunter)',
|
||||
};
|
||||
const HUNT_TYPES = Object.keys(TYPE_NAMES);
|
||||
|
||||
const SQUAWK_MEANING = { 7500: 'HIJACK', 7600: 'RADIO FAIL', 7700: 'EMERGENCY' };
|
||||
const SQUAWK_COLOR = { 7500: '#ff2d95', 7600: '#ff9f40', 7700: '#ff453a' };
|
||||
|
||||
const ftRow = (id) => [
|
||||
['Type', id.t || '—'],
|
||||
['Reg', id.reg || '—'],
|
||||
['Altitude', `${Math.round(id.altFt || 0)} ft`],
|
||||
['Speed', `${Math.round(id.gs || 0)} kt`],
|
||||
];
|
||||
|
||||
export const ADSB_LAYERS = [
|
||||
{
|
||||
id: 'emergency', name: 'Emergency squawks', category: 'Air', defaultOn: true,
|
||||
pollMs: 60000, alertOnHits: true,
|
||||
endpoints: ['sqk/7700', 'sqk/7600', 'sqk/7500'],
|
||||
color: (a) => SQUAWK_COLOR[a.squawk] || '#ff453a',
|
||||
scale: 1.15,
|
||||
pickTitle: (id) => `⚠ ${SQUAWK_MEANING[id.squawk] || 'SQUAWK'} ${id.flight || id.hex}`,
|
||||
describe: (id) => [['Status', SQUAWK_MEANING[id.squawk] || id.squawk || '—'], ...ftRow(id)],
|
||||
status: (n) => `${n} squawking emergency ⚠`,
|
||||
emptyStatus: () => 'clear — no emergencies',
|
||||
},
|
||||
{
|
||||
id: 'rare-types', name: 'Rare-type hunter', category: 'Air', defaultOn: false,
|
||||
pollMs: 120000,
|
||||
// These are all military types, so filter the shared /v2/mil feed (one cached
|
||||
// call the military layer already warms) instead of 8 rate-limited /type calls.
|
||||
endpoints: ['mil'],
|
||||
filter: (a) => HUNT_TYPES.includes(a.t),
|
||||
color: () => '#35e0ff',
|
||||
scale: 0.95,
|
||||
pickTitle: (id) => `✈ ${TYPE_NAMES[id.t] || id.t || 'aircraft'}`,
|
||||
describe: (id) => [['Aircraft', TYPE_NAMES[id.t] || id.t || '—'], ['Callsign', id.flight || '—'],
|
||||
['Reg', id.reg || '—'], ['Altitude', `${Math.round(id.altFt || 0)} ft`], ['Speed', `${Math.round(id.gs || 0)} kt`]],
|
||||
status: (n) => `${n} rare-type aircraft up`,
|
||||
emptyStatus: () => 'none of the tracked types airborne',
|
||||
},
|
||||
{
|
||||
id: 'shadow', name: 'Blocked / shadow aircraft', category: 'Air', defaultOn: false,
|
||||
pollMs: 120000,
|
||||
endpoints: ['ladd', 'pia'],
|
||||
color: (a) => (a._src === 'pia' ? '#ff7bd5' : '#b57bff'),
|
||||
scale: 0.8,
|
||||
pickTitle: (id) => `👤 ${id.reg || id.flight || id.hex}`,
|
||||
describe: (id) => [
|
||||
['Program', id.src === 'pia' ? 'PIA (privacy address)' : 'LADD (FAA limited-display)'],
|
||||
...ftRow(id),
|
||||
],
|
||||
status: (n) => `${n} blocked/shadow aircraft`,
|
||||
},
|
||||
];
|
||||
117
js/layers/adsb-layer.js
Normal file
117
js/layers/adsb-layer.js
Normal file
@ -0,0 +1,117 @@
|
||||
// Generic adsb.lol query-layer factory (free OSINT feeds). Powers the emergency
|
||||
// squawk, rare-type hunter, and shadow-aircraft layers — each is a spec that
|
||||
// names one or more adsb.lol v2 endpoints (proxy/adsb/<path>), merged by hex and
|
||||
// drawn as a BillboardCollection primitive with a click overlay. Live-only
|
||||
// (paused when scrubbed off-now or the tab is hidden), like the aircraft layer.
|
||||
//
|
||||
// spec: { id, name, category, defaultOn, pollMs,
|
||||
// endpoints: ['sqk/7700', 'type/U2', ...], // fetched sequentially (anti-burst)
|
||||
// color(a) -> hex, scale(a)|number, label(a) -> string|null,
|
||||
// describe(a) -> [[k,v],...], status(n) -> string, emptyStatus? -> string }
|
||||
|
||||
export function createAdsbLayer(ctx, spec) {
|
||||
const { viewer, Cesium, lib, ui } = ctx;
|
||||
const bc = viewer.scene.primitives.add(new Cesium.BillboardCollection());
|
||||
const glyph = lib.aircraftGlyph();
|
||||
const pollMs = spec.pollMs || 90000;
|
||||
|
||||
let enabled = spec.defaultOn !== false;
|
||||
let isLive = true;
|
||||
let inFlight = false;
|
||||
let timer = null;
|
||||
let lastOk = true;
|
||||
|
||||
bc.show = enabled;
|
||||
ui.addLayer(spec.id, spec.name, enabled, (on) => {
|
||||
enabled = on;
|
||||
bc.show = on && isLive;
|
||||
if (on && isLive) kick();
|
||||
}, spec.category);
|
||||
ui.setStatus(spec.id, 'loading…', 'warn');
|
||||
|
||||
function canPoll() { return enabled && isLive && !document.hidden; }
|
||||
function schedule(ms) { if (timer) clearTimeout(timer); timer = setTimeout(tick, ms); }
|
||||
function kick() { if (!canPoll() || timer || inFlight) return; schedule(0); }
|
||||
|
||||
async function tick() {
|
||||
timer = null;
|
||||
if (!canPoll() || inFlight) return;
|
||||
inFlight = true;
|
||||
try { await poll(); } finally { inFlight = false; }
|
||||
// On a total failure (e.g. adsb.lol rate-limited the whole batch), retry
|
||||
// soon instead of waiting the full interval.
|
||||
if (canPoll()) schedule(lastOk ? pollMs : 9000);
|
||||
}
|
||||
|
||||
const altFeet = (a) => (typeof a.alt_baro === 'number' ? a.alt_baro : 0);
|
||||
|
||||
async function poll() {
|
||||
// Fetch endpoints in PARALLEL and merge by hex. The proxy already spaces the
|
||||
// upstream calls (rate-limit politeness), so parallel is safe here — and it
|
||||
// means one slow/hung endpoint (adsb.lol can be flaky) never stalls the rest.
|
||||
const byHex = new Map();
|
||||
let anyOk = false;
|
||||
const results = await Promise.allSettled(
|
||||
spec.endpoints.map((ep) => fetch(`proxy/adsb/${ep}`).then((r) => {
|
||||
if (!r.ok) throw new Error(String(r.status));
|
||||
return r.json().then((d) => ({ ep, d }));
|
||||
}))
|
||||
);
|
||||
for (const r of results) {
|
||||
if (r.status !== 'fulfilled') continue;
|
||||
anyOk = true;
|
||||
const { ep, d } = r.value;
|
||||
for (const a of (Array.isArray(d.ac) ? d.ac : [])) {
|
||||
if (a.lat == null || a.lon == null || byHex.has(a.hex)) continue;
|
||||
a._src = ep;
|
||||
byHex.set(a.hex, a);
|
||||
}
|
||||
}
|
||||
lastOk = anyOk;
|
||||
if (!anyOk) { ui.setStatus(spec.id, 'busy — retrying shortly', 'warn'); return; }
|
||||
|
||||
// Optional client-side filter (e.g. rare-types filters the shared mil feed
|
||||
// by ICAO type — one cached call, no per-type rate-limit risk).
|
||||
let list = [...byHex.values()];
|
||||
if (spec.filter) list = list.filter(spec.filter);
|
||||
|
||||
bc.removeAll();
|
||||
for (const a of list) {
|
||||
bc.add({
|
||||
position: Cesium.Cartesian3.fromDegrees(a.lon, a.lat, altFeet(a) * 0.3048),
|
||||
image: glyph,
|
||||
color: lib.cz(spec.color(a)),
|
||||
rotation: lib.headingToRotation(a.track),
|
||||
scale: typeof spec.scale === 'function' ? spec.scale(a) : (spec.scale || 0.8),
|
||||
id: {
|
||||
layer: spec.id, hex: a.hex, flight: (a.flight || '').trim(),
|
||||
t: a.t, reg: a.r, altFt: altFeet(a), gs: a.gs, track: a.track,
|
||||
squawk: a.squawk, src: a._src,
|
||||
},
|
||||
});
|
||||
}
|
||||
const n = list.length;
|
||||
const txt = n === 0 && spec.emptyStatus ? spec.emptyStatus() : spec.status(n);
|
||||
ui.setStatus(spec.id, txt, n > 0 && spec.alertOnHits ? 'err' : 'ok');
|
||||
}
|
||||
|
||||
function onClockTick(currentTime, live) {
|
||||
isLive = live;
|
||||
if (!isLive) { bc.show = false; return; }
|
||||
bc.show = enabled;
|
||||
if (enabled) kick();
|
||||
}
|
||||
|
||||
function handlePick(picked) {
|
||||
if (picked?.id?.layer !== spec.id) return false;
|
||||
ui.showPickOverlay(spec.pickTitle ? spec.pickTitle(picked.id) : (picked.id.flight || picked.id.hex || '—'),
|
||||
spec.describe(picked.id));
|
||||
return true;
|
||||
}
|
||||
function clearPick() { ui.hidePickOverlay(); }
|
||||
|
||||
document.addEventListener('visibilitychange', () => { if (!document.hidden) kick(); });
|
||||
kick();
|
||||
|
||||
return { id: spec.id, onClockTick, handlePick, clearPick };
|
||||
}
|
||||
106
js/layers/radius.js
Normal file
106
js/layers/radius.js
Normal file
@ -0,0 +1,106 @@
|
||||
// Radius focus (Wave 3.1) — all traffic within 250 nm of where you're looking,
|
||||
// via adsb.lol's free lat/lon/dist query (proxy/adsb/lat/.../lon/.../dist/250).
|
||||
// Camera-driven: refetches when you stop panning/zooming, plus a slow refresh
|
||||
// for freshness. Most useful zoomed into a region. Live-only; off by default.
|
||||
|
||||
export default function create(ctx) {
|
||||
const { viewer, Cesium, lib, ui } = ctx;
|
||||
const bc = viewer.scene.primitives.add(new Cesium.BillboardCollection());
|
||||
const glyph = lib.aircraftGlyph();
|
||||
const DIST_NM = 250; // adsb.lol free radius cap
|
||||
|
||||
let enabled = false;
|
||||
let isLive = true;
|
||||
let inFlight = false;
|
||||
let debounce = null;
|
||||
let poller = null;
|
||||
let lastKey = null;
|
||||
|
||||
bc.show = false;
|
||||
ui.addLayer('radius', `Traffic near view (${DIST_NM}nm)`, false, (on) => {
|
||||
enabled = on;
|
||||
bc.show = on && isLive;
|
||||
if (on && isLive) { refresh(); ensurePoller(); }
|
||||
else { clearInterval(poller); poller = null; ui.setStatus('radius', on ? 'locating…' : '', on ? 'warn' : 'ok'); }
|
||||
});
|
||||
|
||||
viewer.camera.percentageChanged = 0.25; // fire camera.changed a bit more eagerly
|
||||
viewer.camera.changed.addEventListener(() => {
|
||||
if (!enabled || !isLive) return;
|
||||
if (debounce) clearTimeout(debounce);
|
||||
debounce = setTimeout(refresh, 1800); // refetch after panning stops
|
||||
});
|
||||
|
||||
function ensurePoller() {
|
||||
if (poller) return;
|
||||
poller = setInterval(() => { if (enabled && isLive && !document.hidden) refresh(); }, 60000);
|
||||
}
|
||||
|
||||
// Center = camera nadir point, rounded (also improves proxy cache hits).
|
||||
function center() {
|
||||
const c = viewer.camera.positionCartographic;
|
||||
return [
|
||||
+Cesium.Math.toDegrees(c.latitude).toFixed(2),
|
||||
+Cesium.Math.toDegrees(c.longitude).toFixed(2),
|
||||
];
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
if (!enabled || !isLive || document.hidden || inFlight) return;
|
||||
const [lat, lon] = center();
|
||||
if (!isFinite(lat) || !isFinite(lon)) return;
|
||||
const key = `${lat}/${lon}`;
|
||||
lastKey = key;
|
||||
inFlight = true;
|
||||
try {
|
||||
const res = await fetch(`proxy/adsb/lat/${lat}/lon/${lon}/dist/${DIST_NM}`);
|
||||
if (key !== lastKey) return; // camera moved again mid-fetch — stale
|
||||
if (!res.ok) { ui.setStatus('radius', `HTTP ${res.status}`, 'warn'); return; }
|
||||
const data = await res.json();
|
||||
const list = Array.isArray(data.ac) ? data.ac : [];
|
||||
bc.removeAll();
|
||||
let n = 0;
|
||||
for (const a of list) {
|
||||
if (a.lat == null || a.lon == null) continue;
|
||||
const altFt = typeof a.alt_baro === 'number' ? a.alt_baro : 0;
|
||||
bc.add({
|
||||
position: Cesium.Cartesian3.fromDegrees(a.lon, a.lat, altFt * 0.3048),
|
||||
image: glyph,
|
||||
color: lib.cz('#7bff9d'),
|
||||
rotation: lib.headingToRotation(a.track),
|
||||
scale: 0.7,
|
||||
id: { layer: 'radius', hex: a.hex, flight: (a.flight || '').trim(), t: a.t,
|
||||
reg: a.r, altFt, gs: a.gs, track: a.track },
|
||||
});
|
||||
n++;
|
||||
}
|
||||
ui.setStatus('radius', `${n} within ${DIST_NM}nm of view`, 'ok');
|
||||
} catch {
|
||||
ui.setStatus('radius', 'network error', 'warn');
|
||||
} finally {
|
||||
inFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onClockTick(currentTime, live) {
|
||||
isLive = live;
|
||||
if (!isLive) { bc.show = false; return; }
|
||||
bc.show = enabled;
|
||||
if (enabled) { refresh(); ensurePoller(); }
|
||||
}
|
||||
|
||||
function handlePick(picked) {
|
||||
if (picked?.id?.layer !== 'radius') return false;
|
||||
const id = picked.id;
|
||||
ui.showPickOverlay('✈ ' + (id.flight || id.hex || '—'), [
|
||||
['Type', id.t || '—'], ['Reg', id.reg || '—'],
|
||||
['Altitude', `${Math.round(id.altFt || 0)} ft`], ['Speed', `${Math.round(id.gs || 0)} kt`],
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
function clearPick() { ui.hidePickOverlay(); }
|
||||
|
||||
document.addEventListener('visibilitychange', () => { if (!document.hidden && enabled && isLive) refresh(); });
|
||||
|
||||
return { id: 'radius', onClockTick, handlePick, clearPick };
|
||||
}
|
||||
13
js/main.js
13
js/main.js
@ -20,6 +20,8 @@ import * as lib from './lib.js';
|
||||
import * as ui from './ui.js';
|
||||
import { REGISTRY } from './registry.js';
|
||||
import { createGeoJsonLayer } from './layers/geojson-layer.js';
|
||||
import { ADSB_LAYERS } from './adsb-registry.js';
|
||||
import { createAdsbLayer } from './layers/adsb-layer.js';
|
||||
|
||||
const Cesium = window.Cesium;
|
||||
|
||||
@ -178,6 +180,7 @@ const LAYER_MODULES = [
|
||||
'./layers/ships.js',
|
||||
'./layers/aircraft.js',
|
||||
'./layers/military.js',
|
||||
'./layers/radius.js',
|
||||
];
|
||||
const activeLayers = [];
|
||||
|
||||
@ -210,6 +213,16 @@ async function loadLayers() {
|
||||
console.error(`[godsigh] registry layer ${spec.id} threw during init:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
// ADS-B OSINT layers (Wave 3.1): emergency squawks, rare-type hunter, shadow.
|
||||
for (const spec of ADSB_LAYERS) {
|
||||
try {
|
||||
const layer = createAdsbLayer(ctx, spec);
|
||||
if (layer) activeLayers.push(layer);
|
||||
} catch (err) {
|
||||
console.error(`[godsigh] adsb layer ${spec.id} threw during init:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- live/scrub gating (throttled ~1/s) ----
|
||||
|
||||
1
js/ui.js
1
js/ui.js
@ -10,6 +10,7 @@ const COLLAPSED_BY_DEFAULT = new Set(['Regional — Australia']);
|
||||
const CATEGORY_BY_ID = {
|
||||
satellites: 'Space', 'sat-paths': 'Space', launches: 'Space',
|
||||
aircraft: 'Air', military: 'Air', 'aircraft-mil': 'Air',
|
||||
emergency: 'Air', 'rare-types': 'Air', shadow: 'Air', radius: 'Air',
|
||||
ships: 'Sea',
|
||||
quakes: 'Earth & hazards', fires: 'Earth & hazards', gdacs: 'Earth & hazards', nws: 'Earth & hazards',
|
||||
infra: 'Context', events: 'Context',
|
||||
|
||||
60
serve.py
60
serve.py
@ -13,6 +13,7 @@ import re
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
@ -60,6 +61,28 @@ ADSBLOL_MIL_URL = "https://api.adsb.lol/v2/mil"
|
||||
ADSBLOL_CACHE_SEC = 60
|
||||
_adsblol_cache = {"body": None, "ts": 0.0}
|
||||
|
||||
# ---- Generic adsb.lol query proxy (proxy/adsb/<v2-path>) ---------------------
|
||||
# Powers the OSINT layers (emergency squawks, rare-type hunter, LADD/PIA shadow
|
||||
# aircraft, radius focus). The regex is the security allowlist — only these v2
|
||||
# shapes are forwardable, so it can't be turned into an open proxy/SSRF. Each
|
||||
# distinct path is cached 60s, and upstream calls are spaced (adsb.lol 429s on
|
||||
# rapid bursts — the same guard the prod nginx regex-location relies on). The
|
||||
# prod nginx block mirrors this allowlist; keep the two in sync.
|
||||
ADSB_PATH_RE = re.compile(
|
||||
r'^(mil|ladd|pia'
|
||||
r'|sqk/\d{3,4}'
|
||||
r'|type/[A-Za-z0-9]{2,5}'
|
||||
r'|hex/[0-9a-fA-F]{6}'
|
||||
r'|callsign/[A-Za-z0-9]{1,8}'
|
||||
r'|reg/[A-Za-z0-9\-]{1,10}'
|
||||
r'|lat/-?\d{1,3}(\.\d+)?/lon/-?\d{1,3}(\.\d+)?/dist/\d{1,4}(\.\d+)?'
|
||||
r'|closest/-?\d{1,3}(\.\d+)?/-?\d{1,3}(\.\d+)?/\d{1,4}(\.\d+)?)$')
|
||||
ADSB_CACHE_SEC = 120
|
||||
ADSB_MIN_INTERVAL = 1.0 # min seconds between upstream adsb.lol calls (anti-burst; it 429s fast)
|
||||
_adsb_cache = {} # path -> (body, ts)
|
||||
_adsb_lock = threading.Lock()
|
||||
_adsb_next = [0.0] # earliest wall-time the next upstream call may start
|
||||
|
||||
# ---- ADS-B Exchange military feed (paid RapidAPI, ~10k req/month) ------------
|
||||
# proxy/adsbx-mil injects the RapidAPI key server-side (never in the browser) and
|
||||
# HARD-caches the /v2/mil/ response — the quota guard. The $10 BASIC tier bills
|
||||
@ -176,6 +199,8 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
name, _, query = self.path[len("/proxy/"):].partition("?")
|
||||
if name == "mil":
|
||||
return self._serve_mil()
|
||||
if name.startswith("adsb/"):
|
||||
return self._serve_adsb(name[len("adsb/"):])
|
||||
if name == "adsbx-mil":
|
||||
return self._serve_adsbx_mil()
|
||||
base = UPSTREAMS.get(name)
|
||||
@ -299,6 +324,41 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
return self._send(200, "application/json", _adsblol_cache["body"], {"X-Godsigh-Cache": "stale"})
|
||||
return self._send_json(502, {"error": str(e)})
|
||||
|
||||
def _serve_adsb(self, path):
|
||||
# Generic adsb.lol v2 query proxy, allowlisted by ADSB_PATH_RE.
|
||||
if not ADSB_PATH_RE.match(path):
|
||||
return self._send_json(400, {"error": "unsupported adsb path"})
|
||||
hit = _adsb_cache.get(path)
|
||||
if hit and time.time() - hit[1] < ADSB_CACHE_SEC:
|
||||
return self._send(200, "application/json", hit[0], {"X-Godsigh-Cache": "hit"})
|
||||
# Reserve a rate-limit slot so call STARTS are spaced ≥ADSB_MIN_INTERVAL,
|
||||
# but do NOT hold the lock during the fetch — otherwise a big response
|
||||
# (ladd, ~300 aircraft) would stall every other layer's poll.
|
||||
with _adsb_lock:
|
||||
hit = _adsb_cache.get(path) # re-check under lock
|
||||
if hit and time.time() - hit[1] < ADSB_CACHE_SEC:
|
||||
return self._send(200, "application/json", hit[0], {"X-Godsigh-Cache": "hit"})
|
||||
start_at = max(time.time(), _adsb_next[0])
|
||||
_adsb_next[0] = start_at + ADSB_MIN_INTERVAL
|
||||
wait = start_at - time.time()
|
||||
if wait > 0:
|
||||
time.sleep(wait)
|
||||
try:
|
||||
req = urllib.request.Request(f"https://api.adsb.lol/v2/{path}",
|
||||
headers={"User-Agent": "godsigh/1.0 (partly.party)"})
|
||||
with urllib.request.urlopen(req, timeout=12) as r: # fail fast; adsb.lol can hang
|
||||
body = r.read()
|
||||
_adsb_cache[path] = (body, time.time())
|
||||
return self._send(200, "application/json", body, {"X-Godsigh-Cache": "miss"})
|
||||
except urllib.error.HTTPError as e:
|
||||
if hit is not None:
|
||||
return self._send(200, "application/json", hit[0], {"X-Godsigh-Cache": "stale"})
|
||||
return self._send_json(e.code, {"error": f"adsb.lol upstream {e.code}"})
|
||||
except Exception as e:
|
||||
if hit is not None:
|
||||
return self._send(200, "application/json", hit[0], {"X-Godsigh-Cache": "stale"})
|
||||
return self._send_json(502, {"error": str(e)})
|
||||
|
||||
def _serve_adsbx_mil(self):
|
||||
host, key = load_adsbx_creds()
|
||||
if not host or not key:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user