diff --git a/radiomanifest.js b/radiomanifest.js index 4ee3fa1..125c954 100644 --- a/radiomanifest.js +++ b/radiomanifest.js @@ -1,170 +1,174 @@ -const fetch = require('isomorphic-unfetch') -const shows = require('./shows.js') -const calendar = require('./calendar.js') +import fetch from 'isomorphic-unfetch' +import shows from './shows.js' +import calendar from './calendar.js' -function getStreaminfoUrl (siteurl) { - return siteurl + '/streaminfo.json' // XXX: improve this logic +function getStreaminfoUrl(siteurl) { + return siteurl + "/streaminfo.json" // XXX: improve this logic } -function getManifestUrl (siteurl) { - return siteurl + '/radiomanifest.xml' // 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; + 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} - */ - 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) + /** + * @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 } - res = doc.evaluate('/radio-manifest/schedule', doc) - const scheduleEl = res.iterateNext() - let scheduleURL = null - if (scheduleEl !== null) { - scheduleURL = scheduleEl.getAttribute('src') + /** + * @returns {RadioStreaming} + */ + getStreaming() { + return this.streaming } - res = xml.evaluate('/radio-manifest/shows', xml) - const showsEl = res.iterateNext() - let showsURL = null - if (showsEl !== null) { - showsURL = showsEl.getAttribute('src') + setName(name) { + this.name = name } - res = xml.evaluate('/radio-manifest/feed', xml) - const feedEl = res.iterateNext() - let feed = null - if (feedEl !== null) { - feed = feedEl.getAttribute('src') + /** + * The radio name, as inferred by stream-meta + * + * @returns {string} + */ + getName() { + return this.name } - const manifest = new Radio(sources, scheduleURL, showsURL, feed) - return manifest - } + 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} + */ + 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 + } } /** @@ -172,75 +176,71 @@ class Radio { * This has probably been derived from the 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} - */ - 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] + constructor(sources) { + this.sources = sources.sort((a, b) => this.getPriority(a) < this.getPriority(a)) } - 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} - */ - 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] - } + /** + * Get the list of possible options that are provided to the user + * @returns {Array} + */ + 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} + */ + 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] + } } /** @@ -250,92 +250,88 @@ class RadioStreaming { * @param {Object} options options. Currenly unused * @return {Radio} */ -async function get (siteurl, options) { - let resp = await fetch(getManifestUrl(siteurl)) - let text = await resp.text() +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) + 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) - } + 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) { - if (e instanceof SyntaxError) { true - } else { - console.error('Error', e) - throw e - } } - } + if (resp !== null) { + try { + text = await resp.text() - return manifest -} + 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) + } - - -function parseM3U (body) { - return body.split('\n').filter((line) => { - if (line.startsWith('#')) { - return false - } else { - try { - new URL(line); return true - } catch { - return false - } + 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 } - -module.exports = { - get: get, - objs: { - Radio: Radio, - RadioStreaming: RadioStreaming - }, - parsers: { - M3U: parseM3U, - radioManifest: Radio.fromDOM, - shows: shows.parse, - } +function parseM3U(body) { + return body.split("\n").filter(line => { + if (line.startsWith("#")) { + return false + } else { + try { + new URL(line) + return true + } catch { + return false + } + } + }) +} + +export default { + get: get, + objs: { + Radio: Radio, + RadioStreaming: RadioStreaming + }, + parsers: { + M3U: parseM3U, + radioManifest: Radio.fromDOM, + shows: shows.parse + } }