6 Commits
v1.0.1 ... main

Author SHA1 Message Date
7b67722ca4 Fixed blank entry detection logic 2026-04-16 10:20:13 -05:00
0c8f99de78 Fixed linebreaks not showing in Feed 2026-04-15 18:43:35 -05:00
35bb43452e Bump version 1.1.1 2026-04-15 18:43:21 -05:00
deaafa530a Updated README 2026-04-15 18:12:20 -05:00
af4243604b Added TAF support 2026-04-15 18:12:11 -05:00
11a1b56514 Bump version 1.1.0 2026-04-15 16:56:21 -05:00
5 changed files with 103 additions and 46 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
node_modules
*-metars.json
data/

View File

@@ -3,9 +3,10 @@ This node app generates rss XML files that you can then serve as an RSS feed.
## Install and Setup (Linux)
```sh
# Clone the repo
# Clone the repo and install modules
sudo mkdir -p /opt
cd /opt && sudo git clone https://git.entropic.pro/Aiden/metar-rss
npm i
# Edit config, see below for details
sudo nano /opt/metar-rss/config.json

129
index.js
View File

@@ -3,11 +3,19 @@ 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 generatorURL = "https://git.entropic.pro/Aiden/metar-rss";
const TAF_SERVICE = "taf";
const SERVICES = [METAR_SERVICE, TAF_SERVICE];
const SERVICE_FUNC = [newMetar, newTAF];
let defaultConfig = {
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",
@@ -16,7 +24,7 @@ let defaultConfig = {
"loglevel": 4
};
/** Load config file */
const configFile = "./config.json";
const configFile = "config.json";
let configData;
try {
configData = JSON.parse(fs.readFileSync(configFile));
@@ -75,69 +83,116 @@ const checkInterval = Number(configData["check-interval"]) * 1000 * 60;
/** Start Service **/
checkMetar(); //Inital check run
const metarCheckJob = setInterval(checkMetar, checkInterval); //Run the check every interval
checkAPI(); //Inital check run
const metarCheckJob = setInterval(checkAPI, checkInterval); //Run the check every interval
/**
* Pulls the current metar and adds it to the feed if it is new
* Pulls the current METAR and TAF and adds it to the feed if it is new
*/
function checkMetar() {
function checkAPI() {
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}));
for (let s = 0; s < SERVICES.length; s++) {
let service = SERVICES[s];
if (loglevel >= 7) console.log("Checking " + service.toUpperCase() + " for " + icao);
let items = [];
for (let i = 0; i < metars.length; i++) {
if (metars[i] != undefined)
items.push(newMetar(metars[i]));
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]));
} else {
data.splice(j);
}
}
let outputPath = path.join(service, icao);
writeFeed(generateFeed(service, icao, items, outputPath), outputPath);
}
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
* 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(metarSplit[2].substring(0,2)), Number(metarSplit[2].substring(2,4)), Number(metarSplit[2].substring(4,6))));
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,
url: API_URL + METAR_SERVICE + "?ids=" + metarSplit[1],
guid: metarSplit[1] + metarSplit[2],
date: eventDate
};
return item
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 getMetar(icao) {
let requestURL = API_URL + METAR_SERVICE + "?ids=" + icao;
function getData(icao, service) {
let requestURL = API_URL + service + "?ids=" + icao;
return axios.get(requestURL);
}
@@ -154,8 +209,8 @@ function generateFeed(service, icao, items, outputPath) {
feedURL.pathname = path.join(feedURL.pathname, outputPath, "rss.xml");
let feedOptions = {
title: icao + " " + service,
description: service + " feed for " + icao + " Airport",
title: icao + " " + service.toUpperCase(),
description: service.toUpperCase() + " feed for " + icao + " Airport",
feed_url: feedURL,
site_url: siteURL,
generator: generatorURL

10
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "metar-rss",
"version": "1.0.1",
"version": "1.1.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "metar-rss",
"version": "1.0.1",
"version": "1.1.1",
"license": "AGPL-3.0-only",
"dependencies": {
"axios": "^1.15.0",
@@ -124,9 +124,9 @@
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
"funding": [
{
"type": "individual",

View File

@@ -1,7 +1,7 @@
{
"name": "metar-rss",
"version": "1.0.1",
"description": "Monitors, generates and updates an RSS Feed File for Airport METARs",
"version": "1.1.1",
"description": "Monitors, generates and updates an RSS Feed File for Airport METARs and TAFs",
"repository": {
"type": "git",
"url": "https://git.entropic.pro/Aiden/metar-rss"