diff --git a/.gitignore b/.gitignore index b512c09..1f6b6bd 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -node_modules \ No newline at end of file +node_modules +*-metars.json \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..b93dd21 --- /dev/null +++ b/config.json @@ -0,0 +1,8 @@ +{ + "outputdir": "/var/www/weather", + "icaos": ["CYWG", "KJFK"], + "siteurl": "https://example.com", + "rss-baseurl": "https://example.com/weather", + "check-interval": 10, + "loglevel": 4 +} \ No newline at end of file diff --git a/index.js b/index.js index 503ac7c..edcba93 100644 --- a/index.js +++ b/index.js @@ -1,24 +1,71 @@ 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 feedOutput = process.env.METAR_FEED_OUTPUT; -const icao = process.env.METAR_ICAO; -const rssMetarOptions = { - title: icao + " METAR", - description: "METAR feed for " + icao + " Airport", - feed_url: process.env.METAR_FEED_URL, - site_url: process.env.METAR_SITE_URL, - generator: "https://git.entropic.pro/Aiden/metar-rss" +const generatorURL = "https://git.entropic.pro/Aiden/metar-rss"; + +/** 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"); } -const checkInterval = 10 * 1000 * 60; //10 Minute Check Interval + +/** Verify config data **/ +if (Number(configData["loglevel"]) == NaN) { + console.warn("loglevel is not a number, using 4"); + configData["loglevel"] = 4; +} +const loglevel = Number(configData["loglevel"]); + +try { + if (!fs.lstatSync(configData["outputdir"]).isDirectory()) { + if (loglevel >= 4) console.warn("outputdir is not a directory, using current folder"); + configData["outputdir"] = __dirname; + } +} catch (err) { + if (loglevel >= 3) console.error(err); + if (loglevel >= 4) console.warn("error finding outputdir, using current folder"); + configData["outputdir"] = __dirname; +} +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"] = ["KJFK"]; +} +const icaos = configData["icaos"]; + +try { + new URL(configData["siteurl"]); +} catch (err) { + if (loglevel >= 4) console.warn("siteurl is not a valid URL, using example.com"); + configData["siteurl"] = "https://example.com" +} +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 example.com"); + configData["rss-baseurl"] = "https://example.com" +} +const rssBaseURL = new URL(configData["rss-baseurl"]); + +if (Number(configData["check-interval"]) == NaN) { + if (loglevel >= 4) console.warn("check-interval is NaN, using 10 minutes"); + configData["check-interval"] = 10; +} +const checkInterval = Number(configData["check-interval"]) * 1000 * 60; -let lastMetar; -let metarRSS = new RSS(rssMetarOptions); - +/** Start Service **/ checkMetar(); //Inital check run const metarCheckJob = setInterval(checkMetar, checkInterval); //Run the check every interval @@ -26,53 +73,109 @@ const metarCheckJob = setInterval(checkMetar, checkInterval); //Run the check ev * Pulls the current metar and adds it to the feed if it is new */ function checkMetar() { - console.log("Checking METAR for " + icao); - getMetar().then(res => { - if (res.data != lastMetar) { - console.log("New METAR: " + res.data); - newMetar(metarRSS, res.data); - writeFeed(metarRSS); - lastMetar = res.data; - } - }); + 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 = METAR_SERVICE + "/" + icao; + writeFeed(generateFeed(METAR_SERVICE, icao, items, outputPath), outputPath); + } + }); + } } /** * Adds a new METAR to the given RSS Feed - * @param {*} rss - RSS Feed to add the entry to - * @param {*} metar - metar data to add + * @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(rss, metar) { +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)))); - rss.item({ + 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() { +function getMetar(icao) { let requestURL = API_URL + METAR_SERVICE + "?ids=" + icao; return axios.get(requestURL); } /** - * Writes the given RSS feed to the specified file - * @param {*} rss - RSS Feed to Write + * 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 writeFeed(rss) { +function generateFeed(service, icao, items, outputPath) { + let feedURL = rssBaseURL; + feedURL.pathname = feedURL.pathname.concat(outputPath); + + 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(feedOutput, xmlFeed); + fs.writeFileSync(fullPathFile, xmlFeed); } catch(err) { console.error(err); }