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