diff --git a/server/src/adsb.mjs b/server/src/adsb.mjs index 92ca645..644a568 100644 --- a/server/src/adsb.mjs +++ b/server/src/adsb.mjs @@ -327,3 +327,38 @@ export async function getADSBRange() { const j = await r.json(); return j?.actualRange?.last24h?.points || []; } + +export async function getADSBImage(icao) { + 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] + } + + 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()); + return data?.results?.[0]?.image || null; + } + + + let aircraft = (await adsbCache).find(a => a.icao?.toLowerCase() === icao?.toLowerCase()); + if(!aircraft) aircraft = enrichAircraft({hex: icao}); + if(!aircraft) return null; + + const generic = duckduckgo(aircraft.model ? `${aircraft.manufacturer} ${aircraft.model}` : `${aircraft.aircraft} Aircraft`); + const specific = await fetch(`https://api.planespotters.net/pub/photos/hex/${icao.toLowerCase()}`).then(resp => resp.ok ? resp.json() : null); + + let src; + if(specific?.photos?.[0]) src = specific?.photos?.[0]; + else src = await generic; + + if(!src) return null; + return fetch(src).then(resp => resp.blob()); +} diff --git a/server/src/ais.mjs b/server/src/ais.mjs index 9b14786..726211e 100644 --- a/server/src/ais.mjs +++ b/server/src/ais.mjs @@ -87,3 +87,8 @@ export async function getAIS() { aisCacheTs = Date.now(); return aisCache; } + +export function getAISImage(mmsi) { + return fetch(`https://www.marinetraffic.com/getAssetDefaultPhoto/?photo_size=800&asset_type_id=0&asset_id=${mmsi}`) + .then(resp => resp.ok ? resp.blob() : null); +} diff --git a/server/src/server.mjs b/server/src/server.mjs index b1895a2..aa5f945 100644 --- a/server/src/server.mjs +++ b/server/src/server.mjs @@ -7,8 +7,8 @@ import {getCelestialCurrent, getCelestialForecast} from './celestial.mjs'; import {apiReference} from '@scalar/express-api-reference'; import {spec} from './spec.mjs'; import {existsSync} from 'fs'; -import {getAIS} from './ais.mjs'; -import {getADSB, getADSBHistory, getADSBRange, initAircraftDb} from './adsb.mjs'; +import {getAIS, getAISImage} from './ais.mjs'; +import {getADSBImage, getADSB, getADSBHistory, getADSBRange, initAircraftDb} from './adsb.mjs'; import {fetchIcon} from './openweather.mjs'; import {getForecast, getWeatherCondition, forecastTTL} from './forecast.mjs'; import {dailyWeather, hourlyWeather} from './openmeteo.mjs'; @@ -155,8 +155,18 @@ app.get('/api/icon/:icon', asyncHandler(async (req, res, next) => { // ── ADSB Proxy ──────────────────────────────────────────────────────────────── app.get('/api/adsb', asyncHandler(async (req, res) => res.json(await getADSB()))); +app.get('/api/adsb-image/:icao', asyncHandler(async (req, res) => { + const blob = await getADSBImage(req.params.icao); + const buffer = Buffer.from(await blob.arrayBuffer()); + res.contentType(blob.type).send(buffer); +})); app.get('/api/adsb/:icao', asyncHandler(async (req, res) => res.json(await getADSBHistory(req.params.icao)))); app.get('/api/ais', asyncHandler(async (req, res) => res.json(await getAIS()))); +app.get('/api/ais-image/:mmsi', asyncHandler(async (req, res) => { + const blob = await getAISImage(req.params.mmsi); + const buffer = Buffer.from(await blob.arrayBuffer()); + res.contentType(blob.type).send(buffer); +})); app.get('/api/range', asyncHandler(async (req, res) => res.json(await getADSBRange()))); // ── DOCS ──────────────────────────────────────────────────────────────────────