radiomanifest.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. const fetch = require('isomorphic-unfetch')
  2. const shows = require('./shows.js')
  3. function getStreaminfoUrl (siteurl) {
  4. return siteurl + '/streaminfo.json' // XXX: improve this logic
  5. }
  6. function getManifestUrl (siteurl) {
  7. return siteurl + '/radiomanifest.xml' // XXX: improve this logic
  8. }
  9. function getAttribute(el, attr, default_value) {
  10. if(el.hasAttribute(attr))
  11. return el.getAttribute(attr);
  12. return default_value;
  13. }
  14. class Radio {
  15. constructor (sources, schedule, showsURL, feed) {
  16. this.streaming = new RadioStreaming(sources)
  17. this.schedule = schedule
  18. this.showsURL = showsURL
  19. this.feed = feed
  20. this.name = ''
  21. this.shows = []
  22. }
  23. getStreaming () {
  24. return this.streaming
  25. }
  26. setName (name) {
  27. this.name = name
  28. }
  29. getShows() {
  30. return this.shows
  31. }
  32. getShowByName (showName) {
  33. return this.shows.find(s => s.name === showName)
  34. }
  35. getSchedule () {
  36. }
  37. getShowAtTime () {
  38. }
  39. static fromDOM (xml) {
  40. const doc = xml.cloneNode(true)
  41. let res = doc.evaluate('/radio-manifest/streaming/source', doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
  42. const sources = []
  43. for (let i = 0; i < res.snapshotLength; i++) {
  44. const src = res.snapshotItem(i)
  45. if (!src.hasAttribute('priority')) {
  46. src.setAttribute('priority', '0')
  47. } else if (parseInt(src.getAttribute('priority'), 10) < 0) {
  48. continue
  49. }
  50. sources.push(src)
  51. }
  52. res = doc.evaluate('/radio-manifest/schedule', doc)
  53. const scheduleEl = res.iterateNext()
  54. let schedule = null
  55. if (scheduleEl !== null) {
  56. schedule = scheduleEl.getAttribute('src')
  57. }
  58. res = xml.evaluate('/radio-manifest/shows', xml)
  59. const showsEl = res.iterateNext()
  60. let showsURL = null
  61. if (showsEl !== null) {
  62. showsURL = showsEl.getAttribute('src')
  63. }
  64. res = xml.evaluate('/radio-manifest/feed', xml)
  65. const feedEl = res.iterateNext()
  66. let feed = null
  67. if (feedEl !== null) {
  68. feed = feedEl.getAttribute('src')
  69. }
  70. const manifest = new Radio(sources, schedule, showsURL, feed)
  71. return manifest
  72. }
  73. }
  74. class RadioStreaming {
  75. constructor (sources) {
  76. this.sources = sources.sort(
  77. (a,b) => this.getPriority(a) < this.getPriority(a)
  78. )
  79. }
  80. getOptions() {
  81. return this.sources.map(function (x) {
  82. return x.getAttribute('name')
  83. })
  84. }
  85. // this is private
  86. getPriority(element) {
  87. return parseInt(getAttribute(element, 'priority', '1'))
  88. }
  89. // this is private
  90. getTopPrioritySources() {
  91. var topPriority = this.getPriority(this.sources[0])
  92. return this.sources.filter(
  93. (src) => parseInt(src.getAttribute('priority'), 10) === topPriority
  94. )
  95. }
  96. getSource(name) {
  97. if (name === undefined) {
  98. return this.getTopPrioritySources()[0]
  99. }
  100. const s = this.sources.find(function (x) {
  101. return x.getAttribute('name') === name
  102. })
  103. if (s === undefined) return s
  104. return s.getAttribute('src')
  105. }
  106. async pickURLs() {
  107. var allSources = this.getTopPrioritySources()
  108. var allAudios = []
  109. for(let src of allSources) {
  110. let url = src.getAttribute('src')
  111. let resp = await fetch(url)
  112. allAudios.unshift(... parseM3U(await resp.text()))
  113. }
  114. return allAudios
  115. }
  116. async pickURL() {
  117. var allAudios = await this.pickURLs()
  118. return allAudios[0]
  119. }
  120. }
  121. async function get (siteurl, options) {
  122. let resp = await fetch(getManifestUrl(siteurl))
  123. let text = await resp.text()
  124. const parser = new DOMParser()
  125. const dom = parser.parseFromString(text, 'text/xml')
  126. const manifest = Radio.fromDOM(dom)
  127. try {
  128. manifest.shows = await shows.get(manifest)
  129. } catch (e) {
  130. console.error("Error while fetching shows file", e)
  131. }
  132. resp = null
  133. try {
  134. resp = await fetch(getStreaminfoUrl(siteurl))
  135. } catch (e) {
  136. true
  137. }
  138. if(resp !== null) {
  139. try {
  140. text = await resp.text()
  141. const data = JSON.parse(text)
  142. const name = data['icy-name']
  143. if (name !== undefined) {
  144. manifest.setName(name)
  145. }
  146. } catch (e) {
  147. if (e instanceof SyntaxError) {
  148. true
  149. } else {
  150. console.error('Error', e)
  151. throw e
  152. }
  153. }
  154. }
  155. return manifest
  156. }
  157. function parseM3U (body) {
  158. return body.split('\n').filter((line) => {
  159. if (line.startsWith('#')) {
  160. return false
  161. } else {
  162. try {
  163. new URL(line); return true
  164. } catch {
  165. return false
  166. }
  167. }
  168. })
  169. }
  170. module.exports = {
  171. get: get,
  172. objs: {
  173. Radio: Radio,
  174. RadioStreaming: RadioStreaming
  175. },
  176. parsers: {
  177. M3U: parseM3U,
  178. radioManifest: Radio.fromDOM,
  179. shows: shows.parse,
  180. }
  181. }