diff --git a/server/src/adsb.mjs b/server/src/adsb.mjs index 8e37feb..3c30938 100644 --- a/server/src/adsb.mjs +++ b/server/src/adsb.mjs @@ -9,6 +9,7 @@ import * as cheerio from 'cheerio'; const DIR = dirname(fileURLToPath(import.meta.url)); const DATA = resolve(DIR, '../data'); +const IMAGES_DIR = resolve(DATA, 'aircraft'); const DB_PATH = resolve(DATA, 'aircraft.db'); const CSV_CACHE = resolve(DATA, 'aircraft_db.csv'); const MAX_HISTORY = 500; @@ -329,50 +330,85 @@ export async function getADSBRange() { } export async function getADSBImage(icao) { + function randomUA() { + const v = 36 + Math.floor(Math.random() * 40); + const builds = [ + `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.${v} (KHTML, like Gecko) Chrome/1${10 + ~~(Math.random() * 16)}.0.0.0 Safari/537.${v}`, + `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.${v} (KHTML, like Gecko) Chrome/1${10 + ~~(Math.random() * 16)}.0.0.0 Safari/537.${v}`, + `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.${v} (KHTML, like Gecko) Chrome/1${10 + ~~(Math.random() * 16)}.0.0.0 Safari/537.${v}`, + ]; + return builds[~~(Math.random() * builds.length)]; + } + async function duckduckgo(query) { async function getVqd(query) { const res = await fetch(`https://duckduckgo.com/?q=${encodeURIComponent(query)}`, { - headers: { 'User-Agent': 'Mozilla/5.0' } - }) - const html = await res.text() - const match = html.match(/vqd=['"]?([\d-]+)['"]?/) - return match?.[1] + headers: { 'User-Agent': randomUA() } + }); + const html = await res.text(); + const match = html.match(/vqd=['"]?([\d-]+)['"]?/); + return match?.[1]; } - const vqd = await getVqd(query) - if (!vqd) throw new Error('Could not get vqd token') + const vqd = await getVqd(query); + if (!vqd) throw new Error('Could not get vqd token'); - const url = `https://duckduckgo.com/i.js?q=${encodeURIComponent(query)}&o=json&vqd=${vqd}&f=,,,,,&p=1` - const data = await fetch(url, - {headers: {'User-Agent': 'Mozilla/5.0', 'Referer': 'https://duckduckgo.com/'}} - ).then(resp => resp.json()); - console.log(query, data.results) + const url = `https://duckduckgo.com/i.js?q=${encodeURIComponent(query)}&o=json&vqd=${vqd}&f=,,,,,&p=1`; + const data = await fetch(url, { + headers: { 'User-Agent': randomUA(), 'Referer': 'https://duckduckgo.com/' } + }).then(resp => resp.json()); - // Sequentially try each image until we find a valid one if (data?.results?.length) { for (const result of data.results) { if (!result.image) continue; try { const imgRes = await fetch(result.image); - if (imgRes.ok) return result.image; - } catch (e) { - continue; // Try next image - } + if (imgRes.ok) return imgRes.blob(); + } catch { } } } return null; } - const aircraft = await this.get(icao); - const generic = duckduckgo(`${aircraft.type || aircraft.shape} Aircraft`); - const specific = await fetch(`https://api.planespotters.net/pub/photos/hex/${icao.toLowerCase()}`, - {headers: {'User-Agent': `open-sight.net (zaktimson@gmail.com)`}} - ).then(resp => resp.ok ? resp.json() : null); + async function genericImage(modelType) { + if (!modelType) return null; + const filePath = resolve(IMAGES_DIR, 'generic', `${modelType}.jpg`); + if (fs.existsSync(filePath)) return fs.readFileSync(filePath); - let src; - if(specific?.photos?.[0]) src = specific.photos[0].thumbnail_large?.src || specific.photos[0].thumbnail?.src; - else src = await generic; + const blob = await duckduckgo(`${modelType} Aircraft`); + if (!blob) return null; - if(!src) return null; - return fetch(src).then(resp => resp.blob()); + fs.mkdirSync(dirname(filePath), { recursive: true }); + const buffer = Buffer.from(await blob.arrayBuffer()); + fs.writeFileSync(filePath, buffer); + return buffer; + } + + async function icaoImage(icao) { + const filePath = resolve(IMAGES_DIR, `${icao}.jpg`); + if (fs.existsSync(filePath)) return fs.readFileSync(filePath); + + const planeSpotters = await fetch(`https://api.planespotters.net/pub/photos/hex/${icao}`, { + headers: { 'User-Agent': 'open-sight.net (zaktimson@gmail.com)' } + }).then(resp => resp.ok ? resp.json() : null).catch(() => null); + + const src = planeSpotters?.photos?.[0]?.thumbnail_large?.src || planeSpotters?.photos?.[0]?.thumbnail?.src; + if (!src) return null; + + const resp = await fetch(src, { headers: { 'Accept': '*/*', 'User-Agent': randomUA() } }); + if (!resp.ok) return null; + + fs.mkdirSync(IMAGES_DIR, { recursive: true }); + const buffer = Buffer.from(await resp.arrayBuffer()); + fs.writeFileSync(filePath, buffer); + return buffer; + } + + icao = icao.toLowerCase(); + const aircraft = await enrichAircraft({ hex: icao }); + const modelType = aircraft.aircraft || aircraft.model || aircraft.type; + + const generic = genericImage(modelType); + const specific = await icaoImage(icao); + return specific || await generic; } diff --git a/server/src/server.mjs b/server/src/server.mjs index 30eb7ab..d414ea5 100644 --- a/server/src/server.mjs +++ b/server/src/server.mjs @@ -156,10 +156,8 @@ app.get('/api/space', async (req, res) => { app.get('/api/adsb', asyncHandler(async (req, res) => res.json(await getADSB()))); app.get('/api/adsb/:icao/image', asyncHandler(async (req, res) => { - const blob = await getADSBImage(req.params.icao); - if (!blob) return res.status(404).send('No image found'); - const buffer = Buffer.from(await blob.arrayBuffer()); - res.contentType(blob.type).send(buffer); + const buffer = await getADSBImage(req.params.icao); + res.contentType('image/jpeg').send(buffer) })); app.get('/api/adsb/:icao', asyncHandler(async (req, res) => res.json(await getADSBHistory(req.params.icao))));