123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341 |
- const fetch = require('isomorphic-unfetch')
- const shows = require('./shows.js')
- const calendar = require('./calendar.js')
- function getStreaminfoUrl (siteurl) {
- return siteurl + '/streaminfo.json' // XXX: improve this logic
- }
- function getManifestUrl (siteurl) {
- return siteurl + '/radiomanifest.xml' // XXX: improve this logic
- }
- function getAttribute(el, attr, default_value) {
- if(el.hasAttribute(attr))
- return el.getAttribute(attr);
- return default_value;
- }
- /**
- * Represents everything we know about a radio. This includes, but is not limited to, radiomanifest.xml.
- */
- class Radio {
- /**
- * @param {Array} [sources] optional
- * @param {string} [scheduleURL]
- * @param {string} [showsURL]
- * @param {string} [feed]
- */
- constructor (sources, scheduleURL, showsURL, feed) {
- this.streaming = new RadioStreaming(sources)
- this.scheduleURL = scheduleURL
- this.showsURL = showsURL
- this.feed = feed
- this.name = ''
- this.description = ''
- this.logo = null
- this.shows = []
- this.schedule = null
- }
- /**
- * @returns {RadioStreaming}
- */
- getStreaming () {
- return this.streaming
- }
- setName (name) {
- this.name = name
- }
- /**
- * The radio name, as inferred by stream-meta
- *
- * @returns {string}
- */
- getName() {
- return this.name
- }
- setDescription(desc) {
- this.description = desc
- }
- /**
- * The description of the radio, as inferred by stream-meta
- *
- * @returns {string}
- */
- getDescription() {
- return this.description
- }
- setLogo(logo) {
- this.logo = logo
- }
- /**
- * @returns {string} the URL of the logo, or `null` if not found
- */
- getLogo() {
- return this.logo
- }
- /**
- *
- * @returns {Array<RadioShow>}
- */
- getShows() {
- return this.shows
- }
- /**
- * @returns {RadioShow} The lookup is exact and case-sensitive. If no such show can be found, `null`
- * is returned.
- */
- getShowByName (showName) {
- if (this.shows === undefined) return null
- return this.shows.find(s => s.name === showName)
- }
- /**
- * @returns {RadioSchedule} If no schedule is present, `null` is returned.
- */
- getSchedule () {
- return this.schedule
- }
- /**
- * Find if a show is running at the given moment. If there's none, `null` is returned.
- * If possible, a complete {@link RadioShow} including full informations (from shows.xml) is returned.
- * If, instead, we know from the `schedule` that there must be a show, but have no additional detail, a
- * {@link RadioShow} object will be created on the fly.
- *
- * @param {ICAL.Time} [now] If omitted, the current time is used.
- * @returns {RadioShow} If we don't know what's going on at the given moment, `null` is returned.
- */
- getShowAtTime (now) {
- if (this.schedule === undefined || this.schedule === null) return null
- return this.getSchedule().getNowShow(now)
- }
- /**
- * This static method can create a Radio object from a valid radiomanifest.xml file
- *
- * @param xml An already parsed xml block
- * @returns {Radio}
- */
- static fromDOM (xml) {
- const doc = xml.cloneNode(true)
- let res = doc.evaluate('/radio-manifest/streaming/source', doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
- const sources = []
- for (let i = 0; i < res.snapshotLength; i++) {
- const src = res.snapshotItem(i)
- if (!src.hasAttribute('priority')) {
- src.setAttribute('priority', '0')
- } else if (parseInt(src.getAttribute('priority'), 10) < 0) {
- continue
- }
- sources.push(src)
- }
- res = doc.evaluate('/radio-manifest/schedule', doc)
- const scheduleEl = res.iterateNext()
- let scheduleURL = null
- if (scheduleEl !== null) {
- scheduleURL = scheduleEl.getAttribute('src')
- }
- res = xml.evaluate('/radio-manifest/shows', xml)
- const showsEl = res.iterateNext()
- let showsURL = null
- if (showsEl !== null) {
- showsURL = showsEl.getAttribute('src')
- }
- res = xml.evaluate('/radio-manifest/feed', xml)
- const feedEl = res.iterateNext()
- let feed = null
- if (feedEl !== null) {
- feed = feedEl.getAttribute('src')
- }
- const manifest = new Radio(sources, scheduleURL, showsURL, feed)
- return manifest
- }
- }
- /**
- * Represents the streaming capabilities of a radio.
- * This has probably been derived from the <streaming> block in radiomanifest.xml
- */
- class RadioStreaming {
- constructor (sources) {
- this.sources = sources.sort(
- (a,b) => this.getPriority(a) < this.getPriority(a)
- )
- }
- /**
- * Get the list of possible options that are provided to the user
- * @returns {Array<string>}
- */
- getOptions() {
- return this.sources.map(function (x) {
- return x.getAttribute('name')
- })
- }
- /**
- * @private
- */
- getPriority(element) {
- return parseInt(getAttribute(element, 'priority', '1'))
- }
- /**
- * @private
- */
- getTopPrioritySources() {
- var topPriority = this.getPriority(this.sources[0])
- return this.sources.filter(
- (src) => parseInt(src.getAttribute('priority'), 10) === topPriority
- )
- }
- /**
- * @return {string} url of the source. Note that this is much probably a playlist, in M3U format
- */
- getSource(name) {
- if (name === undefined) {
- return this.getTopPrioritySources()[0]
- }
- const s = this.sources.find(function (x) {
- return x.getAttribute('name') === name
- })
- if (s === undefined) return s
- return s.getAttribute('src')
- }
-
- /**
- * This is your go-to function whenever you need a list of URLs to play.
- * They will be picked honoring priorities, and expanding the playlist source
- * @return {Array<string>}
- */
- async pickURLs() {
- var allSources = this.getTopPrioritySources()
- var allAudios = []
- for(let src of allSources) {
- let url = src.getAttribute('src')
- let resp = await fetch(url)
- allAudios.unshift(... parseM3U(await resp.text()))
- }
- return allAudios
- }
- /**
- * Just like {@link RadioStreaming#pickURLs}, but get a single URL
- * @return {string}
- */
- async pickURL() {
- var allAudios = await this.pickURLs()
- return allAudios[0]
- }
- }
- /**
- * Create everything you need - **you should start from here**
- *
- * @param {string} siteurl URL of website you want to load
- * @param {Object} options options. Currenly unused
- * @return {Radio}
- */
- async function get (siteurl, options) {
- let resp = await fetch(getManifestUrl(siteurl))
- let text = await resp.text()
- const parser = new DOMParser()
- const dom = parser.parseFromString(text, 'text/xml')
- const manifest = Radio.fromDOM(dom)
- try {
- manifest.shows = await shows.get(manifest)
- } catch (e) {
- console.error("Error while fetching shows file", e)
- }
- try {
- manifest.schedule = await calendar.get(manifest)
- if (manifest.schedule !== undefined)
- manifest.schedule.radio = manifest
- } catch (e) {
- console.error("Error while fetching shows file", e)
- }
- resp = null
- try {
- resp = await fetch(getStreaminfoUrl(siteurl))
- } catch (e) {
- true
- }
- if(resp !== null) {
- try {
- text = await resp.text()
- const data = JSON.parse(text)
- const name = data['icy-name']
- if (name !== undefined) {
- manifest.setName(name)
- }
- const desc = data['icy-description']
- if (desc !== undefined) {
- manifest.setDescription(desc)
- }
- const logo = data['icy-logo']
- if (desc !== undefined) {
- manifest.setLogo(logo)
- }
- } catch (e) {
- if (e instanceof SyntaxError) {
- true
- } else {
- console.error('Error', e)
- throw e
- }
- }
- }
- return manifest
- }
- function parseM3U (body) {
- return body.split('\n').filter((line) => {
- if (line.startsWith('#')) {
- return false
- } else {
- try {
- new URL(line); return true
- } catch {
- return false
- }
- }
- })
- }
- module.exports = {
- get: get,
- objs: {
- Radio: Radio,
- RadioStreaming: RadioStreaming
- },
- parsers: {
- M3U: parseM3U,
- radioManifest: Radio.fromDOM,
- shows: shows.parse,
- }
- }
|