dev (#160)
Some checks failed
continuous-integration/drone/push Build is failing

Co-authored-by: sora-ext
Co-authored-by: soraefir
Reviewed-on: #160
This commit is contained in:
2025-03-02 01:09:29 +01:00
parent 977751d517
commit aee79aac75
67 changed files with 6060 additions and 5661 deletions

90
src/server/api.ts Normal file
View File

@@ -0,0 +1,90 @@
//import { ProxyAgent, setGlobalDispatcher } from 'undici';
import { flight_get_data } from './api_flight'
import { nominatim_get_data } from './api_nominatim';
//setGlobalDispatcher(new ProxyAgent(process.env.HTTPS_PROXY as string));
export default function (server, opts, done) {
server.get("/flight/:id", async (req, reply) =>
flight_get_data(req.params.id)
.then(res => {
let wait_for_all: Promise<any>[] = []
res.forEach(r => {
wait_for_all.push(nominatim_get_data(r.from).then(geo => (r as any).from_geo = geo[0]));
wait_for_all.push(nominatim_get_data(r.to).then(geo => (r as any).to_geo = geo[0]));
});
return Promise.all(wait_for_all).then(_ => res)
})
.then(res => reply.send(res))
);
server.get("/place/:id", async (req, reply) =>
nominatim_get_data(req.params.id, JSON.parse(req.query.bb))
.then(res => reply.send(res))
);
server.get("/gpx/:id", async (req, reply) => {
if (req.params.id == undefined)
return reply.code(400).send({ error: "No ID query parameter" });
server.level.db.get(req.params.id, (err, val) => {
if (err) {
console.warn(err);
reply.code(500).send();
} else {
let file = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><gpx xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.topografix.com/GPX/1/1" version="1.1" creator="EMTAC BTGPS Trine II DataLog Dump 1.0 - http://www.ayeltd.biz" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">'
const data = JSON.parse(val);
const gen_wpt = (name, desc, latlon, icon = "Flag") => `<wpt lat="${latlon[0]}" lon="${latlon[1]}"><ele>0</ele><name>${name}</name><cmt>-</cmt><desc>${desc}</desc><sym>${icon}</sym></wpt>`
const esc_str = (str) => (str || "Undefined").replace('"', "&quot;").replace("'", "&apos;").replace("<", "&lt;").replace(">", "&gt;").replace("&", "&amp;").replace("\n", "...")
data.main.forEach(a => {
file += gen_wpt(esc_str(a.hotel.name), esc_str(a.hotel.notes), a.hotel.latlon, "Hotel");
a.places.restaurants.forEach(b => {
file += gen_wpt(esc_str(b.name), esc_str(b.notes), b.latlon, "Restaurant");
});
a.places.activities.forEach(b => {
file += gen_wpt(esc_str(b.name), esc_str(b.notes), b.latlon, "Tree");
});
});
file += "</gpx>";
reply.header('Content-Type', 'application/gpx+xml');
reply.header('Content-Disposition', `attachment; filename=${req.params.id}.gpx`);
reply.send(file);
}
});
return reply;
});
server.get("/:id", async (req, reply) => {
if (req.params.id == undefined)
return reply.code(400).send({ error: "No ID query parameter" });
server.level.db.get(req.params.id, (err, val) => {
if (err) {
console.warn(err);
reply.code(500).send();
} else {
reply.send(JSON.parse(val));
}
});
return reply;
});
server.post("/:id", async (req, reply) => {
if (req.params.id == undefined)
return reply.code(400).send({ error: "No ID query parameter" });
server.level.db.put(req.params.id, req.body, (err) => {
if (err) {
console.warn(err);
reply.code(500).send({ error: "Error with DB" });
} else {
reply.send({ content: "ok" });
}
});
return reply;
});
done();
};

86
src/server/api_flight.ts Normal file
View File

@@ -0,0 +1,86 @@
import { JSDOM } from 'jsdom';
interface FlightData {
id: string;
date: string;
from: string;
to: string;
std: string;
atd: string | string[] | null;
sta: string;
ata: string | string[] | null;
}
function clean_times(s: string): string | null {
if (s == "—" || s == "Scheduled") return null
if (s.indexOf("Estimated departure") == 0) return null
if (s.indexOf("Landed") == 0) return s.replace("Landed ", "")
return s
}
export function flight_get_data(flightId: string): Promise<FlightData[]> {
const url = new URL(`https://www.flightradar24.com/data/flights/${flightId}`);
return fetch(url).then(res => res.text()).then(res => new JSDOM(res)).then(dom => {
const rows = dom.window.document.querySelectorAll('table tbody tr');
const flightData: FlightData[] = [];
rows.forEach(row => {
const columns = row.querySelectorAll('td');
/*
* 2/6 = Date/Duration
* 3/4 = From/To
* 7/8 = STD/ATD
* 9/11 = STA/ATA
*/
const flight: FlightData = {
id: flightId,
date: columns[2].textContent.trim(),
from: columns[3].textContent.trim(),
to: columns[4].textContent.trim(),
std: columns[7].textContent.trim(),
atd: clean_times(columns[8].textContent.trim()),
sta: columns[9].textContent.trim(),
ata: clean_times(columns[11].textContent.trim())
};
flightData.push(flight);
});
return groupByPair(flightData).map(v => groupByFrequency(v));
})
}
function groupByPair(flightData: FlightData[]): FlightData[][] {
const flightMap: { [key: string]: FlightData[] } = {};
flightData.forEach(flight => {
const key = `${flight.from}-${flight.to}-${flight.std}`;
if (!flightMap[key])
flightMap[key] = [];
flightMap[key].push(flight);
});
return Object.values(flightMap);
}
function groupByFrequency(flightData: FlightData[]): FlightData {
let data = 'OOOOOOO'; // Initialize with no flights
const atdArray: string[] = [];
const ataArray: string[] = [];
flightData.forEach(flight => {
const dayOfWeek = (new Date(flight.date).getDay() + 6) % 7;
data = data.substring(0, dayOfWeek) + 'X' + data.substring(dayOfWeek + 1);
if (flight.atd)
atdArray.push(flight.atd as string);
if (flight.ata)
ataArray.push(flight.ata as string);
});
return {
id: flightData[0].id,
date: data,
from: flightData[0].from,
to: flightData[0].to,
std: flightData[0].std,
sta: flightData[0].sta,
atd: atdArray,
ata: ataArray
};
};

View File

@@ -0,0 +1,27 @@
function drop_fields(results) {
results.forEach(e => {
delete e.licence;
delete e.place_rank;
delete e.importance;
delete e.boundingbox;
});
return results
}
export function nominatim_get_data(id: string, bb: string[][] | null = null): Promise<any> {
if (!id) return Promise.resolve([])
const url = new URL("https://nominatim.openstreetmap.org/search");
url.searchParams.append('format', 'jsonv2')
url.searchParams.append('q', id)
url.searchParams.append('limit', '20')
if (bb) {
url.searchParams.append('viewbox', `${bb[0][0]},${bb[0][1]},${bb[1][0]},${bb[1][1]}`)
url.searchParams.append('bounded', `1`)
}
return fetch(url).then((res) => {
if (!res.ok) throw new Error("Nominatim Error")
return res.json().then(r => drop_fields(r))
})
}

36
src/server/main.ts Normal file
View File

@@ -0,0 +1,36 @@
import fastify from 'fastify'
import fastify_static from '@fastify/static'
import fastify_db from '@fastify/leveldb'
import fastify_view from '@fastify/view';
import pug from 'pug'
import { join as pathJoin } from "path";
import api from "./api"
const server = fastify(); //{ logger: true });
server.register(fastify_static, {
root: pathJoin(__dirname, "../public"),
prefix: "/public/",
});
server.register(
fastify_db as any,
{ name: "db" }
);
server.register(fastify_view, {
engine: { pug: pug },
});
server.register(api, { prefix: "/api" });
server.get("/", (req, reply) => reply.view("/src/template/home.pug"));
server.get("/:id", (req, reply) => reply.view("/src/template/journey.pug"));
server.get("/view/:id", (req, reply) => reply.view("/src/template/view.pug"));
server.get("/short/:id", (req, reply) => reply.view("/src/template/short.pug"));
server.listen({ port: 8080, host: "0.0.0.0" }, (err, address) => {
if (err) throw err;
console.log("Listening on", address);
});