Files
metar-rss/index.js

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);
}
}