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 = { 0: 'Other', 1: 'Unknown', 2: 'Cargo', 3: 'Class B', 4: 'Passenger', 5: 'Special', 6: 'Tanker', 7: 'High Speed', 8: 'Fishing', 9: 'Aircraft', 10: 'Helicopter', 11: 'Base Station', 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; function decodeMsgTypes(raw) { const types = []; for (let i = 0; i < 32; i++) { if (raw & (1 << i)) types.push(i); } return types; } function decodeFlags(flags) { return { validated: flags & 3, validated2: (flags & 3) == 2 ? -1 : flags & 3, repeat: (flags >> 2) & 3, virtual_aid: (flags >> 4) & 1, approx: (flags >> 5) & 1, channels: (flags >> 6) & 15, cs_unit: (flags >> 10) & 3, raim: (flags >> 12) & 3, dte: (flags >> 14) & 3, assigned: (flags >> 16) & 3, display: (flags >> 18) & 3, dsc: (flags >> 20) & 3, band: (flags >> 22) & 3, msg22: (flags >> 24) & 3, off_position: (flags >> 26) & 3, maneuver: (flags >> 28) & 3, }; } export async function getAIS() { if (aisCache && Date.now() - aisCacheTs < AIS_TTL) return aisCache; const {ADSB_URL} = cfg(); if (!ADSB_URL) return {data: []}; const r = await fetch(`${ADSB_URL}:9990/api/ships_array.json`); const {dynamic, static: staticRows, time} = await r.json(); const staticMap = new Map(staticRows.map(row => [row[0], row])); aisCache = dynamic.map(row => { const [mmsi, lat, lon, distance, bearing, heading, cog, speed, status, rrsi, ppm, count, msg_type, last_signal, last_group, group_mask, flags, altitude, received_stations, mmsi_type, shipclass, country] = row; const s = staticMap.get(mmsi) ?? []; const [_mmsi, shipname, callsign, destination, shiptype, imo, to_bow, to_stern, to_port, to_starboard, draught, eta_month, eta_day, eta_hour, eta_minute] = s; const f = decodeFlags(flags); return { mmsi, type: SENDER_TYPE[shipclass], lat, lon, altitude, heading, cog, speed, distance, bearing, destination, rrsi, ppm, count, msg_type: decodeMsgTypes(msg_type), channels: CHANNEL_MAP[f.channels] ?? f.channels, ...f, shiptype, imo, draught, country, callsign, shipname, to_bow, to_stern, to_port, to_starboard, eta_month, eta_day, eta_hour, eta_minute, last_signal: last_signal ? time - last_signal : null, last_group, group_mask, received_stations, shipclass, status }; }); aisCacheTs = Date.now(); return aisCache; } export async function getAISImage(mmsi) { if(!mmsi) return null; mmsi = String(mmsi); 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; }