mirror of
https://github.com/KevinMidboe/miljobilen-rss.git
synced 2025-10-28 17:20:23 +00:00
Refactor, CLI arg, full type coverage w/ JSDoc, handle rollover year and most other errors
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1 +1,4 @@
|
||||
rss.xml
|
||||
|
||||
.DS_Store
|
||||
node_modules/
|
||||
|
||||
24
jsconfig.json
Normal file
24
jsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"strictBindCallApply": true,
|
||||
"noImplicitThis": true,
|
||||
"noImplicitReturns": true,
|
||||
"alwaysStrict": true,
|
||||
"esModuleInterop": true,
|
||||
"checkJs": true,
|
||||
"allowJs": true,
|
||||
"declaration": true,
|
||||
"target": "es2021",
|
||||
"module": "commonjs",
|
||||
"outDir": "dist",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*.js"
|
||||
],
|
||||
"verbose": true
|
||||
}
|
||||
9
package.json
Normal file
9
package.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"scripts": {
|
||||
"check-types": "tsc --project ./jsconfig.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.14.2",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
120
src/errors.js
Normal file
120
src/errors.js
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* @typedef {Object} CustomRegexType
|
||||
* @property {number} inputLength
|
||||
*
|
||||
* @typedef {RegExpMatchArray & CustomRegexType} CustomRegExpMatchArray
|
||||
*/
|
||||
|
||||
class WebsiteNotFoundError extends Error {
|
||||
/**
|
||||
* @param {Response} resp
|
||||
* @param {string} url
|
||||
*/
|
||||
constructor(resp, url) {
|
||||
const { status, statusText } = resp;
|
||||
|
||||
const message = `Unable to fetch data from website, responded with status code: ${status}.`;
|
||||
super(message)
|
||||
|
||||
this.status = status;
|
||||
this.statusText = statusText;
|
||||
this.url = url;
|
||||
}
|
||||
}
|
||||
|
||||
class WebsiteRedirectedURLError extends Error {
|
||||
/**
|
||||
* @param {Response} resp
|
||||
* @param {string} url
|
||||
*/
|
||||
constructor(resp, url) {
|
||||
const message = 'Server responded with redirect, check URL for errors.'
|
||||
super(message)
|
||||
|
||||
const { status, statusText, headers } = resp;
|
||||
this.status = status;
|
||||
this.statusText = statusText;
|
||||
this.location = headers.get('Location');
|
||||
this.url = url;
|
||||
}
|
||||
}
|
||||
|
||||
class WebsiteUnexpectedError extends Error {
|
||||
/**
|
||||
* @param {string} errorMessage
|
||||
* @param {string} url
|
||||
*/
|
||||
constructor(errorMessage, url) {
|
||||
const message = 'Unexpected error occured! Unable to fetch date from website.';
|
||||
super(message);
|
||||
|
||||
this.errorMessage = errorMessage;
|
||||
this.url = url;
|
||||
}
|
||||
}
|
||||
|
||||
class LocationNotFoundError extends Error {
|
||||
/**
|
||||
* @param {string} location
|
||||
* @param {RegExpMatchArray | null} matched
|
||||
*/
|
||||
constructor(location, matched) {
|
||||
const message = `Pick up location: '${location}' not found on folloren.no website.`;
|
||||
super(message)
|
||||
|
||||
if (matched?.input) {
|
||||
// matched.inputLength = matched.input?.length
|
||||
delete matched.input
|
||||
}
|
||||
|
||||
this.foundMatch = matched
|
||||
this.name = 'LocationNotFoundError';
|
||||
}
|
||||
}
|
||||
|
||||
class TimesNotFoundError extends Error {
|
||||
/**
|
||||
* @param {string} location
|
||||
* @param {RegExpMatchArray | null} matched
|
||||
*/
|
||||
constructor(location, matched) {
|
||||
const message = `Times for location: '${location}' not found on folloren.no website.`;
|
||||
super(message)
|
||||
|
||||
if (matched?.input) {
|
||||
// matched.inputLength = matched.input?.length
|
||||
delete matched.input
|
||||
}
|
||||
|
||||
this.foundMatch = matched
|
||||
this.name = 'TimesNotFoundError';
|
||||
}
|
||||
}
|
||||
|
||||
class DatesNotFoundError extends Error {
|
||||
/**
|
||||
* @param {string} location
|
||||
* @param {RegExpMatchArray | null} matched
|
||||
*/
|
||||
constructor(location, matched) {
|
||||
const message = `Dates for location '${location}' not found on folloren.no website.`;
|
||||
super(message)
|
||||
|
||||
if (matched?.input) {
|
||||
// matched.inputLength = matched.input?.length
|
||||
delete matched.input
|
||||
}
|
||||
|
||||
this.foundMatch = matched
|
||||
this.name = 'DatesNotFoundError';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
WebsiteNotFoundError,
|
||||
WebsiteRedirectedURLError,
|
||||
WebsiteUnexpectedError,
|
||||
LocationNotFoundError,
|
||||
TimesNotFoundError,
|
||||
DatesNotFoundError
|
||||
}
|
||||
100
src/rss.js
100
src/rss.js
@@ -1,11 +1,14 @@
|
||||
const fs = require('fs')
|
||||
const { uuidv4 } = require('./utils')
|
||||
const fs = require("fs");
|
||||
const crypto = require("crypto");
|
||||
const { CURRENT_DATE, timeToWebsiteDate } = require("./utils.js");
|
||||
|
||||
class RSS {
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
this.filename = 'rss.xml';
|
||||
this.filename = "rss.xml";
|
||||
this.feed = null;
|
||||
|
||||
// this.read()
|
||||
@@ -13,55 +16,78 @@ class RSS {
|
||||
|
||||
// reads RSS file
|
||||
read() {
|
||||
fs.readFile(this.filename, 'utf8', (err, content) => {
|
||||
fs.readFile(this.filename, "utf8", (err, content) => {
|
||||
if (err) {
|
||||
console.error("Unable to read file:", this.filename)
|
||||
console.error("Unable to read file:", this.filename);
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
this.feed = content;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// writes RSS file
|
||||
write(content=this.feed) {
|
||||
/**
|
||||
* writes RSS file
|
||||
* @param {string | null} content
|
||||
*/
|
||||
write(content = this.feed) {
|
||||
if (content == null) return;
|
||||
|
||||
fs.writeFile(this.filename, content, (err) => {
|
||||
if (err) {
|
||||
console.error(`Error writing to ${this.filename}:`, err);
|
||||
console.error(`Error writing to ${this.filename}:`, err);
|
||||
} else {
|
||||
console.log(`Successfully wrote to ${this.filename}`);
|
||||
console.log(`Successfully wrote to ${this.filename}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('./types.js').LocationTimes} times
|
||||
* @param {Date} date
|
||||
* @param {string} url
|
||||
* @param {number} n
|
||||
* @returns {string}
|
||||
*/
|
||||
itemTemplate(times, date, url, n) {
|
||||
const relativeDate = new Date().getTime() - (n * 100000000)
|
||||
const time = this.formatDate(new Date(relativeDate))
|
||||
const relativeDate = CURRENT_DATE.getTime() - n * 100000000;
|
||||
const time = this.formatDate(new Date(relativeDate));
|
||||
// const currentRSSDate = this.formatDate(new Date());
|
||||
|
||||
const dateString = timeToWebsiteDate(date);
|
||||
const description = `Vi minner om miljøbilen fra FolloRen besøker oss på ${this.name} kl ${times.from}-${times.to} den ${dateString}.`;
|
||||
const descriptionHash = crypto
|
||||
.createHash("md5")
|
||||
.update(description)
|
||||
.digest("hex");
|
||||
|
||||
return `
|
||||
<item>
|
||||
<title></title>
|
||||
<description>Vi minner om miljøbilen fra FolloRen besøker oss på ${this.name} kl ${times.from}-${times.to} den ${date}.</description>
|
||||
<description>${description}</description>
|
||||
<link>${url}</link>
|
||||
<guid isPermaLink="false">${uuidv4()}</guid>
|
||||
<guid isPermaLink="false">${descriptionHash}</guid>
|
||||
<pubDate>${time}</pubDate>
|
||||
</item>
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
getRSSItems() {
|
||||
|
||||
}
|
||||
getRSSItems() {}
|
||||
|
||||
/**
|
||||
* @param {import('./types.js').LocationTimes} times
|
||||
* @param {Array.<Date>} dates
|
||||
* @param {string} url
|
||||
*/
|
||||
generate(times, dates, url) {
|
||||
const description = "Viser hentetider for miljøbilen fra folloren.no";
|
||||
const currentRSSDate = this.formatDate(new Date())
|
||||
const currentRSSDate = this.formatDate(CURRENT_DATE);
|
||||
|
||||
const blocks = dates.reverse().map((date, n) => this.itemTemplate(times, date, url, n))
|
||||
const blocks = dates
|
||||
.reverse()
|
||||
.map((date, n) => this.itemTemplate(times, date, url, n));
|
||||
this.feed = `
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<rss version="2.0">
|
||||
@@ -69,33 +95,49 @@ class RSS {
|
||||
<title>Miljøbilen hentetider ${this.name}</title>
|
||||
<description>${description}</description>
|
||||
<link>https://github.com/kevinmidboe/miljobilen-rss</link>
|
||||
<copyright>2020 Example.com All rights reserved</copyright>
|
||||
<copyright>${CURRENT_DATE.getFullYear()} Kevin No rights reserved, please contact</copyright>
|
||||
<lastBuildDate>${currentRSSDate}</lastBuildDate>
|
||||
<pubDate>${currentRSSDate}</pubDate>
|
||||
<ttl>1800</ttl>
|
||||
|
||||
${blocks.join('')}
|
||||
${blocks.join("")}
|
||||
|
||||
</channel>
|
||||
</rss>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Date} date
|
||||
* @returns {string}
|
||||
*/
|
||||
formatDate(date) {
|
||||
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||
const months = [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
];
|
||||
|
||||
const day = days[date.getUTCDay()];
|
||||
const dayOfMonth = date.getUTCDate();
|
||||
const month = months[date.getUTCMonth()];
|
||||
const year = date.getUTCFullYear();
|
||||
const hours = String(date.getUTCHours()).padStart(2, '0');
|
||||
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getUTCSeconds()).padStart(2, '0');
|
||||
const hours = String(date.getUTCHours()).padStart(2, "0");
|
||||
const minutes = String(date.getUTCMinutes()).padStart(2, "0");
|
||||
const seconds = String(date.getUTCSeconds()).padStart(2, "0");
|
||||
|
||||
return `${day}, ${dayOfMonth} ${month} ${year} ${hours}:${minutes}:${seconds} +0000`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = RSS
|
||||
module.exports = RSS;
|
||||
|
||||
279
src/run.js
279
src/run.js
@@ -1,166 +1,193 @@
|
||||
const RSS = require('./rss.js');
|
||||
const RSS = require("./rss.js");
|
||||
const {
|
||||
CURRENT_DATE,
|
||||
websiteDateToTime,
|
||||
validateArguments,
|
||||
timeToWebsiteDate,
|
||||
} = require("./utils.js");
|
||||
const {
|
||||
WebsiteRedirectedURLError,
|
||||
WebsiteNotFoundError,
|
||||
WebsiteUnexpectedError,
|
||||
LocationNotFoundError,
|
||||
TimesNotFoundError,
|
||||
DatesNotFoundError,
|
||||
} = require("./errors.js");
|
||||
|
||||
const CURRENT_DATE = new Date()
|
||||
const URL = "https://folloren.no/levering-av-avfall/miljobilen/";
|
||||
|
||||
// TODO
|
||||
//
|
||||
// When running we want to fill all dates that are:
|
||||
// 1. next date
|
||||
// 2. any previous date
|
||||
// 3. no date that already exists
|
||||
//
|
||||
// solved with iterating until the current date found
|
||||
/**
|
||||
* Get HTML text content from url.
|
||||
* @param {string} url
|
||||
* @param {RequestInit} options
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function getSite(url, options = { redirect: "manual" }) {
|
||||
return fetch(url, options)
|
||||
.catch((err) => {
|
||||
throw new WebsiteUnexpectedError(err.message, url);
|
||||
})
|
||||
.then(async (resp) => {
|
||||
if (resp.ok) {
|
||||
return await resp.text();
|
||||
}
|
||||
|
||||
// TODO Handle rollover of date list
|
||||
//
|
||||
// New years to start: 30.12 04.01 30.01
|
||||
// End to new years: 21.12 31.12 03.01 14.01
|
||||
//
|
||||
// Create function that takes a list of dates and creates
|
||||
// real date objects. It should include logic for look-ahead
|
||||
// to adress rollover.
|
||||
|
||||
async function getSite(url) {
|
||||
return fetch(url).then(async (resp) => {
|
||||
if (!resp.ok) {
|
||||
console.log("unable to fetch site");
|
||||
console.log(err);
|
||||
console.log(resp.status);
|
||||
console.log(resp.statusText);
|
||||
throw err;
|
||||
}
|
||||
|
||||
return await resp.text();
|
||||
});
|
||||
if (resp.status === 301) throw new WebsiteRedirectedURLError(resp, url);
|
||||
throw new WebsiteNotFoundError(resp, url);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches HTML response with regexp looking for location name.
|
||||
* @param {string} text
|
||||
* @param {string} key
|
||||
* @returns {string}
|
||||
* @throws {LocationNotFoundError} If unable to match using regexp
|
||||
*/
|
||||
function getFullLocationName(text, key) {
|
||||
regexpLocation = new RegExp(`(${key}[\\w\\d\\søæå]*),`, "i");
|
||||
location = text.match(regexpLocation);
|
||||
const regexpLocation = new RegExp(`(${key}[\\w\\d\\søæå]*),`, "i");
|
||||
const location = text?.match(regexpLocation);
|
||||
|
||||
// TODO null handle
|
||||
|
||||
return location[1]
|
||||
if (location == null || location?.length < 2)
|
||||
throw new LocationNotFoundError(key, location);
|
||||
|
||||
return location[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches HTML response with regexp looking for times.
|
||||
* @param {string} text
|
||||
* @param {string} location
|
||||
* @returns {Array.<string>}
|
||||
* @throws {TimesNotFoundError} If unable to match using regexp
|
||||
*/
|
||||
function getTimeForLocation(text, location) {
|
||||
regexpTime = new RegExp(`${location}, (kl (\\d+:\\d+) – (\\d+:\\d+))`, "i");
|
||||
times = text.match(regexpTime);
|
||||
console.log(times[2], times[3])
|
||||
const regexpTime = new RegExp(
|
||||
`${location}, (kl (\\d+:\\d+) – (\\d+:\\d+))`,
|
||||
"i"
|
||||
);
|
||||
const times = text?.match(regexpTime);
|
||||
|
||||
// TODO null handle
|
||||
if (times == null || times?.length < 4)
|
||||
throw new TimesNotFoundError(location, times);
|
||||
|
||||
from = times[2];
|
||||
to = times[3];
|
||||
const from = times[2];
|
||||
const to = times[3];
|
||||
return [from, to];
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches HTML response with regexp looking for dates.
|
||||
* @param {string} text
|
||||
* @param {string} location
|
||||
* @returns {Array.<string>}
|
||||
* @throws {DatesNotFoundError} If unable to match using regexp
|
||||
*/
|
||||
function getDatesForLocation(text, location) {
|
||||
regexpDatesString = new RegExp(`${location}.*<br>((\\d+.\\d+).*)</p>`, "i");
|
||||
datesString = text.match(regexpDatesString)[0];
|
||||
const regexpDatesString = new RegExp(
|
||||
`${location}.*<br>((\\d+.\\d+).*)</p>`,
|
||||
"i"
|
||||
);
|
||||
let datesStringMatches = text.match(regexpDatesString);
|
||||
|
||||
if (datesStringMatches == null || datesStringMatches?.length === 0)
|
||||
throw new DatesNotFoundError(location, datesStringMatches);
|
||||
|
||||
// only care about first paragraph
|
||||
// TODO make regex stop at first capture
|
||||
datesString = datesString.split('</p>')[0]
|
||||
const datesString = datesStringMatches?.[0]?.split("</p>")?.[0];
|
||||
|
||||
// TODO null check
|
||||
const regexpDates = /(\d+\.\d+)+/g;
|
||||
const dates = String(datesString)?.match(regexpDates);
|
||||
|
||||
regexpDates = /(\d+\.\d+)+/g
|
||||
dates = datesString.match(regexpDates)
|
||||
if (dates == null || dates?.length === 0)
|
||||
throw new DatesNotFoundError(location, dates);
|
||||
|
||||
// TODO null check
|
||||
|
||||
return dates
|
||||
/*
|
||||
dates.push('25.06')
|
||||
dates.push('02.02')
|
||||
dates.push('07.04')
|
||||
dates.push('07.05')
|
||||
*/
|
||||
return dates;
|
||||
}
|
||||
|
||||
function getSolberg(site, TITLE) {
|
||||
const name = getFullLocationName(site, TITLE)
|
||||
/**
|
||||
* Since webpage only has DD.MM dates we want to handle
|
||||
* passing from one year to the next correctly
|
||||
* @param {Array.<Date>} dates
|
||||
* @returns {Array.<Date>}
|
||||
*/
|
||||
function handleDatesWrappingNewYear(dates) {
|
||||
let previousDate = dates[0];
|
||||
return dates.map((date) => {
|
||||
// increments year if a date is found to be wrapping
|
||||
if (date < previousDate) {
|
||||
date.setFullYear(date.getFullYear() + 1);
|
||||
}
|
||||
|
||||
previousDate = date;
|
||||
return date;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for name, times and dates in HTML text response.
|
||||
* @param {string} site
|
||||
* @param {string} TITLE
|
||||
* @returns {import('./types.js').Location}
|
||||
*/
|
||||
function getPickupDatesForLocation(site, TITLE) {
|
||||
const name = getFullLocationName(site, TITLE);
|
||||
const [from, to] = getTimeForLocation(site, name);
|
||||
const dates = getDatesForLocation(site, TITLE);
|
||||
const dateStrings = getDatesForLocation(site, TITLE);
|
||||
const dates = dateStrings.map(websiteDateToTime);
|
||||
|
||||
return { name, times: { from, to }, dates };
|
||||
handleDatesWrappingNewYear(dates);
|
||||
|
||||
return { name, times: { from, to }, dates, dateStrings };
|
||||
}
|
||||
|
||||
// convert string DD.MM to JS date object
|
||||
function websiteDateToTime(dateString) {
|
||||
const date = new Date()
|
||||
let [_, day, month] = dateString.match(/(\d+).(\d+)/)
|
||||
day = Number(day)
|
||||
month = Number(month)
|
||||
/**
|
||||
* Filters out only the dates we want in RSS feed.
|
||||
* @param {Array.<Date>} dates
|
||||
* @param {number} LOOK_AHEAD
|
||||
* @returns {Array.<Date>}
|
||||
*/
|
||||
function relevantDates(dates, LOOK_AHEAD) {
|
||||
let futureDatesFound = 0;
|
||||
|
||||
date.setMonth(Number(month - 1))
|
||||
date.setDate(Number(day))
|
||||
// console.log({day, month})
|
||||
// console.log('prev:', date <= CURRENT_DATE)
|
||||
// console.log('futr:', date > CURRENT_DATE)
|
||||
return dates.filter((date) => {
|
||||
if (date > CURRENT_DATE) futureDatesFound = futureDatesFound + 1;
|
||||
if (futureDatesFound > LOOK_AHEAD) return undefined;
|
||||
|
||||
return date
|
||||
return date;
|
||||
});
|
||||
}
|
||||
|
||||
// convert JS date object to DD.MM string
|
||||
function timeToWebsiteDate(date) {
|
||||
const day = date.getDate()
|
||||
const month = date.getMonth() + 1
|
||||
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
|
||||
return `${pad(day)}.${pad(month)}`
|
||||
}
|
||||
|
||||
function relevantDates(allDates) {
|
||||
const relevantDates = []
|
||||
let index = 0;
|
||||
let date = 0
|
||||
|
||||
// this selects all dates before current date AND the
|
||||
// next one since index incrementation is after push
|
||||
while (date <= CURRENT_DATE) {
|
||||
date = websiteDateToTime(allDates[index])
|
||||
|
||||
relevantDates.push(timeToWebsiteDate(date))
|
||||
index = index + 1;
|
||||
}
|
||||
|
||||
return relevantDates
|
||||
}
|
||||
|
||||
// fetch websites
|
||||
// parse for name, time and dates
|
||||
// convert to JS dates
|
||||
// parse RSS feed
|
||||
// convert to JS dates
|
||||
// add dates not in feed
|
||||
// write feed
|
||||
|
||||
async function main() {
|
||||
const URL = "https://folloren.no/levering-av-avfall/miljobilen"
|
||||
site = await getSite(URL);
|
||||
const PLACE = process.argv[2];
|
||||
const LOOK_AHEAD = process.argv[3] || 2;
|
||||
const PRINT = process.argv[4] === '-p' || false;
|
||||
validateArguments(PLACE, LOOK_AHEAD);
|
||||
|
||||
console.log("got site:", site?.length || -1);
|
||||
const PLACE = 'Langhus'
|
||||
location = getSolberg(site, PLACE)
|
||||
|
||||
console.log(location)
|
||||
const site = await getSite(URL);
|
||||
const location = getPickupDatesForLocation(site, PLACE);
|
||||
// console.log(location);
|
||||
|
||||
let { name, times, dates } = location;
|
||||
// dates[0] = '30.12'
|
||||
dates.push('11.05')
|
||||
dates.push('12.05')
|
||||
dates.push('13.05')
|
||||
console.log("all dates:", dates)
|
||||
dates = relevantDates(dates, Number(LOOK_AHEAD));
|
||||
|
||||
if (PRINT) {
|
||||
console.log(
|
||||
`${name} @ ${times.from}-${times.to}, dates found: ${dates.length}`
|
||||
);
|
||||
console.log(dates.map(timeToWebsiteDate));
|
||||
}
|
||||
|
||||
// todo relevant dates elsewhere
|
||||
dates = relevantDates(dates)
|
||||
console.log("rel dates:", dates)
|
||||
const rss = new RSS(name);
|
||||
rss.generate(times, dates, URL)
|
||||
rss.write()
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (err) {
|
||||
console.log("something went wront when runnning script");
|
||||
console.log(err);
|
||||
rss.generate(times, dates, URL);
|
||||
rss.write();
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
15
src/types.js
Normal file
15
src/types.js
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @typedef {Object} Location
|
||||
* @property {string} name
|
||||
* @property {LocationTimes} times
|
||||
* @property {Array.<Date>} dates
|
||||
* @property {Array.<string>} dateStrings
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} LocationTimes
|
||||
* @property {string} from - The from time string
|
||||
* @property {string} to - The to time string
|
||||
*/
|
||||
|
||||
module.exports = {}
|
||||
79
src/utils.js
79
src/utils.js
@@ -1,9 +1,76 @@
|
||||
function uuidv4() {
|
||||
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c =>
|
||||
(+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16)
|
||||
);
|
||||
const CURRENT_DATE = new Date();
|
||||
const HELP_TEXT = `\
|
||||
usage: node src/run.js <name> [<look-ahead>] [-p] [-h | --help]
|
||||
|
||||
These are the available arguments:
|
||||
name Name of location to search for
|
||||
look-ahead How many days in future to generate, defaults 2
|
||||
print Prints results
|
||||
help Prints this message`;
|
||||
|
||||
/**
|
||||
* Converts string DD.MM to JS date object.
|
||||
* @param {string} dateString
|
||||
* @returns {Date}
|
||||
*/
|
||||
function websiteDateToTime(dateString) {
|
||||
const date = new Date();
|
||||
const dayAndMonthString = dateString?.match(/(\d+).(\d+)/);
|
||||
|
||||
if (dayAndMonthString == null || dayAndMonthString.length < 3)
|
||||
throw new Error("Unable to created string from unparsable date.");
|
||||
|
||||
const day = Number(dayAndMonthString[1]);
|
||||
const month = Number(dayAndMonthString[2]);
|
||||
|
||||
date.setMonth(Number(month - 1));
|
||||
date.setDate(Number(day));
|
||||
|
||||
return date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts JS date object to DD.MM(.YY) string
|
||||
* @param {Date} date
|
||||
* @returns {string}
|
||||
*/
|
||||
function timeToWebsiteDate(date) {
|
||||
const day = date.getDate();
|
||||
const month = date.getMonth() + 1;
|
||||
const year = date.getFullYear();
|
||||
|
||||
/**
|
||||
* @param {string | number} n
|
||||
*/
|
||||
const pad = (n) => String(n).padStart(2, "0");
|
||||
|
||||
// adds year if showing next years date
|
||||
if (year > CURRENT_DATE.getFullYear())
|
||||
return `${pad(day)}.${pad(month)}.${pad(year - 2000)}`;
|
||||
else return `${pad(day)}.${pad(month)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints help and validates argv
|
||||
* @param {string} place
|
||||
* @param {number | string} lookAhead
|
||||
* @throws Error
|
||||
*/
|
||||
function validateArguments(place, lookAhead) {
|
||||
const placeUndefined = place === undefined;
|
||||
const help = (place === "-h" || place === "--help")
|
||||
const numberIsNotNumber = isNaN(Number(lookAhead))
|
||||
const validation = [placeUndefined, help, numberIsNotNumber]
|
||||
|
||||
if (!validation.every(v => v === false)) {
|
||||
console.log(HELP_TEXT);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
uuidv4
|
||||
}
|
||||
CURRENT_DATE,
|
||||
websiteDateToTime,
|
||||
timeToWebsiteDate,
|
||||
validateArguments,
|
||||
};
|
||||
|
||||
20
yarn.lock
Normal file
20
yarn.lock
Normal file
@@ -0,0 +1,20 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@types/node@^20.14.2":
|
||||
version "20.14.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.2.tgz#a5f4d2bcb4b6a87bffcaa717718c5a0f208f4a18"
|
||||
integrity sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
typescript@^5.4.5:
|
||||
version "5.4.5"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611"
|
||||
integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==
|
||||
|
||||
undici-types@~5.26.4:
|
||||
version "5.26.5"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
|
||||
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
||||
Reference in New Issue
Block a user