calendar.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. import ICAL from "ical.js";
  2. import shows from "./shows";
  3. function max(a, b) {
  4. if (a < b) return b;
  5. return a;
  6. }
  7. /**
  8. * Represent a schedule (ie: a .ics file)
  9. */
  10. class RadioSchedule {
  11. constructor(calendar, radio) {
  12. this.calendar = calendar; // ICAL.Calendar
  13. this.radio = radio; // radiomanifest.Radio
  14. }
  15. /**
  16. * Get a list of all known {@link vEvent}s
  17. *
  18. * @returns {Array<external:ICAL~Component>}
  19. * */
  20. getEvents() {
  21. return this.calendar.getAllSubcomponents("vevent");
  22. }
  23. /**
  24. * @returns {RadioShow} tries to get a matching show, or create a new one
  25. */
  26. getShowByEvent(ev) {
  27. if (ev === null) return null;
  28. if (this.radio !== undefined) {
  29. const showid = RadioSchedule.veventGetShowID(ev);
  30. var show = this.radio.getShowByName(showid);
  31. if (show === null || show === undefined) {
  32. return new shows.RadioShow(RadioSchedule.veventGetSummary(ev));
  33. }
  34. return show;
  35. }
  36. return new shows.RadioShow(RadioSchedule.veventGetSummary(ev));
  37. }
  38. /**
  39. * @returns {external:ICAL~Component} if nothing is going on right now, `null` is returned
  40. */
  41. getNowEvent(now) {
  42. var ev_now = this.getEvents().filter(function (vevent) {
  43. const ev = new ICAL.Event(vevent);
  44. return isNow(ev, now);
  45. });
  46. ev_now.sort((e1, e2) => {
  47. return this.veventGetPriority(e1) - this.veventGetPriority(e2);
  48. });
  49. if (ev_now.length === 0) return null;
  50. return ev_now[0];
  51. }
  52. /**
  53. * @returns {RadioShow} if nothing is going on right now, `null` is returned
  54. */
  55. getNowShow(now) {
  56. const ev = this.getNowEvent(now);
  57. return this.getShowByEvent(ev);
  58. }
  59. /**
  60. * @returns {NextEvent} if there is none, `null` is returned
  61. */
  62. getNextEvent(now) {
  63. var nowEvent = this.getNowEvent(now);
  64. let future_events = this.getEvents()
  65. .filter((e) => {
  66. return e != nowEvent;
  67. })
  68. .map((e) => {
  69. const vEvent = new ICAL.Event(e);
  70. return { event: e, time: getNext(vEvent, now) };
  71. })
  72. .filter((x) => {
  73. return x.time !== null && x.time !== undefined;
  74. });
  75. // since ".sort()" is guaranteed to be stable, we can sort by priority, then by date, so that two events
  76. // starting at the same time will be sorted observing priority
  77. future_events.sort(
  78. (x1, x2) =>
  79. this.veventGetPriority(x1.event) - this.veventGetPriority(x2.event),
  80. );
  81. future_events.sort((x, y) => x.time.toUnixTime() - y.time.toUnixTime());
  82. if (future_events.length === 0) {
  83. return null;
  84. }
  85. return future_events[0];
  86. }
  87. /**
  88. * @returns {NextShow} if there is no next show, null will be returned
  89. */
  90. getNextShow(now) {
  91. const next = this.getNextEvent(now);
  92. if (next === null) return null;
  93. const ev = next.event;
  94. const show = this.getShowByEvent(ev);
  95. return { show: show, time: next.time };
  96. }
  97. static veventGetSummary(vevent) {
  98. return vevent.getFirstProperty("summary").getFirstValue();
  99. }
  100. static veventGetShowID(vevent) {
  101. return RadioSchedule.veventGetSummary(vevent); // XXX: X-Show-ID
  102. }
  103. /**
  104. * @return {integer} a normalized version of priority, easier to compare
  105. */
  106. veventGetPriority(ev) {
  107. const prop = ev.getFirstProperty("priority");
  108. let prio;
  109. if (prop === null) {
  110. prio = null;
  111. } else {
  112. prio = prop.getFirstValue();
  113. }
  114. if (prio === null || prio === 0) {
  115. prio = 100;
  116. }
  117. return prio;
  118. }
  119. }
  120. function isNow(vEvent, now) {
  121. if (now === undefined) {
  122. now = ICAL.Time.now();
  123. }
  124. if (vEvent.isRecurring()) {
  125. return isNowRecurring(vEvent, now);
  126. }
  127. return now < vEvent.endDate && now > vEvent.startDate;
  128. }
  129. function isNowRecurring(vEvent, now) {
  130. var expand = vEvent.iterator(vEvent.startDate);
  131. var next, next_end;
  132. while ((next = expand.next())) {
  133. next_end = next.clone();
  134. next_end.addDuration(vEvent.duration);
  135. if (next_end > now) {
  136. break;
  137. }
  138. }
  139. return now < next_end && now > next;
  140. }
  141. /*
  142. * @private
  143. * @param {external:ICAL~Component} vEvent a _recurring_ vEvent
  144. * @param {external:ICAL~Time} [now]
  145. * @return {external:ICAL~Time} first future occurrence of this event
  146. */
  147. function getNext(vEvent, now) {
  148. if (now === undefined) {
  149. now = ICAL.Time.now();
  150. }
  151. if (vEvent.isRecurring()) {
  152. return getNextRecurring(vEvent, now);
  153. }
  154. if (vEvent.endDate > now) {
  155. const val = max(now, vEvent.startDate);
  156. return val;
  157. }
  158. return null;
  159. }
  160. /*
  161. * @private
  162. * @param {external:ICAL~Component} vEvent a _recurring_ vEvent
  163. * @param {external:ICAL~Time} now
  164. * @return {external:ICAL~Time} first future occurrence of this event
  165. */
  166. function getNextRecurring(vEvent, now) {
  167. var expand = vEvent.iterator(vEvent.startDate);
  168. var next, next_end;
  169. while ((next = expand.next())) {
  170. const start = next.clone();
  171. next_end = start.clone();
  172. next_end.addDuration(vEvent.duration);
  173. if (next_end <= now) {
  174. continue;
  175. }
  176. return max(start, now);
  177. }
  178. return null;
  179. }
  180. async function get(manifest) {
  181. if (manifest.scheduleURL) {
  182. let resp = null;
  183. try {
  184. resp = await fetch(manifest.scheduleURL);
  185. } catch (e) {
  186. true;
  187. }
  188. if (resp !== null) {
  189. try {
  190. const text = await resp.text();
  191. return parse(text);
  192. } catch (e) {
  193. console.error("Error while parsing schedule", e);
  194. throw e;
  195. }
  196. }
  197. }
  198. }
  199. /**
  200. * Parse ICAL and get a RadioSchedule
  201. *
  202. * @param {string} text The text, in ICS format
  203. * @returns {RadioSchedule}
  204. */
  205. function parse(text) {
  206. var jcalData = ICAL.parse(text);
  207. var vcalendar = new ICAL.Component(jcalData);
  208. return new RadioSchedule(vcalendar);
  209. }
  210. export default {
  211. get: get,
  212. parse: parse,
  213. RadioSchedule: RadioSchedule,
  214. };
  215. /**
  216. * @typedef {Object} NextShow
  217. * @property {RadioShow} show The next show scheduled
  218. * @property {external:ICAL~Time} time When it will start
  219. */
  220. /**
  221. * @typedef {Object} NextEvent
  222. * @property {RadioShow} event The next show scheduled
  223. * @property {external:ICAL~Time} time When it will start
  224. */
  225. /**
  226. * @external ICAL
  227. * @see https://mozilla-comm.github.io/ical.js/api/index.html
  228. */
  229. /**
  230. * @class Component
  231. * @memberof external:ICAL
  232. * @inner
  233. * @see https://mozilla-comm.github.io/ical.js/api/ICAL.Component.html
  234. */
  235. /**
  236. * @class Time
  237. * @memberof external:ICAL
  238. * @inner
  239. * @see https://mozilla-comm.github.io/ical.js/api/ICAL.Time.html
  240. */