import RSS from "rss"; import axios from "axios"; import fs from 'fs'; import path from 'path'; const API_URL = "https://aviationweather.gov/api/data/"; const METAR_SERVICE = "metar"; const generatorURL = "https://git.entropic.pro/Aiden/metar-rss"; let 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 default"); 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 **/ checkMetar(); //Inital check run const metarCheckJob = setInterval(checkMetar, checkInterval); //Run the check every interval /** * Pulls the current metar and adds it to the feed if it is new */ function checkMetar() { for (let i = 0; i < icaos.length; i++) { let icao = icaos[i]; if (loglevel >= 7) console.log("Checking METAR for " + icao); getMetar(icao).then(res => { let metarFile = icao+"-metars.json"; let metars = []; if (fs.existsSync(metarFile)) metars = JSON.parse(fs.readFileSync(icao+"-metars.json"))["data"]; if (res.data != metars[0]) { if (loglevel >= 6) console.log("New METAR: " + res.data); metars.unshift(res.data); if (metars.length > 20) metars.pop(); fs.writeFileSync(metarFile, JSON.stringify({"data": metars})); let items = []; for (let i = 0; i < metars.length; i++) { if (newMetar(metars[i]) != undefined) items.push(newMetar(metars[i])); } let outputPath = path.join(METAR_SERVICE, icao); writeFeed(generateFeed(METAR_SERVICE, icao, items, outputPath), outputPath); } }); } } /** * Adds a new METAR to the given RSS Feed * @param {RSS} rss - RSS Feed to add the entry to * @param {String} metar - metar data to add * @returns {Object} - Object of RSS Item Data */ function newMetar(metar) { let metarSplit = metar.split(" "); let date = new Date(Date.now()); let eventDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), Number(metarSplit[2].substring(0,2)), Number(metarSplit[2].substring(2,4)), Number(metarSplit[2].substring(4,6)))); let item = { title: metarSplit[0] + " " + metarSplit[1] + " " + metarSplit[2], description: metar, url: API_URL + METAR_SERVICE, guid: metarSplit[1] + metarSplit[2], date: eventDate }; return item } /** * Pulls METAR data from the API * @param {String} icao ICAO Airport Code * @returns Axios generated GET Request Response */ function getMetar(icao) { let requestURL = API_URL + METAR_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, description: service + " 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) { console.error(err); } }