apply prettier

This commit is contained in:
boyska 2023-09-14 00:07:07 +01:00
parent dd96ce7b2a
commit 4e19b6304e
18 changed files with 902 additions and 852 deletions

1
.prettierignore Normal file
View file

@ -0,0 +1 @@
libs/

View file

@ -13,6 +13,5 @@ npm run test
## Supported spec ## Supported spec
- [radiomanifest](https://radiomanifest.degenerazione.xyz/) - [radiomanifest](https://radiomanifest.degenerazione.xyz/)
- [stream-meta v2](https://www.stream-meta.info/version_2_files.html): name of the radio is extracted from `/streaminfo.json` - [stream-meta v2](https://www.stream-meta.info/version_2_files.html): name of the radio is extracted from `/streaminfo.json`

View file

@ -1,18 +1,18 @@
import ICAL from 'ical.js' import ICAL from "ical.js";
import shows from './shows' import shows from "./shows";
function max(a, b) { function max(a, b) {
if (a<b) return b if (a < b) return b;
return a return a;
} }
/** /**
* Represent a schedule (ie: a .ics file) * Represent a schedule (ie: a .ics file)
*/ */
class RadioSchedule { class RadioSchedule {
constructor (calendar, radio) { constructor(calendar, radio) {
this.calendar = calendar // ICAL.Calendar this.calendar = calendar; // ICAL.Calendar
this.radio = radio // radiomanifest.Radio this.radio = radio; // radiomanifest.Radio
} }
/** /**
@ -21,136 +21,138 @@ class RadioSchedule {
* @returns {Array<external:ICAL~Component>} * @returns {Array<external:ICAL~Component>}
* */ * */
getEvents() { getEvents() {
return this.calendar.getAllSubcomponents('vevent'); return this.calendar.getAllSubcomponents("vevent");
} }
/** /**
* @returns {RadioShow} tries to get a matching show, or create a new one * @returns {RadioShow} tries to get a matching show, or create a new one
*/ */
getShowByEvent(ev) { getShowByEvent(ev) {
if (ev === null) return null if (ev === null) return null;
if (this.radio !== undefined) { if (this.radio !== undefined) {
const showid = RadioSchedule.veventGetShowID(ev) const showid = RadioSchedule.veventGetShowID(ev);
var show = this.radio.getShowByName(showid) var show = this.radio.getShowByName(showid);
if (show === null || show === undefined) { if (show === null || show === undefined) {
return new shows.RadioShow(RadioSchedule.veventGetSummary(ev)) return new shows.RadioShow(RadioSchedule.veventGetSummary(ev));
} }
return show; return show;
} }
return new shows.RadioShow(RadioSchedule.veventGetSummary(ev)) return new shows.RadioShow(RadioSchedule.veventGetSummary(ev));
} }
/** /**
* @returns {external:ICAL~Component} if nothing is going on right now, `null` is returned * @returns {external:ICAL~Component} if nothing is going on right now, `null` is returned
*/ */
getNowEvent(now) { getNowEvent(now) {
var ev_now = this.getEvents().filter(function(vevent) { var ev_now = this.getEvents().filter(function (vevent) {
const ev = new ICAL.Event(vevent) const ev = new ICAL.Event(vevent);
return isNow(ev, now) return isNow(ev, now);
}) });
ev_now.sort((e1, e2) => { return this.veventGetPriority(e1) - this.veventGetPriority(e2) }) ev_now.sort((e1, e2) => {
if(ev_now.length === 0) return this.veventGetPriority(e1) - this.veventGetPriority(e2);
return null });
return ev_now[0] if (ev_now.length === 0) return null;
return ev_now[0];
} }
/** /**
* @returns {RadioShow} if nothing is going on right now, `null` is returned * @returns {RadioShow} if nothing is going on right now, `null` is returned
*/ */
getNowShow(now) { getNowShow(now) {
const ev = this.getNowEvent(now) const ev = this.getNowEvent(now);
return this.getShowByEvent(ev) return this.getShowByEvent(ev);
} }
/** /**
* @returns {NextEvent} if there is none, `null` is returned * @returns {NextEvent} if there is none, `null` is returned
*/ */
getNextEvent(now) { getNextEvent(now) {
var nowEvent = this.getNowEvent(now) var nowEvent = this.getNowEvent(now);
let future_events = this.getEvents().filter((e) => { return e != nowEvent }) let future_events = this.getEvents()
.map((e) => { .filter((e) => {
const vEvent = new ICAL.Event(e) return e != nowEvent;
return {event: e, time: getNext(vEvent, now)}
}) })
.filter((x) => { return x.time !== null && x.time !== undefined }) .map((e) => {
const vEvent = new ICAL.Event(e);
return { event: e, time: getNext(vEvent, now) };
})
.filter((x) => {
return x.time !== null && x.time !== undefined;
});
// since ".sort()" is guaranteed to be stable, we can sort by priority, then by date, so that two events // since ".sort()" is guaranteed to be stable, we can sort by priority, then by date, so that two events
// starting at the same time will be sorted observing priority // starting at the same time will be sorted observing priority
future_events.sort((x1, x2) => this.veventGetPriority(x1.event) - this.veventGetPriority(x2.event)) future_events.sort(
future_events.sort((x, y) => x.time.toUnixTime() - y.time.toUnixTime()) (x1, x2) =>
this.veventGetPriority(x1.event) - this.veventGetPriority(x2.event),
);
future_events.sort((x, y) => x.time.toUnixTime() - y.time.toUnixTime());
if (future_events.length === 0) { if (future_events.length === 0) {
return null return null;
} }
return future_events[0] return future_events[0];
} }
/** /**
* @returns {NextShow} if there is no next show, null will be returned * @returns {NextShow} if there is no next show, null will be returned
*/ */
getNextShow(now) { getNextShow(now) {
const next = this.getNextEvent(now) const next = this.getNextEvent(now);
if (next === null) return null if (next === null) return null;
const ev = next.event const ev = next.event;
const show = this.getShowByEvent(ev) const show = this.getShowByEvent(ev);
return {show: show, time: next.time} return { show: show, time: next.time };
} }
static veventGetSummary(vevent) { static veventGetSummary(vevent) {
return vevent.getFirstProperty('summary').getFirstValue() return vevent.getFirstProperty("summary").getFirstValue();
} }
static veventGetShowID(vevent) { static veventGetShowID(vevent) {
return RadioSchedule.veventGetSummary(vevent) // XXX: X-Show-ID return RadioSchedule.veventGetSummary(vevent); // XXX: X-Show-ID
} }
/** /**
* @return {integer} a normalized version of priority, easier to compare * @return {integer} a normalized version of priority, easier to compare
*/ */
veventGetPriority(ev) { veventGetPriority(ev) {
const prop = ev.getFirstProperty('priority') const prop = ev.getFirstProperty("priority");
let prio; let prio;
if (prop === null) { if (prop === null) {
prio = null prio = null;
} else { } else {
prio = prop.getFirstValue() prio = prop.getFirstValue();
} }
if (prio === null || prio === 0) { if (prio === null || prio === 0) {
prio = 100 prio = 100;
} }
return prio return prio;
} }
} }
function isNow(vEvent, now) { function isNow(vEvent, now) {
if (now === undefined) { if (now === undefined) {
now = ICAL.Time.now() now = ICAL.Time.now();
} }
if (vEvent.isRecurring()) { if (vEvent.isRecurring()) {
return isNowRecurring(vEvent, now) return isNowRecurring(vEvent, now);
} }
return (now < vEvent.endDate) && (now > vEvent.startDate); return now < vEvent.endDate && now > vEvent.startDate;
} }
function isNowRecurring(vEvent, now) { function isNowRecurring(vEvent, now) {
var expand = vEvent.iterator(vEvent.startDate) var expand = vEvent.iterator(vEvent.startDate);
var next, next_end; var next, next_end;
while ((next = expand.next())) { while ((next = expand.next())) {
next_end = next.clone() next_end = next.clone();
next_end.addDuration(vEvent.duration) next_end.addDuration(vEvent.duration);
if (next_end > now) { if (next_end > now) {
break; break;
} }
} }
return (now < next_end && now > next); return now < next_end && now > next;
} }
/* /*
* @private * @private
* @param {external:ICAL~Component} vEvent a _recurring_ vEvent * @param {external:ICAL~Component} vEvent a _recurring_ vEvent
@ -159,16 +161,16 @@ function isNowRecurring(vEvent, now) {
*/ */
function getNext(vEvent, now) { function getNext(vEvent, now) {
if (now === undefined) { if (now === undefined) {
now = ICAL.Time.now() now = ICAL.Time.now();
} }
if (vEvent.isRecurring()) { if (vEvent.isRecurring()) {
return getNextRecurring(vEvent, now) return getNextRecurring(vEvent, now);
} }
if (vEvent.endDate > now) { if (vEvent.endDate > now) {
const val = max(now, vEvent.startDate) const val = max(now, vEvent.startDate);
return val return val;
} }
return null return null;
} }
/* /*
@ -178,37 +180,36 @@ function getNext(vEvent, now) {
* @return {external:ICAL~Time} first future occurrence of this event * @return {external:ICAL~Time} first future occurrence of this event
*/ */
function getNextRecurring(vEvent, now) { function getNextRecurring(vEvent, now) {
var expand = vEvent.iterator(vEvent.startDate) var expand = vEvent.iterator(vEvent.startDate);
var next, next_end; var next, next_end;
while ((next = expand.next())) { while ((next = expand.next())) {
const start = next.clone() const start = next.clone();
next_end = start.clone() next_end = start.clone();
next_end.addDuration(vEvent.duration) next_end.addDuration(vEvent.duration);
if (next_end <= now) { if (next_end <= now) {
continue continue;
} }
return max(start, now) return max(start, now);
} }
return null return null;
} }
async function get(manifest) { async function get(manifest) {
if (manifest.scheduleURL) { if (manifest.scheduleURL) {
let resp = null let resp = null;
try { try {
resp = await fetch(manifest.scheduleURL) resp = await fetch(manifest.scheduleURL);
} catch (e) { } catch (e) {
true true;
} }
if (resp !== null) { if (resp !== null) {
try { try {
const text = await resp.text() const text = await resp.text();
return parse(text) return parse(text);
} catch (e) { } catch (e) {
console.error('Error while parsing schedule', e) console.error("Error while parsing schedule", e);
throw e throw e;
} }
} }
} }
@ -224,15 +225,14 @@ function parse(text) {
var jcalData = ICAL.parse(text); var jcalData = ICAL.parse(text);
var vcalendar = new ICAL.Component(jcalData); var vcalendar = new ICAL.Component(jcalData);
return new RadioSchedule(vcalendar) return new RadioSchedule(vcalendar);
} }
export default { export default {
get: get, get: get,
parse: parse, parse: parse,
RadioSchedule: RadioSchedule, RadioSchedule: RadioSchedule,
} };
/** /**
* @typedef {Object} NextShow * @typedef {Object} NextShow

View file

@ -2,5 +2,6 @@ Radiomanifest is a specification to make webradios express their offer in a stan
library aiming at making the development of expressive webapps a breeze. library aiming at making the development of expressive webapps a breeze.
You are strongly invited to start from: You are strongly invited to start from:
- {@tutorial quickstart} tutorial
- {@link get} this is the first function you'll ever call, in 99% of user scenarios - {@tutorial quickstart} tutorial
- {@link get} this is the first function you'll ever call, in 99% of user scenarios

View file

@ -1,9 +1,9 @@
Using radiomanifest is pretty simple. In an ideal usecase, you can easily do this: Using radiomanifest is pretty simple. In an ideal usecase, you can easily do this:
```javascript ```javascript
const radiomanifest = require('radiomanifest') const radiomanifest = require("radiomanifest");
const radio = radiomanifest.get('http://myradio.com/') const radio = radiomanifest.get("http://myradio.com/");
console.log(radio.getName()) console.log(radio.getName());
``` ```
Now we have `radio`, a {@link Radio} object, which can be seen as the "center" of our data. From here, we can Now we have `radio`, a {@link Radio} object, which can be seen as the "center" of our data. From here, we can
@ -11,13 +11,13 @@ get more data.
## Streaming ## Streaming
The first thing we could want to do is just to *play* the radio. Let's use the {@link RadioStreaming#pickURLs The first thing we could want to do is just to _play_ the radio. Let's use the {@link RadioStreaming#pickURLs
RadioStreaming.pickURLs} method then RadioStreaming.pickURLs} method then
```javascript ```javascript
var streaming = radio.getStreaming() var streaming = radio.getStreaming();
var urls = await streaming.pickURLs() var urls = await streaming.pickURLs();
console.log(urls) console.log(urls);
``` ```
and here we go! This is a list of URLs that the radio is indicating to us as their preferred ones. Why not a and here we go! This is a list of URLs that the radio is indicating to us as their preferred ones. Why not a
@ -30,17 +30,18 @@ Our {@link Radio} keeps track of those, and for each show we can have useful met
for more details. for more details.
```javascript ```javascript
var shows = radio.getShows() var shows = radio.getShows();
console.log(shows.map(s => s.getName())) console.log(shows.map((s) => s.getName()));
``` ```
## Schedule ## Schedule
```javascript ```javascript
const show = radio.getShowAtTime() const show = radio.getShowAtTime();
if (show !== null) { if (show !== null) {
console.log(show.getName()) console.log(show.getName());
} else { } else {
console.log("Nothing special going on right now, sorry") console.log("Nothing special going on right now, sorry");
} }
``` ```
@ -51,4 +52,4 @@ now and {@link RadioSchedule#getNextShow what will go on next}
## Conclusions ## Conclusions
I hope this tutorial helped you get your feet wet. Hopefully, using radiomanifest you'd be able to create I hope this tutorial helped you get your feet wet. Hopefully, using radiomanifest you'd be able to create
great webapps that work on *any* webradio (well, any webradio that supports radiomanifest, at least). great webapps that work on _any_ webradio (well, any webradio that supports radiomanifest, at least).

View file

@ -1,34 +1,30 @@
module.exports = function (config) { module.exports = function (config) {
config.set({ config.set({
frameworks: ['mocha'], frameworks: ["mocha"],
// plugins: ['karma-webpack', 'karma-mocha', 'karma-chai-as-promised'], // plugins: ['karma-webpack', 'karma-mocha', 'karma-chai-as-promised'],
webpack: { webpack: {
// karma watches the test entry points // karma watches the test entry points
// Do NOT specify the entry option // Do NOT specify the entry option
// webpack watches dependencies // webpack watches dependencies
// webpack configuration // webpack configuration
}, },
preprocessors: { preprocessors: {
'test/**/*.js': ['webpack'], "test/**/*.js": ["webpack"],
'radiomanifest.js': ['webpack'] "radiomanifest.js": ["webpack"],
}, },
files: [ files: ["radiomanifest.js", "test/**/*.js"],
'radiomanifest.js', reporters: ["progress"],
'test/**/*.js'
],
reporters: ['progress'],
port: 9876, // karma web server port port: 9876, // karma web server port
colors: true, colors: true,
logLevel: config.LOG_INFO, logLevel: config.LOG_INFO,
browsers: ['ChromeHeadless', 'FirefoxHeadless'], browsers: ["ChromeHeadless", "FirefoxHeadless"],
autoWatch: false, autoWatch: false,
concurrency: Infinity, concurrency: Infinity,
customLaunchers: { customLaunchers: {
FirefoxHeadless: { FirefoxHeadless: {
base: 'Firefox', base: "Firefox",
flags: ['-headless'] flags: ["-headless"],
} },
} },
}) });
} };

View file

@ -1,174 +1,174 @@
import fetch from 'isomorphic-unfetch' import fetch from "isomorphic-unfetch";
import shows from './shows.js' import shows from "./shows.js";
import calendar from './calendar.js' import calendar from "./calendar.js";
function getStreaminfoUrl(siteurl) { function getStreaminfoUrl(siteurl) {
return siteurl + "/streaminfo.json" // XXX: improve this logic return siteurl + "/streaminfo.json"; // XXX: improve this logic
} }
function getManifestUrl(siteurl) { function getManifestUrl(siteurl) {
return siteurl + "/radiomanifest.xml" // XXX: improve this logic return siteurl + "/radiomanifest.xml"; // XXX: improve this logic
} }
function getAttribute(el, attr, default_value) { function getAttribute(el, attr, default_value) {
if (el.hasAttribute(attr)) return el.getAttribute(attr) if (el.hasAttribute(attr)) return el.getAttribute(attr);
return default_value return default_value;
} }
/** /**
* Represents everything we know about a radio. This includes, but is not limited to, radiomanifest.xml. * Represents everything we know about a radio. This includes, but is not limited to, radiomanifest.xml.
*/ */
class Radio { class Radio {
/** /**
* @param {Array} [sources] optional * @param {Array} [sources] optional
* @param {string} [scheduleURL] * @param {string} [scheduleURL]
* @param {string} [showsURL] * @param {string} [showsURL]
* @param {string} [feed] * @param {string} [feed]
*/ */
constructor(sources, scheduleURL, showsURL, feed) { constructor(sources, scheduleURL, showsURL, feed) {
this.streaming = new RadioStreaming(sources) this.streaming = new RadioStreaming(sources);
this.scheduleURL = scheduleURL this.scheduleURL = scheduleURL;
this.showsURL = showsURL this.showsURL = showsURL;
this.feed = feed this.feed = feed;
this.name = "" this.name = "";
this.description = "" this.description = "";
this.logo = null this.logo = null;
this.shows = [] this.shows = [];
this.schedule = null 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);
* @returns {RadioStreaming} const scheduleEl = res.iterateNext();
*/ let scheduleURL = null;
getStreaming() { if (scheduleEl !== null) {
return this.streaming scheduleURL = scheduleEl.getAttribute("src");
} }
setName(name) { res = xml.evaluate("/radio-manifest/shows", xml);
this.name = name const showsEl = res.iterateNext();
let showsURL = null;
if (showsEl !== null) {
showsURL = showsEl.getAttribute("src");
} }
/** res = xml.evaluate("/radio-manifest/feed", xml);
* The radio name, as inferred by stream-meta const feedEl = res.iterateNext();
* let feed = null;
* @returns {string} if (feedEl !== null) {
*/ feed = feedEl.getAttribute("src");
getName() {
return this.name
} }
setDescription(desc) { const manifest = new Radio(sources, scheduleURL, showsURL, feed);
this.description = desc return manifest;
} }
/**
* 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
}
} }
/** /**
@ -176,71 +176,75 @@ class Radio {
* This has probably been derived from the <streaming> block in radiomanifest.xml * This has probably been derived from the <streaming> block in radiomanifest.xml
*/ */
class RadioStreaming { class RadioStreaming {
constructor(sources) { constructor(sources) {
this.sources = sources.sort((a, b) => this.getPriority(a) < this.getPriority(a)) this.sources = sources.sort(
} (a, b) => this.getPriority(a) < this.getPriority(a),
);
}
/** /**
* Get the list of possible options that are provided to the user * Get the list of possible options that are provided to the user
* @returns {Array<string>} * @returns {Array<string>}
*/ */
getOptions() { getOptions() {
return this.sources.map(function (x) { return this.sources.map(function (x) {
return x.getAttribute("name") return x.getAttribute("name");
}) });
} }
/** /**
* @private * @private
*/ */
getPriority(element) { getPriority(element) {
return parseInt(getAttribute(element, "priority", "1")) return parseInt(getAttribute(element, "priority", "1"));
} }
/** /**
* @private * @private
*/ */
getTopPrioritySources() { getTopPrioritySources() {
var topPriority = this.getPriority(this.sources[0]) var topPriority = this.getPriority(this.sources[0]);
return this.sources.filter(src => parseInt(src.getAttribute("priority"), 10) === topPriority) 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) { * @return {string} url of the source. Note that this is much probably a playlist, in M3U format
if (name === undefined) { */
return this.getTopPrioritySources()[0] getSource(name) {
} if (name === undefined) {
const s = this.sources.find(function (x) { return this.getTopPrioritySources()[0];
return x.getAttribute("name") === name
})
if (s === undefined) return s
return s.getAttribute("src")
} }
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. * 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 * They will be picked honoring priorities, and expanding the playlist source
* @return {Array<string>} * @return {Array<string>}
*/ */
async pickURLs() { async pickURLs() {
var allSources = this.getTopPrioritySources() var allSources = this.getTopPrioritySources();
var allAudios = [] var allAudios = [];
for (let src of allSources) { for (let src of allSources) {
let url = src.getAttribute("src") let url = src.getAttribute("src");
let resp = await fetch(url) let resp = await fetch(url);
allAudios.unshift(...parseM3U(await resp.text())) allAudios.unshift(...parseM3U(await resp.text()));
}
return allAudios
} }
return allAudios;
}
/** /**
* Just like {@link RadioStreaming#pickURLs}, but get a single URL * Just like {@link RadioStreaming#pickURLs}, but get a single URL
* @return {string} * @return {string}
*/ */
async pickURL() { async pickURL() {
var allAudios = await this.pickURLs() var allAudios = await this.pickURLs();
return allAudios[0] return allAudios[0];
} }
} }
/** /**
@ -251,87 +255,87 @@ class RadioStreaming {
* @return {Radio} * @return {Radio}
*/ */
async function get(siteurl, options) { async function get(siteurl, options) {
let resp = await fetch(getManifestUrl(siteurl)) let resp = await fetch(getManifestUrl(siteurl));
let text = await resp.text() let text = await resp.text();
const parser = new DOMParser() const parser = new DOMParser();
const dom = parser.parseFromString(text, "text/xml") const dom = parser.parseFromString(text, "text/xml");
const manifest = Radio.fromDOM(dom) 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 { try {
manifest.shows = await shows.get(manifest) 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) { } catch (e) {
console.error("Error while fetching shows file", e) if (e instanceof SyntaxError) {
true;
} else {
console.error("Error", e);
throw e;
}
} }
}
try { return manifest;
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) { function parseM3U(body) {
return body.split("\n").filter(line => { return body.split("\n").filter((line) => {
if (line.startsWith("#")) { if (line.startsWith("#")) {
return false return false;
} else { } else {
try { try {
new URL(line) new URL(line);
return true return true;
} catch { } catch {
return false return false;
} }
} }
}) });
} }
export default { export default {
get: get, get: get,
objs: { objs: {
Radio: Radio, Radio: Radio,
RadioStreaming: RadioStreaming RadioStreaming: RadioStreaming,
}, },
parsers: { parsers: {
M3U: parseM3U, M3U: parseM3U,
radioManifest: Radio.fromDOM, radioManifest: Radio.fromDOM,
shows: shows.parse shows: shows.parse,
} },
} };

132
shows.js
View file

@ -3,30 +3,28 @@
* schedule. * schedule.
*/ */
class RadioShow { class RadioShow {
constructor(name, description, website, feed, schedule, radio_calendar) { constructor(name, description, website, feed, schedule, radio_calendar) {
this.name = name this.name = name;
this.description = description this.description = description;
this.website = website this.website = website;
this.feed = feed this.feed = feed;
this.schedule = schedule this.schedule = schedule;
this.radio_calendar = radio_calendar this.radio_calendar = radio_calendar;
} }
getName() { getName() {
return this.name return this.name;
} }
getWebsite() { getWebsite() {
return this.website return this.website;
} }
getFeed() { getFeed() {
return this.feed return this.feed;
} }
getSchedule() { getSchedule() {
return this.schedule return this.schedule;
} }
} }
/** /**
@ -34,51 +32,98 @@ class RadioShow {
* @return {Array<RadioShow>} * @return {Array<RadioShow>}
*/ */
function parseRadioShows(xml) { function parseRadioShows(xml) {
const doc = xml.cloneNode(true) const doc = xml.cloneNode(true);
const bookmarks = doc.evaluate('//bookmark', doc, showsNamespaceResolver, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null) const bookmarks = doc.evaluate(
const shows = [] "//bookmark",
doc,
showsNamespaceResolver,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
null,
);
const shows = [];
for (let i = 0; i < bookmarks.snapshotLength; i++) { for (let i = 0; i < bookmarks.snapshotLength; i++) {
const bm = bookmarks.snapshotItem(i) const bm = bookmarks.snapshotItem(i);
let name = doc.evaluate('./info/metadata/show:name', bm, showsNamespaceResolver, XPathResult.STRING_TYPE).stringValue let name = doc.evaluate(
if (name === '') { "./info/metadata/show:name",
name = doc.evaluate('./title', bm, showsNamespaceResolver, XPathResult.STRING_TYPE).stringValue bm,
showsNamespaceResolver,
XPathResult.STRING_TYPE,
).stringValue;
if (name === "") {
name = doc.evaluate(
"./title",
bm,
showsNamespaceResolver,
XPathResult.STRING_TYPE,
).stringValue;
} }
let website = doc.evaluate('./info/metadata/show:website', bm, showsNamespaceResolver, XPathResult.STRING_TYPE).stringValue let website = doc.evaluate(
if (website === '') { "./info/metadata/show:website",
website = bm.getAttribute('href') bm,
showsNamespaceResolver,
XPathResult.STRING_TYPE,
).stringValue;
if (website === "") {
website = bm.getAttribute("href");
} }
const feed = doc.evaluate('./info/metadata/show:feed', bm, showsNamespaceResolver, XPathResult.STRING_TYPE).stringValue const feed = doc.evaluate(
const schedule = doc.evaluate('./info/metadata/show:schedule', bm, showsNamespaceResolver, XPathResult.STRING_TYPE).stringValue "./info/metadata/show:feed",
let description = doc.evaluate('./info/metadata/show:description', bm, showsNamespaceResolver, XPathResult.STRING_TYPE).stringValue bm,
if (description === '') { showsNamespaceResolver,
description = doc.evaluate('./desc', bm, showsNamespaceResolver, XPathResult.STRING_TYPE).stringValue XPathResult.STRING_TYPE,
).stringValue;
const schedule = doc.evaluate(
"./info/metadata/show:schedule",
bm,
showsNamespaceResolver,
XPathResult.STRING_TYPE,
).stringValue;
let description = doc.evaluate(
"./info/metadata/show:description",
bm,
showsNamespaceResolver,
XPathResult.STRING_TYPE,
).stringValue;
if (description === "") {
description = doc.evaluate(
"./desc",
bm,
showsNamespaceResolver,
XPathResult.STRING_TYPE,
).stringValue;
} }
const show = new RadioShow(name, description || null, website || null, feed || null, schedule || null) const show = new RadioShow(
shows.push(show) name,
description || null,
website || null,
feed || null,
schedule || null,
);
shows.push(show);
} }
return shows return shows;
} }
async function getShows(manifest) { async function getShows(manifest) {
if (manifest.showsURL) { if (manifest.showsURL) {
let resp = null let resp = null;
try { try {
resp = await fetch(manifest.showsURL) resp = await fetch(manifest.showsURL);
} catch (e) { } catch (e) {
true true;
} }
if (resp !== null) { if (resp !== null) {
try { try {
const text = await resp.text() const text = await resp.text();
const parser = new DOMParser() const parser = new DOMParser();
const showsDom = parser.parseFromString(text, 'text/xml') const showsDom = parser.parseFromString(text, "text/xml");
return parseRadioShows(showsDom) return parseRadioShows(showsDom);
} catch (e) { } catch (e) {
console.error('Error while parsing shows file', e) console.error("Error while parsing shows file", e);
throw e throw e;
} }
} }
} }
@ -86,14 +131,13 @@ async function getShows(manifest) {
function showsNamespaceResolver(prefix) { function showsNamespaceResolver(prefix) {
const prefixes = { const prefixes = {
show: 'https://radiomanifest.degenerazione.xyz/shows/', show: "https://radiomanifest.degenerazione.xyz/shows/",
} };
return prefixes[prefix] || null return prefixes[prefix] || null;
} }
export default { export default {
get: getShows, get: getShows,
parse: parseRadioShows, parse: parseRadioShows,
RadioShow: RadioShow, RadioShow: RadioShow,
} };

View file

@ -1,17 +1,17 @@
const radiomanifest = require('../radiomanifest.js') const radiomanifest = require("../radiomanifest.js");
const chai = require('chai') const chai = require("chai");
chai.use(require('chai-as-promised')) chai.use(require("chai-as-promised"));
const assert = chai.assert const assert = chai.assert;
const expect = chai.expect const expect = chai.expect;
const testName = 'idontexist' const testName = "idontexist";
const url = 'https://example.org/radiomanifest/examples/' + testName + '/' const url = "https://example.org/radiomanifest/examples/" + testName + "/";
describe('radiomanifest.js supports example ' + testName, () => { describe("radiomanifest.js supports example " + testName, () => {
describe('Get empty radiomanifest', () => { describe("Get empty radiomanifest", () => {
it('should throw', () => { it("should throw", () => {
const p = radiomanifest.get(url) const p = radiomanifest.get(url);
expect(p).to.be.rejected expect(p).to.be.rejected;
}) });
}) });
}) });

View file

@ -1,26 +1,29 @@
const radiomanifest = require('../radiomanifest.js') const radiomanifest = require("../radiomanifest.js");
const chai = require('chai') const chai = require("chai");
chai.use(require('chai-as-promised')) chai.use(require("chai-as-promised"));
const assert = chai.assert const assert = chai.assert;
const expect = chai.expect const expect = chai.expect;
const tests = ['empty', 'empty-no-streaminfo', 'empty-invalid-streaminfo'] const tests = ["empty", "empty-no-streaminfo", "empty-invalid-streaminfo"];
for (const exampleName of tests) { for (const exampleName of tests) {
let url = 'https://radiomanifest.degenerazione.xyz/v0.2/examples/' + exampleName + '/' let url =
"https://radiomanifest.degenerazione.xyz/v0.2/examples/" +
exampleName +
"/";
describe('examples/' + exampleName, () => { describe("examples/" + exampleName, () => {
describe('Get radiomanifest', () => { describe("Get radiomanifest", () => {
it('should return a Promise', () => { it("should return a Promise", () => {
const p = radiomanifest.get(url) const p = radiomanifest.get(url);
expect(p instanceof Promise).to.be.eql(true) expect(p instanceof Promise).to.be.eql(true);
}) });
}) });
describe('streaming', () => { describe("streaming", () => {
it('shoud return no streaming option', async () => { it("shoud return no streaming option", async () => {
const p = await radiomanifest.get(url) const p = await radiomanifest.get(url);
expect(p.getStreaming().getOptions().length).to.be.equal(0) expect(p.getStreaming().getOptions().length).to.be.equal(0);
}) });
}) });
}) });
} }

View file

@ -1,107 +1,104 @@
const ICAL = require('ical.js') const ICAL = require("ical.js");
const radiomanifest = require('../radiomanifest.js') const radiomanifest = require("../radiomanifest.js");
const chai = require('chai') const chai = require("chai");
chai.use(require('chai-as-promised')) chai.use(require("chai-as-promised"));
const assert = chai.assert const assert = chai.assert;
const exampleName = 'full-ondarossa' const exampleName = "full-ondarossa";
const expect = chai.expect const expect = chai.expect;
const url = 'https://radiomanifest.degenerazione.xyz/v0.2/examples/' + exampleName + '/' const url =
"https://radiomanifest.degenerazione.xyz/v0.2/examples/" + exampleName + "/";
const testShowName = 'Entropia Massima' const testShowName = "Entropia Massima";
const testWebsite = "http://www.ondarossa.info/trx/entropia-massima" const testWebsite = "http://www.ondarossa.info/trx/entropia-massima";
const testFeed = 'http://www.ondarossa.info/podcast/by-trx-id/10497/podcast.xml' const testFeed =
"http://www.ondarossa.info/podcast/by-trx-id/10497/podcast.xml";
describe('examples/' + exampleName, () => { describe("examples/" + exampleName, () => {
describe('shows', () => { describe("shows", () => {
it('shoud find many shows', async () => { it("shoud find many shows", async () => {
const rm = await radiomanifest.get(url) const rm = await radiomanifest.get(url);
assert.isAbove(rm.getShows().length, 1) assert.isAbove(rm.getShows().length, 1);
}) });
it('one of which is called "Entropia Massima"', async () => { it('one of which is called "Entropia Massima"', async () => {
const rm = await radiomanifest.get(url) const rm = await radiomanifest.get(url);
const show = rm.getShowByName(testShowName) const show = rm.getShowByName(testShowName);
assert.equal(show.getName(), testShowName) assert.equal(show.getName(), testShowName);
assert.equal(show.getWebsite(), testWebsite) assert.equal(show.getWebsite(), testWebsite);
assert.equal(show.getSchedule(), null) assert.equal(show.getSchedule(), null);
}) });
}) });
describe('schedule', () => { describe("schedule", () => {
it('should find many event', async () => { it("should find many event", async () => {
const rm = await radiomanifest.get(url) const rm = await radiomanifest.get(url);
assert.isAbove(rm.getSchedule().getEvents().length, 1) assert.isAbove(rm.getSchedule().getEvents().length, 1);
}) });
it('At 1AM, nothing is going on', async () => { it("At 1AM, nothing is going on", async () => {
const rm = await radiomanifest.get(url) const rm = await radiomanifest.get(url);
const rs = rm.getSchedule() const rs = rm.getSchedule();
const now = new ICAL.Time({ const now = new ICAL.Time({
year: 2022, year: 2022,
month: 1, month: 1,
day: 31, day: 31,
hour: 1, hour: 1,
minute: 0, minute: 0,
second: 0, second: 0,
isDate: false isDate: false,
}); });
const ev = rs.getNowEvent(now) const ev = rs.getNowEvent(now);
assert.equal(ev, null) assert.equal(ev, null);
}) });
it('monday at 8PM, "Entropia Massima" is going on', async () => { it('monday at 8PM, "Entropia Massima" is going on', async () => {
const rm = await radiomanifest.get(url) const rm = await radiomanifest.get(url);
const rs = rm.getSchedule() const rs = rm.getSchedule();
const now = new ICAL.Time({ const now = new ICAL.Time({
year: 2022, year: 2022,
month: 1, month: 1,
day: 31, day: 31,
hour: 20, hour: 20,
minute: 10, minute: 10,
second: 0, second: 0,
isDate: false isDate: false,
}, });
); const vevent = rs.getNowEvent(now);
const vevent = rs.getNowEvent(now) assert.notEqual(vevent, null);
assert.notEqual(vevent, null) const show = rs.getNowShow(now);
const show = rs.getNowShow(now) assert.notEqual(show, null);
assert.notEqual(show, null) assert.equal(show.getName(), testShowName);
assert.equal(show.getName(), testShowName)
assert.equal(show.getFeed(), testFeed) assert.equal(show.getFeed(), testFeed);
}) });
it('monday at 7PM, "Entropia Massima" is next', async () => { it('monday at 7PM, "Entropia Massima" is next', async () => {
const rm = await radiomanifest.get(url) const rm = await radiomanifest.get(url);
const rs = rm.getSchedule() const rs = rm.getSchedule();
const now = new ICAL.Time({ const now = new ICAL.Time({
year: 2022, year: 2022,
month: 1, month: 1,
day: 31, day: 31,
hour: 19, hour: 19,
minute: 10, minute: 10,
second: 0, second: 0,
isDate: false isDate: false,
}, });
); const vevent = rs.getNowEvent(now);
const vevent = rs.getNowEvent(now) assert.notEqual(vevent, null);
assert.notEqual(vevent, null) const show = rs.getNowShow(now);
const show = rs.getNowShow(now) assert.notEqual(show, null);
assert.notEqual(show, null) assert.equal(show.getName(), "Baraonda");
assert.equal(show.getName(), 'Baraonda')
const next_event = rs.getNextEvent(now)
assert.notEqual(next_event, null)
assert.notEqual(next_event.event, null)
const next_show = rs.getNextShow(now)
assert.notEqual(next_show, null)
assert.notEqual(next_show.show, null)
assert.equal(next_show.show.getName(), testShowName)
assert.equal(next_show.show.getFeed(), testFeed)
})
})
})
const next_event = rs.getNextEvent(now);
assert.notEqual(next_event, null);
assert.notEqual(next_event.event, null);
const next_show = rs.getNextShow(now);
assert.notEqual(next_show, null);
assert.notEqual(next_show.show, null);
assert.equal(next_show.show.getName(), testShowName);
assert.equal(next_show.show.getFeed(), testFeed);
});
});
});

View file

@ -1,79 +1,75 @@
const ICAL = require('ical.js') const ICAL = require("ical.js");
const radiomanifest = require('../radiomanifest.js') const radiomanifest = require("../radiomanifest.js");
const chai = require('chai') const chai = require("chai");
chai.use(require('chai-as-promised')) chai.use(require("chai-as-promised"));
const assert = chai.assert const assert = chai.assert;
const exampleName = 'full-spore' const exampleName = "full-spore";
const expect = chai.expect const expect = chai.expect;
const url = 'https://radiomanifest.degenerazione.xyz/v0.2/examples/' + exampleName + '/' const url =
"https://radiomanifest.degenerazione.xyz/v0.2/examples/" + exampleName + "/";
const testShowName = 'scaricomerci' const testShowName = "scaricomerci";
const testNextShowName = 'nastrone notte' const testNextShowName = "nastrone notte";
describe('examples/' + exampleName, () => { describe("examples/" + exampleName, () => {
describe('schedule.getNow', () => { describe("schedule.getNow", () => {
it('observes priority correctly', async () => { it("observes priority correctly", async () => {
// tuesday, half past midnight // tuesday, half past midnight
const rm = await radiomanifest.get(url) const rm = await radiomanifest.get(url);
const rs = rm.getSchedule() const rs = rm.getSchedule();
const now = new ICAL.Time({ const now = new ICAL.Time(
year: 2022, {
month: 1, year: 2022,
day: 30, month: 1,
hour: 2, day: 30,
minute: 20, hour: 2,
second: 0, minute: 20,
isDate: false second: 0,
isDate: false,
}, },
ICAL.Timezone.utcTimezone ICAL.Timezone.utcTimezone,
); );
const vevent = rs.getNowEvent(now) const vevent = rs.getNowEvent(now);
assert.notEqual(vevent, null) assert.notEqual(vevent, null);
const show = rs.getNowShow(now) const show = rs.getNowShow(now);
assert.notEqual(show, null) assert.notEqual(show, null);
assert.equal(show.getName(), testShowName) assert.equal(show.getName(), testShowName);
}) });
}) });
describe('schedule.getNext', () => { describe("schedule.getNext", () => {
it("getNext observes priority correctly", async () => {
it('getNext observes priority correctly', async () => {
// tuesday, half past midnight // tuesday, half past midnight
const rm = await radiomanifest.get(url) const rm = await radiomanifest.get(url);
const rs = rm.getSchedule() const rs = rm.getSchedule();
const now = new ICAL.Time({ const now = new ICAL.Time({
year: 2022, year: 2022,
month: 2, month: 2,
day: 2, day: 2,
hour: 2, hour: 2,
minute: 20, minute: 20,
second: 0, second: 0,
isDate: false isDate: false,
}, });
); const vevent = rs.getNowEvent(now);
const vevent = rs.getNowEvent(now) assert.notEqual(vevent, null);
assert.notEqual(vevent, null) const show = rs.getNowShow(now);
const show = rs.getNowShow(now) assert.notEqual(show, null);
assert.notEqual(show, null) assert.equal(show.getName(), testShowName);
assert.equal(show.getName(), testShowName)
const next_event = rs.getNextEvent(now)
assert.notEqual(next_event, null)
assert.isObject(next_event.event)
assert.isObject(next_event.time)
const next_show = rs.getNextShow(now)
assert.isObject(next_show)
assert.isObject(next_show.show)
assert.equal(next_show.show.getName(), testNextShowName)
const time = next_event.time.toJSDate()
assert.equal(time.getHours(), 2)
assert.equal(time.getMinutes(), 58)
})
})
})
const next_event = rs.getNextEvent(now);
assert.notEqual(next_event, null);
assert.isObject(next_event.event);
assert.isObject(next_event.time);
const next_show = rs.getNextShow(now);
assert.isObject(next_show);
assert.isObject(next_show.show);
assert.equal(next_show.show.getName(), testNextShowName);
const time = next_event.time.toJSDate();
assert.equal(time.getHours(), 2);
assert.equal(time.getMinutes(), 58);
});
});
});

View file

@ -1,60 +1,59 @@
const ICAL = require('ical.js') const ICAL = require("ical.js");
const radiomanifest = require('../radiomanifest.js') const radiomanifest = require("../radiomanifest.js");
const chai = require('chai') const chai = require("chai");
chai.use(require('chai-as-promised')) chai.use(require("chai-as-promised"));
const assert = chai.assert const assert = chai.assert;
const exampleName = 'onlyics' const exampleName = "onlyics";
const expect = chai.expect const expect = chai.expect;
const url = 'https://radiomanifest.degenerazione.xyz/v0.2/examples/' + exampleName + '/' const url =
"https://radiomanifest.degenerazione.xyz/v0.2/examples/" + exampleName + "/";
describe('examples/' + exampleName, () => { describe("examples/" + exampleName, () => {
describe('schedule', () => { describe("schedule", () => {
it('should find one event', async () => { it("should find one event", async () => {
const rm = await radiomanifest.get(url) const rm = await radiomanifest.get(url);
assert.equal(rm.getSchedule().getEvents().length, 1) assert.equal(rm.getSchedule().getEvents().length, 1);
}) });
it('with a specific name', async () => { it("with a specific name", async () => {
const rm = await radiomanifest.get(url) const rm = await radiomanifest.get(url);
const ev = rm.getSchedule().getEvents()[0] const ev = rm.getSchedule().getEvents()[0];
const summary = ev.getFirstProperty('summary').getFirstValue() const summary = ev.getFirstProperty("summary").getFirstValue();
assert.equal(summary, 'JavaScript show') assert.equal(summary, "JavaScript show");
}) });
it('happens every monday at 6AM', async () => { it("happens every monday at 6AM", async () => {
const rm = await radiomanifest.get(url) const rm = await radiomanifest.get(url);
const rs = rm.getSchedule() const rs = rm.getSchedule();
const now = new ICAL.Time({ const now = new ICAL.Time({
year: 2022, year: 2022,
month: 1, month: 1,
day: 3, day: 3,
hour: 6, hour: 6,
minute: 30, minute: 30,
second: 0, second: 0,
isDate: false isDate: false,
}); });
const show = rs.getNowShow(now) const show = rs.getNowShow(now);
assert.notEqual(show, null) assert.notEqual(show, null);
assert.equal(show.getName(), 'JavaScript show') assert.equal(show.getName(), "JavaScript show");
}) });
it('doesnt happen at any other time than 6AM', async () => { it("doesnt happen at any other time than 6AM", async () => {
const rm = await radiomanifest.get(url) const rm = await radiomanifest.get(url);
const rs = rm.getSchedule() const rs = rm.getSchedule();
const now = new ICAL.Time({ const now = new ICAL.Time({
year: 2022, year: 2022,
month: 1, month: 1,
day: 3, day: 3,
hour: 9, hour: 9,
minute: 0, minute: 0,
second: 0, second: 0,
isDate: false isDate: false,
}); });
const ev = rs.getNowEvent(now) const ev = rs.getNowEvent(now);
assert.equal(ev, null) assert.equal(ev, null);
}) });
}) });
}) });

View file

@ -1,23 +1,27 @@
const radiomanifest = require('../radiomanifest.js') const radiomanifest = require("../radiomanifest.js");
const chai = require('chai') const chai = require("chai");
chai.use(require('chai-as-promised')) chai.use(require("chai-as-promised"));
const assert = chai.assert const assert = chai.assert;
const exampleName = 'source404' const exampleName = "source404";
const expect = chai.expect const expect = chai.expect;
const url = 'https://radiomanifest.degenerazione.xyz/v0.2/examples/' + exampleName + '/' const url =
"https://radiomanifest.degenerazione.xyz/v0.2/examples/" + exampleName + "/";
describe('examples/' + exampleName, () => { describe("examples/" + exampleName, () => {
describe('streaming', () => { describe("streaming", () => {
it('shoud return one streaming option', async () => { it("shoud return one streaming option", async () => {
const p = await radiomanifest.get(url) const p = await radiomanifest.get(url);
assert.equal(p.getStreaming().getOptions().length, 1) assert.equal(p.getStreaming().getOptions().length, 1);
}) });
it('... whose url is stream.m3u', async () => { it("... whose url is stream.m3u", async () => {
const p = await radiomanifest.get(url) const p = await radiomanifest.get(url);
assert.equal(p.getStreaming().getOptions()[0], 'try to find me') assert.equal(p.getStreaming().getOptions()[0], "try to find me");
const name = p.getStreaming().getOptions()[0] const name = p.getStreaming().getOptions()[0];
assert.equal(p.getStreaming().getSource(name), 'https://www.radioexample.org/stream.m3u') assert.equal(
}) p.getStreaming().getSource(name),
}) "https://www.radioexample.org/stream.m3u",
}) );
});
});
});

View file

@ -1,53 +1,52 @@
const parseM3U = require('../radiomanifest.js').parsers.M3U const parseM3U = require("../radiomanifest.js").parsers.M3U;
const chai = require('chai') const chai = require("chai");
chai.use(require('chai-as-promised')) chai.use(require("chai-as-promised"));
const assert = chai.assert const assert = chai.assert;
const expect = chai.expect const expect = chai.expect;
describe('parseM3U parses basic M3U', () => {
describe('empty M3U', () => {
it('should return empty list', () => {
var r = parseM3U("")
assert.equal(r.length, 0)
})
it('should discard empty lines', () => {
var r = parseM3U("\n\n\n")
assert.equal(r.length, 0)
})
})
describe('just some lines', () => {
it('should return appropriate list', () => {
var r = parseM3U("http://foo")
assert.equal(r.length, 1)
assert.equal(r[0], "http://foo")
})
it('should work with longer list', () => {
var r = parseM3U("http://foo\nhttp://baz\nhttp://bar\n")
assert.equal(r.length, 3)
assert.equal(r[0], "http://foo")
assert.equal(r[1], "http://baz")
assert.equal(r[2], "http://bar")
})
it('should discard empty lines', () => {
var r = parseM3U("http://foo\n\nhttp://baz\nhttp://bar\n\n\n")
assert.equal(r.length, 3)
assert.equal(r[0], "http://foo")
assert.equal(r[1], "http://baz")
assert.equal(r[2], "http://bar")
})
})
describe('comments', () => {
it('comments should be ignored', () => {
var r = parseM3U("http://foo\n#asd")
assert.equal(r.length, 1)
assert.equal(r[0], "http://foo")
})
it('mid-line hash is not a comment', () => {
var r = parseM3U("http://foo#asd")
assert.equal(r.length, 1)
assert.equal(r[0], "http://foo#asd")
})
})
})
describe("parseM3U parses basic M3U", () => {
describe("empty M3U", () => {
it("should return empty list", () => {
var r = parseM3U("");
assert.equal(r.length, 0);
});
it("should discard empty lines", () => {
var r = parseM3U("\n\n\n");
assert.equal(r.length, 0);
});
});
describe("just some lines", () => {
it("should return appropriate list", () => {
var r = parseM3U("http://foo");
assert.equal(r.length, 1);
assert.equal(r[0], "http://foo");
});
it("should work with longer list", () => {
var r = parseM3U("http://foo\nhttp://baz\nhttp://bar\n");
assert.equal(r.length, 3);
assert.equal(r[0], "http://foo");
assert.equal(r[1], "http://baz");
assert.equal(r[2], "http://bar");
});
it("should discard empty lines", () => {
var r = parseM3U("http://foo\n\nhttp://baz\nhttp://bar\n\n\n");
assert.equal(r.length, 3);
assert.equal(r[0], "http://foo");
assert.equal(r[1], "http://baz");
assert.equal(r[2], "http://bar");
});
});
describe("comments", () => {
it("comments should be ignored", () => {
var r = parseM3U("http://foo\n#asd");
assert.equal(r.length, 1);
assert.equal(r[0], "http://foo");
});
it("mid-line hash is not a comment", () => {
var r = parseM3U("http://foo#asd");
assert.equal(r.length, 1);
assert.equal(r[0], "http://foo#asd");
});
});
});

View file

@ -1,25 +1,23 @@
const radiomanifest = require('../radiomanifest.js') const radiomanifest = require("../radiomanifest.js");
const chai = require('chai') const chai = require("chai");
chai.use(require('chai-as-promised')) chai.use(require("chai-as-promised"));
const expect = chai.expect const expect = chai.expect;
describe('radiomanifest.js', () => { describe("radiomanifest.js", () => {
describe('Get a radiomanifest', () => { describe("Get a radiomanifest", () => {
it('should return a Promise', () => { it("should return a Promise", () => {
const p = radiomanifest.get('http://example.com/') const p = radiomanifest.get("http://example.com/");
expect(p instanceof Promise).to.be.eql(true) expect(p instanceof Promise).to.be.eql(true);
}) });
it('should reject on invalid URL', () => { it("should reject on invalid URL", () => {
const p = radiomanifest.get('http://example.com/') const p = radiomanifest.get("http://example.com/");
expect(p).to.eventually.be.rejected expect(p).to.eventually.be.rejected;
}) });
}) });
describe('streaming', () => { describe("streaming", () => {
it('shoud return a valid streaming URL', () => { it("shoud return a valid streaming URL", () => {});
});
}) });
})
})

92
ui.js
View file

@ -1,55 +1,65 @@
// const radiomanifest = require('radiomanifest.js') // const radiomanifest = require('radiomanifest.js')
function updateNow(radio) { function updateNow(radio) {
var box = document.querySelector('#now-info') var box = document.querySelector("#now-info");
const show = radio.getSchedule().getNowShow() const show = radio.getSchedule().getNowShow();
try { try {
var showText = show.getName() + '\nfeed: ' + show.getWebsite() var showText = show.getName() + "\nfeed: " + show.getWebsite();
} catch (e) { } catch (e) {
var showText = String(show) var showText = String(show);
} }
var text = radio.getName() + ' - ' + radio.getDescription() + ' -- ' + showText var text =
box.textContent = text radio.getName() + " - " + radio.getDescription() + " -- " + showText;
box.textContent = text;
} }
async function fai () { async function fai() {
const radio = await radiomanifest.get('https://radiomanifest.degenerazione.xyz/v0.2/examples/full-ondarossa') const radio = await radiomanifest.get(
console.log('radio?', radio) "https://radiomanifest.degenerazione.xyz/v0.2/examples/full-ondarossa",
const s = radio.getStreaming() );
console.log(s.sources) console.log("radio?", radio);
console.log(s.getOptions()) const s = radio.getStreaming();
console.log(s.getSource(s.getOptions()[0])) console.log(s.sources);
console.log(s.getSource()) console.log(s.getOptions());
console.log(s.getSource(s.getOptions()[0]));
console.log(s.getSource());
var audioEl = document.querySelector('#player audio') var audioEl = document.querySelector("#player audio");
var urls = await s.pickURLs() var urls = await s.pickURLs();
console.log('audios', urls) console.log("audios", urls);
urls.forEach( function(url) { urls.forEach(function (url) {
var srcEl = document.createElement("source") var srcEl = document.createElement("source");
srcEl.setAttribute('src', url) srcEl.setAttribute("src", url);
console.log('src', srcEl, url) console.log("src", srcEl, url);
audioEl.appendChild(srcEl) audioEl.appendChild(srcEl);
}) });
const showList = document.querySelector('#shows > ul') const showList = document.querySelector("#shows > ul");
for (const show of radio.getShows()) { for (const show of radio.getShows()) {
const item = document.createElement('li') const item = document.createElement("li");
const link = document.createElement('a') const link = document.createElement("a");
link.dataset['show'] = show.getName() link.dataset["show"] = show.getName();
link.textContent = show.getName() link.textContent = show.getName();
link.setAttribute('href', show.getWebsite()) link.setAttribute("href", show.getWebsite());
item.appendChild(link) item.appendChild(link);
showList.appendChild(item) showList.appendChild(item);
} }
showList.addEventListener('mouseenter', function (evt) { showList.addEventListener(
if (evt.target.dataset['show'] === undefined) "mouseenter",
return; function (evt) {
const info = document.querySelector('#show-info') if (evt.target.dataset["show"] === undefined) return;
info.textContent = radio.getShowByName(evt.target.dataset['show']).getFeed() const info = document.querySelector("#show-info");
}, true) info.textContent = radio
.getShowByName(evt.target.dataset["show"])
.getFeed();
},
true,
);
updateNow(radio) updateNow(radio);
console.log(radio.getSchedule()) console.log(radio.getSchedule());
setInterval(function() { updateNow(radio) }, 2000) setInterval(function () {
updateNow(radio);
}, 2000);
} }
fai() fai();

View file

@ -1,27 +1,25 @@
const path = require('path') const path = require("path");
const variants = [ const variants = [
{ name: 'web', libtype: 'amd', target: 'web' }, { name: "web", libtype: "amd", target: "web" },
{ name: 'node', libtype: 'amd', target: 'node' }, { name: "node", libtype: "amd", target: "node" },
{ name: 'oldstyle', libtype: 'umd', target: 'web' } { name: "oldstyle", libtype: "umd", target: "web" },
] ];
module.exports = variants.map( module.exports = variants.map((variant) => {
variant => { const obj = {
const obj = { name: variant.name,
name: variant.name, mode: "production",
mode: 'production', entry: "./radiomanifest.js",
entry: './radiomanifest.js', target: [variant.target],
target: [variant.target], output: {
output: { path: path.resolve(__dirname, "dist"),
path: path.resolve(__dirname, 'dist'), filename: "radiomanifest-" + variant.name + ".bundle.js",
filename: 'radiomanifest-' + variant.name + '.bundle.js', clean: false,
clean: false, library: {
library: { name: "radiomanifest",
name: 'radiomanifest', type: variant.libtype,
type: variant.libtype },
} },
} };
} return obj;
return obj });
}
)