From a99cbf335415ca364dedba8fff61e9166a5d6363 Mon Sep 17 00:00:00 2001 From: ztimson Date: Sat, 12 Sep 2026 12:04:57 -0400 Subject: [PATCH] Save ais / adsb images to disk --- client/src/services/adsb.ts | 2 +- client/src/services/ais.ts | 2 +- client/src/services/sats.ts | 2 +- server/src/ais.mjs | 62 +++++++++++++++++++++++++++++-------- server/src/server.mjs | 6 ++-- 5 files changed, 54 insertions(+), 20 deletions(-) diff --git a/client/src/services/adsb.ts b/client/src/services/adsb.ts index c89c8c7..fa39e70 100644 --- a/client/src/services/adsb.ts +++ b/client/src/services/adsb.ts @@ -167,7 +167,7 @@ export class AirTrafficLayer { const makeProps = (p: any) => ({ plane: p, - position: mobile ? { x: window.innerWidth / 2 - 180, y: window.innerHeight - 540 - 16 } : { x: window.innerWidth - 360 - 16, y: 16 }, + position: mobile ? { x: window.innerWidth / 2 - 180, y: window.innerHeight - 540 - 16 } : { x: 16, y: 16 }, onClose: () => this._closePopup(icao), onBringToFront: () => bringToFront(container), }) diff --git a/client/src/services/ais.ts b/client/src/services/ais.ts index 69db28f..2d9d842 100644 --- a/client/src/services/ais.ts +++ b/client/src/services/ais.ts @@ -129,7 +129,7 @@ export class AISLayer { const makeProps = (b: any) => ({ boat: b, - position: mobile ? { x: window.innerWidth / 2 - 180, y: window.innerHeight - 540 - 16 } : { x: window.innerWidth - 360 - 16, y: 16 }, + position: mobile ? { x: window.innerWidth / 2 - 180, y: window.innerHeight - 540 - 16 } : { x: 16, y: 16 }, onClose: () => this._closePopup(mmsi), onBringToFront: () => bringToFront(container), }) diff --git a/client/src/services/sats.ts b/client/src/services/sats.ts index 430728e..312993b 100644 --- a/client/src/services/sats.ts +++ b/client/src/services/sats.ts @@ -194,7 +194,7 @@ export class SatsLayer { history: h_, range: r, eta: e, - position: mobile ? { x: window.innerWidth / 2 - 180, y: window.innerHeight - 540 - 16 } : { x: window.innerWidth - 360 - 16, y: 16 }, + position: mobile ? { x: window.innerWidth / 2 - 180, y: window.innerHeight - 540 - 16 } : { x: 16, y: 16 }, onClose: () => this._closePopup(sat.satellite), onBringToFront: () => bringToFront(container), }) diff --git a/server/src/ais.mjs b/server/src/ais.mjs index f08f149..4df669f 100644 --- a/server/src/ais.mjs +++ b/server/src/ais.mjs @@ -1,4 +1,7 @@ import {cfg} from './config.mjs'; +import * as fs from 'node:fs'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; const CHANNEL_MAP = {0: 'A, B', 1: 'A', 2: 'B'}; const SENDER_TYPE = { @@ -8,6 +11,10 @@ const SENDER_TYPE = { 12: 'AtoN', 13: 'SART/EPIRB' }; +const DIR = dirname(fileURLToPath(import.meta.url)); +const DATA = resolve(DIR, '../data'); +const IMAGES_DIR = resolve(DATA, 'ships'); + let aisCache = null; let aisCacheTs = 0; const AIS_TTL = 1000; @@ -89,18 +96,47 @@ export async function getAIS() { } export async function getAISImage(mmsi) { - const res = await fetch(`https://www.vesselfinder.com/vessels/details/${mmsi}`, { - headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36' } - }) - const html = await res.text() - const match = html.match(//) - if(!match) return null; - const srcMatch = match[0].match(/src="(.+?)"/) - if (!srcMatch) return null; + if(!mmsi) return null; + mmsi = String(mmsi); - let imgUrl = srcMatch[1] - if (imgUrl.startsWith('/')) imgUrl = `https://www.vesselfinder.com${imgUrl}` - return fetch(imgUrl, { - headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36' } - }).then(resp => resp.blob()) + const filePath = resolve(IMAGES_DIR, `${mmsi}.jpg`); + if (fs.existsSync(filePath)) return fs.readFileSync(filePath); + + 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)]; + } + + const html = await fetch(`https://www.vesselfinder.com/vessels/details/${mmsi}`, { + headers: {'Accept': '*/*', 'User-Agent': randomUA(), 'Referer': 'https://www.vesselfinder.com/'} + }).then(resp => { + if(resp.ok) return resp.text(); + throw new Error('Failed to fetch vessel page'); + }); + + const match = html.match(//); + if(!match) return null; + + const srcMatch = match[0].match(/src="(.+?)"/); + if(!srcMatch) return null; + + let imgUrl = srcMatch[1]; + if(imgUrl.startsWith('//')) imgUrl = `https:${imgUrl}`; + if(imgUrl.startsWith('/')) imgUrl = `https://www.vesselfinder.com${imgUrl}`; + + const resp = await fetch(imgUrl, { + headers: {'Accept': '*/*', 'User-Agent': randomUA(), 'Referer': 'https://www.vesselfinder.com/'} + }); + + if(!resp.ok) throw new Error('Failed to fetch image'); + + const buffer = Buffer.from(await resp.arrayBuffer()); + fs.mkdirSync(IMAGES_DIR, { recursive: true }); + fs.writeFileSync(filePath, buffer); + return buffer; } diff --git a/server/src/server.mjs b/server/src/server.mjs index d414ea5..ab4a6bf 100644 --- a/server/src/server.mjs +++ b/server/src/server.mjs @@ -163,10 +163,8 @@ app.get('/api/adsb/:icao', asyncHandler(async (req, res) => res.json(await getAD app.get('/api/ais', asyncHandler(async (req, res) => res.json(await getAIS()))); app.get('/api/ais/:mmsi/image', asyncHandler(async (req, res) => { - const blob = await getAISImage(req.params.mmsi); - 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 getAISImage(req.params.mmsi); + res.contentType('image/jpeg').send(buffer) })); app.get('/api/sats', (_req, res) => res.json(getTinyGSData()));