244 lines
8.1 KiB
JavaScript
244 lines
8.1 KiB
JavaScript
import RSS from "rss";
|
|
import axios from "axios";
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
const generatorURL = "https://git.entropic.pro/Aiden/metar-rss";
|
|
const API_URL = "https://aviationweather.gov/api/data/";
|
|
const METAR_SERVICE = "metar";
|
|
const TAF_SERVICE = "taf";
|
|
const SERVICES = [METAR_SERVICE, TAF_SERVICE];
|
|
const SERVICE_FUNC = [newMetar, newTAF];
|
|
|
|
const DATA_DIR = "data";
|
|
if (!fs.existsSync(DATA_DIR)) {
|
|
fs.mkdirSync(DATA_DIR);
|
|
}
|
|
|
|
const defaultConfig = {
|
|
"outputdir": "/var/www/weather",
|
|
"icaos": ["KJFK"],
|
|
"siteurl": "https://example.com",
|
|
"rss-baseurl": "https://example.com/weather",
|
|
"check-interval": 10,
|
|
"loglevel": 4
|
|
};
|
|
/** Load config file */
|
|
const configFile = "config.json";
|
|
let configData;
|
|
try {
|
|
configData = JSON.parse(fs.readFileSync(configFile));
|
|
} catch (err) {
|
|
console.error(err);
|
|
console.warn("Unable to read config file, using defaults");
|
|
configData = defaultConfig;
|
|
}
|
|
|
|
/** Verify config data **/
|
|
if (Number(configData["loglevel"]) == NaN) {
|
|
console.warn("loglevel is not a number, using " + defaultConfig["loglevel"]);
|
|
configData["loglevel"] = defaultConfig["loglevel"];
|
|
}
|
|
const loglevel = Number(configData["loglevel"]);
|
|
|
|
try {
|
|
if (!fs.lstatSync(configData["outputdir"]).isDirectory()) {
|
|
if (loglevel >= 4) console.warn("outputdir is not a directory, using " + defaultConfig["outputdir"]);
|
|
configData["outputdir"] = defaultConfig["outputdir"];
|
|
}
|
|
} catch (err) {
|
|
if (loglevel >= 3) console.error(err);
|
|
if (loglevel >= 4) console.warn("error finding outputdir, using " + defaultConfig["outputdir"]);
|
|
configData["outputdir"] = defaultConfig["outputdir"];
|
|
}
|
|
const outputDir = configData["outputdir"];
|
|
|
|
if (!Array.isArray(configData["icaos"])) {
|
|
if (loglevel >= 4) console.warn("icao config must be an array of ICAO codes, using " + defaultConfig["icaos"]);
|
|
configData["icaos"] = defaultConfig["icaos"];
|
|
}
|
|
const icaos = configData["icaos"];
|
|
|
|
try {
|
|
new URL(configData["siteurl"]);
|
|
} catch (err) {
|
|
if (loglevel >= 4) console.warn("siteurl is not a valid URL, using " + defaultConfig["siteurl"]);
|
|
configData["siteurl"] = defaultConfig["siteurl"];
|
|
}
|
|
const siteURL = new URL(configData["siteurl"]);
|
|
|
|
try {
|
|
new URL(configData["rss-baseurl"]);
|
|
} catch (err) {
|
|
if (loglevel >= 4) console.warn("rss-baseurl is not a valid URL, using " + defaultConfig["rss-baseurl"]);
|
|
configData["rss-baseurl"] = defaultConfig["rss-baseurl"];
|
|
}
|
|
const rssBaseURL = new URL(configData["rss-baseurl"]);
|
|
|
|
if (Number(configData["check-interval"]) == NaN) {
|
|
if (loglevel >= 4) console.warn("check-interval is NaN, using " + defaultConfig["check-interval"] + " minutes.");
|
|
configData["check-interval"] = defaultConfig["check-interval"];
|
|
}
|
|
const checkInterval = Number(configData["check-interval"]) * 1000 * 60;
|
|
|
|
|
|
/** Start Service **/
|
|
checkAPI(); //Inital check run
|
|
const metarCheckJob = setInterval(checkAPI, checkInterval); //Run the check every interval
|
|
|
|
/**
|
|
* Pulls the current METAR and TAF and adds it to the feed if it is new
|
|
*/
|
|
function checkAPI() {
|
|
for (let i = 0; i < icaos.length; i++) {
|
|
let icao = icaos[i];
|
|
|
|
for (let s = 0; s < SERVICES.length; s++) {
|
|
let service = SERVICES[s];
|
|
if (loglevel >= 7) console.log("Checking " + service.toUpperCase() + " for " + icao);
|
|
|
|
getData(icao, service).then(res => {
|
|
let dataFile = path.join(DATA_DIR, icao+"-"+service+".json");
|
|
let data = new Array();
|
|
if (fs.existsSync(dataFile))
|
|
data = JSON.parse(fs.readFileSync(dataFile))[service];
|
|
|
|
//If the recived data is new
|
|
if (data === undefined || res.data != data[0]) {
|
|
if (loglevel >= 6) console.log("New " + service.toUpperCase() + " for " + icao);
|
|
|
|
if (data === undefined) data = [res.data];
|
|
else data.unshift(res.data);
|
|
|
|
// Keep a max of 20 entries
|
|
if (data.length > 20) data.pop();
|
|
fs.writeFileSync(dataFile, JSON.stringify({[service]: data}));
|
|
|
|
let items = [];
|
|
for (let j = 0; j < data.length; j++) {
|
|
if (data[j] != undefined || data[j] != null) {
|
|
items.push(SERVICE_FUNC[s](data[j]));
|
|
}
|
|
}
|
|
|
|
let outputPath = path.join(service, icao);
|
|
writeFeed(generateFeed(service, icao, items, outputPath), outputPath);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates METAR RSS Item
|
|
* @param {String} metar - metar text/data to add
|
|
* @returns {Object} - Object of RSS Item Data
|
|
*/
|
|
function newMetar(metar) {
|
|
let metarSplit = metar.split(" ");
|
|
let metarTime = metarSplit[2]; // time/date section of metar
|
|
|
|
let date = new Date(Date.now());
|
|
let eventDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), Number(metarTime.substring(0,2)), Number(metarTime.substring(2,4)), Number(metarTime.substring(4,6))));
|
|
|
|
let item = {
|
|
title: metarSplit[0] + " " + metarSplit[1] + " " + metarSplit[2],
|
|
description: metar,
|
|
url: API_URL + METAR_SERVICE + "?ids=" + metarSplit[1],
|
|
guid: metarSplit[1] + metarSplit[2],
|
|
date: eventDate
|
|
};
|
|
return item;
|
|
}
|
|
|
|
/**
|
|
* Generates TAF RSS Item
|
|
* @param {String} taf - taf text/data to add
|
|
* @returns {Object} - Object of RSS Item Data
|
|
*/
|
|
function newTAF(taf) {
|
|
let tafSplit = taf.split(" ");
|
|
let tafTime = tafSplit[((tafSplit[1] == "AMD") ? 3 : 2)]; // index of the time/date section is pushed if TAF is amended
|
|
|
|
let date = new Date(Date.now());
|
|
let eventDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(),Number(tafTime.substring(0,2)), Number(tafTime.substring(2,4)), Number(tafTime.substring(4,6))));
|
|
|
|
let title;
|
|
let id;
|
|
if (tafSplit[1] == "AMD") {
|
|
title = tafSplit[0] + " " + tafSplit[1] + " " + tafSplit[2] + " " + tafSplit[3];
|
|
id = tafSplit[1] + tafSplit[2] + tafSplit[3];
|
|
} else {
|
|
title = tafSplit[0] + " " + tafSplit[1] + " " + tafSplit[2];
|
|
id = tafSplit[1] + tafSplit[2];
|
|
}
|
|
|
|
let item = {
|
|
title: title,
|
|
description: taf.replace(/(?:\r\n|\r|\n)/g, '<br>'),
|
|
url: API_URL + TAF_SERVICE + "?ids=" + ((tafSplit[1] == "AMD") ? tafSplit[2] : tafSplit[1]),
|
|
guid: id,
|
|
date: eventDate
|
|
};
|
|
return item;
|
|
}
|
|
|
|
/**
|
|
* Pulls METAR data from the API
|
|
* @param {String} icao ICAO Airport Code
|
|
* @param {String} service service to fetch (eg. "metar" or "taf")
|
|
* @returns Axios generated GET Request Response
|
|
*/
|
|
function getData(icao, service) {
|
|
let requestURL = API_URL + service + "?ids=" + icao;
|
|
return axios.get(requestURL);
|
|
}
|
|
|
|
/**
|
|
* Generates and populates a feed for the given details
|
|
* @param {String} service eg. "metar"
|
|
* @param {String} icao ICAO Airpot Code
|
|
* @param {Array} items Array of Objects containing RSS item details
|
|
* @param {String} outputPath path for this Service + ICAO
|
|
* @returns {RSS} RSS Feed with populated items
|
|
*/
|
|
function generateFeed(service, icao, items, outputPath) {
|
|
let feedURL = new URL(rssBaseURL);
|
|
feedURL.pathname = path.join(feedURL.pathname, outputPath, "rss.xml");
|
|
|
|
let feedOptions = {
|
|
title: icao + " " + service.toUpperCase(),
|
|
description: service.toUpperCase() + " feed for " + icao + " Airport",
|
|
feed_url: feedURL,
|
|
site_url: siteURL,
|
|
generator: generatorURL
|
|
}
|
|
let feed = new RSS(feedOptions);
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
feed.item(items[i]);
|
|
}
|
|
|
|
return feed;
|
|
}
|
|
|
|
/**
|
|
* Writes the given RSS feed to the specified file
|
|
* @param {RSS} rss - RSS Feed to Write
|
|
* @param {String} outputPath path for this Service + ICAO
|
|
*/
|
|
function writeFeed(rss, outputPath) {
|
|
let xmlFeed = rss.xml({indent: true})
|
|
let fullPath = path.join(outputDir, outputPath);
|
|
let fullPathFile = path.join(fullPath, "rss.xml");
|
|
|
|
if (!fs.existsSync(fullPath)){
|
|
fs.mkdirSync(fullPath, { recursive: true });
|
|
}
|
|
|
|
try {
|
|
fs.writeFileSync(fullPathFile, xmlFeed);
|
|
} catch(err) {
|
|
if (loglevel >= 3)console.error(err);
|
|
}
|
|
} |