123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341 |
- 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 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(b),
- );
- }
- /**
- * 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;
- }
- }
- });
- }
- export default {
- get: get,
- objs: {
- Radio: Radio,
- RadioStreaming: RadioStreaming,
- },
- parsers: {
- M3U: parseM3U,
- radioManifest: Radio.fromDOM,
- shows: shows.parse,
- },
- };
|