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} * */ 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 */