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