radiomanifest.js/calendar.js
2023-09-14 00:07:13 +01:00

266 lines
6.3 KiB
JavaScript

import ICAL from "ical.js";
import shows from "./shows";
function max(a, b) {
if (a < b) return b;
return a;
}
/**
* Represent a schedule (ie: a .ics file)
*/
class RadioSchedule {
constructor(calendar, radio) {
this.calendar = calendar; // ICAL.Calendar
this.radio = radio; // radiomanifest.Radio
}
/**
* Get a list of all known {@link vEvent}s
*
* @returns {Array<external:ICAL~Component>}
* */
getEvents() {
return this.calendar.getAllSubcomponents("vevent");
}
/**
* @returns {RadioShow} tries to get a matching show, or create a new one
*/
getShowByEvent(ev) {
if (ev === null) return null;
if (this.radio !== undefined) {
const showid = RadioSchedule.veventGetShowID(ev);
var show = this.radio.getShowByName(showid);
if (show === null || show === undefined) {
return new shows.RadioShow(RadioSchedule.veventGetSummary(ev));
}
return show;
}
return new shows.RadioShow(RadioSchedule.veventGetSummary(ev));
}
/**
* @returns {external:ICAL~Component} if nothing is going on right now, `null` is returned
*/
getNowEvent(now) {
var ev_now = this.getEvents().filter(function (vevent) {
const ev = new ICAL.Event(vevent);
return isNow(ev, now);
});
ev_now.sort((e1, e2) => {
return this.veventGetPriority(e1) - this.veventGetPriority(e2);
});
if (ev_now.length === 0) return null;
return ev_now[0];
}
/**
* @returns {RadioShow} if nothing is going on right now, `null` is returned
*/
getNowShow(now) {
const ev = this.getNowEvent(now);
return this.getShowByEvent(ev);
}
/**
* @returns {NextEvent} if there is none, `null` is returned
*/
getNextEvent(now) {
var nowEvent = this.getNowEvent(now);
let future_events = this.getEvents()
.filter((e) => {
return e != nowEvent;
})
.map((e) => {
const vEvent = new ICAL.Event(e);
return { event: e, time: getNext(vEvent, now) };
})
.filter((x) => {
return x.time !== null && x.time !== undefined;
});
// since ".sort()" is guaranteed to be stable, we can sort by priority, then by date, so that two events
// starting at the same time will be sorted observing priority
future_events.sort(
(x1, x2) =>
this.veventGetPriority(x1.event) - this.veventGetPriority(x2.event),
);
future_events.sort((x, y) => x.time.toUnixTime() - y.time.toUnixTime());
if (future_events.length === 0) {
return null;
}
return future_events[0];
}
/**
* @returns {NextShow} if there is no next show, null will be returned
*/
getNextShow(now) {
const next = this.getNextEvent(now);
if (next === null) return null;
const ev = next.event;
const show = this.getShowByEvent(ev);
return { show: show, time: next.time };
}
static veventGetSummary(vevent) {
return vevent.getFirstProperty("summary").getFirstValue();
}
static veventGetShowID(vevent) {
return RadioSchedule.veventGetSummary(vevent); // XXX: X-Show-ID
}
/**
* @return {integer} a normalized version of priority, easier to compare
*/
veventGetPriority(ev) {
const prop = ev.getFirstProperty("priority");
let prio;
if (prop === null) {
prio = null;
} else {
prio = prop.getFirstValue();
}
if (prio === null || prio === 0) {
prio = 100;
}
return prio;
}
}
function isNow(vEvent, now) {
if (now === undefined) {
now = ICAL.Time.now();
}
if (vEvent.isRecurring()) {
return isNowRecurring(vEvent, now);
}
return now < vEvent.endDate && now > vEvent.startDate;
}
function isNowRecurring(vEvent, now) {
var expand = vEvent.iterator(vEvent.startDate);
var next, next_end;
while ((next = expand.next())) {
next_end = next.clone();
next_end.addDuration(vEvent.duration);
if (next_end > now) {
break;
}
}
return now < next_end && now > next;
}
/*
* @private
* @param {external:ICAL~Component} vEvent a _recurring_ vEvent
* @param {external:ICAL~Time} [now]
* @return {external:ICAL~Time} first future occurrence of this event
*/
function getNext(vEvent, now) {
if (now === undefined) {
now = ICAL.Time.now();
}
if (vEvent.isRecurring()) {
return getNextRecurring(vEvent, now);
}
if (vEvent.endDate > now) {
const val = max(now, vEvent.startDate);
return val;
}
return null;
}
/*
* @private
* @param {external:ICAL~Component} vEvent a _recurring_ vEvent
* @param {external:ICAL~Time} now
* @return {external:ICAL~Time} first future occurrence of this event
*/
function getNextRecurring(vEvent, now) {
var expand = vEvent.iterator(vEvent.startDate);
var next, next_end;
while ((next = expand.next())) {
const start = next.clone();
next_end = start.clone();
next_end.addDuration(vEvent.duration);
if (next_end <= now) {
continue;
}
return max(start, now);
}
return null;
}
async function get(manifest) {
if (manifest.scheduleURL) {
let resp = null;
try {
resp = await fetch(manifest.scheduleURL);
} catch (e) {
true;
}
if (resp !== null) {
try {
const text = await resp.text();
return parse(text);
} catch (e) {
console.error("Error while parsing schedule", e);
throw e;
}
}
}
}
/**
* Parse ICAL and get a RadioSchedule
*
* @param {string} text The text, in ICS format
* @returns {RadioSchedule}
*/
function parse(text) {
var jcalData = ICAL.parse(text);
var vcalendar = new ICAL.Component(jcalData);
return new RadioSchedule(vcalendar);
}
export default {
get: get,
parse: parse,
RadioSchedule: RadioSchedule,
};
/**
* @typedef {Object} NextShow
* @property {RadioShow} show The next show scheduled
* @property {external:ICAL~Time} time When it will start
*/
/**
* @typedef {Object} NextEvent
* @property {RadioShow} event The next show scheduled
* @property {external:ICAL~Time} time When it will start
*/
/**
* @external ICAL
* @see https://mozilla-comm.github.io/ical.js/api/index.html
*/
/**
* @class Component
* @memberof external:ICAL
* @inner
* @see https://mozilla-comm.github.io/ical.js/api/ICAL.Component.html
*/
/**
* @class Time
* @memberof external:ICAL
* @inner
* @see https://mozilla-comm.github.io/ical.js/api/ICAL.Time.html
*/