266 lines
6.3 KiB
JavaScript
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
|
|
*/
|