mirror of
https://github.com/MichMich/MagicMirror.git
synced 2026-06-15 20:25:16 +00:00
ef2c7dbb55
This adds a pre-filter that drops out-of-window non-recurring events from the raw ICS data before `node-ical` parses it. Recurring events and anything we cannot classify cheaply are kept - the existing logic handles those as before. Some calendars accumulate years of old events. Pre-filtering them reduces the amount of work `node-ical` has to do and keeps event loop lag lower. My benchmarks on a fast machine showed a small but measurable speedup (roughly 10-20% with generated test data); on a Raspberry Pi it should be significantly more noticeable. For the implementation I ended up using [`ics-filter`](https://github.com/runely/ics-filter), suggested by @rejas. I had a custom version first, but ICS date string parsing has enough complexity that it makes sense to delegate it to a package built specifically for this. I also contributed a couple of fixes to `ics-filter` along the way, and the maintainer was responsive and open to improvements, so it seems like a good fit. Solves #4103.
142 lines
4.3 KiB
JavaScript
142 lines
4.3 KiB
JavaScript
const ical = require("node-ical");
|
|
const Log = require("logger");
|
|
const CalendarFetcherUtils = require("./calendarfetcherutils");
|
|
const HTTPFetcher = require("#http_fetcher");
|
|
|
|
/**
|
|
* CalendarFetcher - Fetches and parses iCal calendar data
|
|
* Uses HTTPFetcher for HTTP handling with intelligent error handling
|
|
* @class
|
|
*/
|
|
class CalendarFetcher {
|
|
|
|
/**
|
|
* Creates a new CalendarFetcher instance
|
|
* @param {string} url - The URL of the calendar to fetch
|
|
* @param {number} reloadInterval - Time in ms between fetches
|
|
* @param {string[]} excludedEvents - Event titles to exclude
|
|
* @param {number} maximumEntries - Maximum number of events to return
|
|
* @param {number} maximumNumberOfDays - Maximum days in the future to fetch
|
|
* @param {object} auth - Authentication options {method: 'basic'|'bearer', user, pass}
|
|
* @param {boolean} includePastEvents - Whether to include past events
|
|
* @param {boolean} selfSignedCert - Whether to accept self-signed certificates
|
|
*/
|
|
constructor (url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, includePastEvents, selfSignedCert) {
|
|
this.url = url;
|
|
this.excludedEvents = excludedEvents;
|
|
this.maximumEntries = maximumEntries;
|
|
this.maximumNumberOfDays = maximumNumberOfDays;
|
|
this.includePastEvents = includePastEvents;
|
|
|
|
this.events = [];
|
|
this.lastFetch = null;
|
|
this.fetchFailedCallback = () => {};
|
|
this.eventsReceivedCallback = () => {};
|
|
|
|
// Use HTTPFetcher for HTTP handling (Composition)
|
|
this.httpFetcher = new HTTPFetcher(url, {
|
|
reloadInterval,
|
|
auth,
|
|
selfSignedCert
|
|
});
|
|
|
|
// Wire up HTTPFetcher events
|
|
this.httpFetcher.on("response", (response) => this.#handleResponse(response));
|
|
this.httpFetcher.on("error", (errorInfo) => this.fetchFailedCallback(this, errorInfo));
|
|
}
|
|
|
|
/**
|
|
* Handles successful HTTP response
|
|
* @param {Response} response - The fetch Response object
|
|
*/
|
|
async #handleResponse (response) {
|
|
try {
|
|
// 304 Not Modified has no body: keep previously fetched events and just re-broadcast them.
|
|
if (response.status === 304) {
|
|
this.lastFetch = Date.now();
|
|
this.broadcastEvents();
|
|
return;
|
|
}
|
|
|
|
const responseData = await response.text();
|
|
|
|
const filteredData = await CalendarFetcherUtils.preFilterICS(responseData, {
|
|
includePastEvents: this.includePastEvents,
|
|
maximumNumberOfDays: this.maximumNumberOfDays
|
|
});
|
|
|
|
const parsed = await ical.async.parseICS(filteredData);
|
|
|
|
Log.debug(`Parsed iCal data from ${this.url} with ${Object.keys(parsed).length} entries.`);
|
|
|
|
this.events = CalendarFetcherUtils.filterEvents(parsed, {
|
|
excludedEvents: this.excludedEvents,
|
|
includePastEvents: this.includePastEvents,
|
|
maximumEntries: this.maximumEntries,
|
|
maximumNumberOfDays: this.maximumNumberOfDays
|
|
});
|
|
|
|
this.lastFetch = Date.now();
|
|
this.broadcastEvents();
|
|
} catch (error) {
|
|
Log.error(`${this.url} - iCal parsing failed: ${error.message}`);
|
|
this.fetchFailedCallback(this, {
|
|
message: `iCal parsing failed: ${error.message}`,
|
|
status: null,
|
|
errorType: "PARSE_ERROR",
|
|
translationKey: "MODULE_ERROR_UNSPECIFIED",
|
|
retryAfter: this.httpFetcher.reloadInterval,
|
|
retryCount: 0,
|
|
url: this.url,
|
|
originalError: error
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Starts fetching calendar data
|
|
*/
|
|
fetchCalendar () {
|
|
this.httpFetcher.startPeriodicFetch();
|
|
}
|
|
|
|
/**
|
|
* Check if enough time has passed since the last fetch to warrant a new one.
|
|
* Uses reloadInterval as the threshold to respect user's configured fetchInterval.
|
|
* @returns {boolean} True if a new fetch should be performed
|
|
*/
|
|
shouldRefetch () {
|
|
if (!this.lastFetch) {
|
|
return true;
|
|
}
|
|
const timeSinceLastFetch = Date.now() - this.lastFetch;
|
|
return timeSinceLastFetch >= this.httpFetcher.reloadInterval;
|
|
}
|
|
|
|
/**
|
|
* Broadcasts the current events to listeners
|
|
*/
|
|
broadcastEvents () {
|
|
Log.info(`Broadcasting ${this.events.length} events from ${this.url}.`);
|
|
this.eventsReceivedCallback(this);
|
|
}
|
|
|
|
/**
|
|
* Sets the callback for successful event fetches
|
|
* @param {(fetcher: CalendarFetcher) => void} callback - Called when events are received
|
|
*/
|
|
onReceive (callback) {
|
|
this.eventsReceivedCallback = callback;
|
|
}
|
|
|
|
/**
|
|
* Sets the callback for fetch failures
|
|
* @param {(fetcher: CalendarFetcher, error: Error) => void} callback - Called when a fetch fails
|
|
*/
|
|
onError (callback) {
|
|
this.fetchFailedCallback = callback;
|
|
}
|
|
}
|
|
|
|
module.exports = CalendarFetcher;
|