revamp continued. still WIP
This commit is contained in:
-434
@@ -1,434 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
var { Writable, Duplex } = require("stream");
|
||||
var { log } = require("./log.js")("DlF");
|
||||
var cp = require("child_process");
|
||||
var fs = require("fs");
|
||||
var { StreamDl } = require("adblockradio-dl"); // TODO publish source ?
|
||||
const Metadata = require("webradio-metadata");
|
||||
//var { getMeta, setLog } = require("webradio-metadata");
|
||||
|
||||
Metadata.setLog(require("./log.js")("meta"));
|
||||
|
||||
class Db {
|
||||
constructor(options) {
|
||||
this.country = options.country;
|
||||
this.name = options.name;
|
||||
this.path = options.path;
|
||||
this.ext = options.ext;
|
||||
this.audioCache = new AudioCache({ bitrate: options.bitrate, cacheLen: options.cacheLen });
|
||||
this.metaCache = new MetaCache({ cacheLen: options.cacheLen });
|
||||
}
|
||||
|
||||
dirDate(now) {
|
||||
return (now.getUTCFullYear()) + "-" + (now.getUTCMonth()+1 < 10 ? "0" : "") + (now.getUTCMonth()+1) + "-" + (now.getUTCDate() < 10 ? "0" : "") + (now.getUTCDate());
|
||||
}
|
||||
|
||||
newAudioSegment() {
|
||||
var now = new Date();
|
||||
var dir = this.path + "/records/" + this.dirDate(now) + "/" + this.country + "_" + this.name + "/todo/";
|
||||
var path = dir + now.toISOString();
|
||||
//log.debug("newAudioSegment: path=" + path);
|
||||
var self = this;
|
||||
try {
|
||||
cp.execSync("mkdir -p \"" + dir + "\"");
|
||||
} catch(e) {
|
||||
log.error("warning, could not create path " + path + " e=" + e);
|
||||
}
|
||||
|
||||
return {
|
||||
audio: new AudioWriteStream(path + "." + self.ext), //new fs.createWriteStream(path + "." + self.ext), //
|
||||
metadata: new MetaWriteStream(path + ".json"),
|
||||
date: now,
|
||||
path: path
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class AudioWriteStream extends Duplex {
|
||||
constructor(path) {
|
||||
super();
|
||||
this.path = path;
|
||||
this.file = new fs.createWriteStream(path + ".part");
|
||||
//this.buffer = null;
|
||||
}
|
||||
|
||||
_write(data, enc, next) {
|
||||
this.file.write(data);
|
||||
//this.buffer = this.buffer ? Buffer.concat([ this.buffer, data ]): data;
|
||||
this.push(data);
|
||||
next();
|
||||
}
|
||||
|
||||
_read() {
|
||||
//this.push(this.buffer);
|
||||
//this.buffer = null;
|
||||
}
|
||||
|
||||
_final(next) {
|
||||
var self = this;
|
||||
this.push(null);
|
||||
this.file.end(function() {
|
||||
fs.rename(self.path + ".part", self.path, function(err) {
|
||||
if (err) {
|
||||
log.error("AudioWriteStream: err=" + err);
|
||||
}
|
||||
next();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class AudioCache extends Writable {
|
||||
constructor(options) {
|
||||
super();
|
||||
this.cacheLen = options.cacheLen;
|
||||
this.bitrate = options.bitrate;
|
||||
this.bitrateValidated = false;
|
||||
this.flushAmount = 60 * this.bitrate;
|
||||
this.readCursor = null;
|
||||
this.buffer = Buffer.allocUnsafe(this.cacheLen * this.bitrate + 2*this.flushAmount).fill(0);
|
||||
this.writeCursor = 0;
|
||||
}
|
||||
|
||||
_write(data, enc, next) {
|
||||
if (this.writeCursor + data.length > this.buffer.length) {
|
||||
log.warn("AudioCache: _write: buffer overflow wC=" + this.writeCursor + " dL=" + data.length + " bL=" + this.buffer.length);
|
||||
}
|
||||
data.copy(this.buffer, this.writeCursor);
|
||||
this.writeCursor += data.length;
|
||||
|
||||
//log.debug("AudioCache: _write: add " + data.length + " to buffer, new len=" + this.buffer.length);
|
||||
if (this.writeCursor >= this.flushAmount && !this.bitrateValidated) {
|
||||
var self = this;
|
||||
this.evalBitrate(this.buffer, function(bitrate) {
|
||||
if (!isNaN(bitrate) && bitrate > 0 && self.bitrate != bitrate) {
|
||||
log.info("AudioCache: bitrate adjusted from " + self.bitrate + "bps to " + bitrate + "bps");
|
||||
|
||||
// if bitrate is higher than expected, expand the buffer accordingly.
|
||||
if (bitrate > self.bitrate) {
|
||||
var expandBuf = Buffer.allocUnsafe(self.cacheLen * (bitrate - self.bitrate)).fill(0);
|
||||
log.info("AudioCache: buffer expanded from " + self.buffer.length + " to " + (self.buffer.length + expandBuf.length) + " bytes");
|
||||
self.buffer = Buffer.concat([ self.buffer, expandBuf ]);
|
||||
}
|
||||
self.bitrate = bitrate;
|
||||
}
|
||||
});
|
||||
this.bitrateValidated = true;
|
||||
}
|
||||
|
||||
if (this.writeCursor >= this.cacheLen * this.bitrate + this.flushAmount) {
|
||||
//log.debug("AudioCache: _write: cutting buffer at len = " + this.cacheLen * this.bitrate);
|
||||
this.buffer.copy(this.buffer, 0, this.flushAmount);
|
||||
this.writeCursor -= this.flushAmount;
|
||||
|
||||
if (this.readCursor) {
|
||||
this.readCursor -= this.flushAmount;
|
||||
if (this.readCursor <= 0) this.readCursor = null;
|
||||
}
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
readLast(secondsFromEnd, duration) {
|
||||
var l = this.writeCursor; //this.buffer.length;
|
||||
if (secondsFromEnd < 0 || duration < 0) {
|
||||
log.error("AudioCache: readLast: negative secondsFromEnd or duration");
|
||||
return null;
|
||||
} else if (duration > secondsFromEnd) {
|
||||
log.error("AudioCache: readLast: duration=" + duration + " higher than secondsFromEnd=" + secondsFromEnd);
|
||||
return null;
|
||||
} else if (secondsFromEnd * this.bitrate >= l) {
|
||||
log.error("AudioCache: readLast: attempted to read " + secondsFromEnd + " seconds (" + secondsFromEnd * this.bitrate + " b) while bufferLen=" + l);
|
||||
return null;
|
||||
}
|
||||
var data;
|
||||
if (duration) {
|
||||
data = this.buffer.slice(l - secondsFromEnd * this.bitrate, l - (secondsFromEnd-duration) * this.bitrate);
|
||||
this.readCursor = l - (secondsFromEnd-duration) * this.bitrate;
|
||||
} else {
|
||||
data = this.buffer.slice(l - secondsFromEnd * this.bitrate);
|
||||
this.readCursor = l;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
readAmountAfterCursor(duration) {
|
||||
var nextCursor = this.readCursor + duration * this.bitrate;
|
||||
if (duration < 0) {
|
||||
log.error("AudioCache: readAmountAfterCursor: negative duration");
|
||||
return null;
|
||||
} else if (nextCursor >= this.writeCursor) {
|
||||
log.warn("AudioCache: readAmountAfterCursor: will read until " + this.writeCursor + " instead of " + nextCursor);
|
||||
}
|
||||
nextCursor = Math.min(this.writeCursor, nextCursor);
|
||||
var data = this.buffer.slice(this.readCursor, nextCursor);
|
||||
this.readCursor = nextCursor;
|
||||
return data;
|
||||
}
|
||||
|
||||
getAvailableCache() {
|
||||
return this.buffer ? this.writeCursor / this.bitrate : 0;
|
||||
}
|
||||
|
||||
evalBitrate(buffer, callback) {
|
||||
var tmpPath = "/tmp/" + Math.floor(Math.random() * 1000000000);
|
||||
fs.writeFile(tmpPath, buffer, function(err) {
|
||||
if (err) {
|
||||
log.warn("evalBitrate: could not write temp file. err=" + err);
|
||||
return callback(null);
|
||||
}
|
||||
cp.exec("ffmpeg -i 'file:" + tmpPath + "' 2>&1 | grep bitrate", function(error, stdout, stderr) {
|
||||
//log.debug("evalBitrate: stdout: " + stdout + ", stderr: " + stderr + ", error: " + error);
|
||||
var indexStartBitrate = stdout.indexOf("bitrate:") + 9;
|
||||
var output = stdout.slice(indexStartBitrate, stdout.length-1);
|
||||
//log.debug("evalBitrate: output1: " + output);
|
||||
var indexStopBitrate = output.indexOf("kb/s") - 1;
|
||||
output = output.slice(0, indexStopBitrate);
|
||||
//log.debug("evalBitrate: output2: ||" + output + "||");
|
||||
var ffmpegBitrate = 1000 * parseInt(output) / 8;
|
||||
//log.debug("evalBitrate: bitrate: " + ffmpegBitrate);
|
||||
return callback(ffmpegBitrate);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class MetaWriteStream extends Writable {
|
||||
constructor(path) {
|
||||
super({ objectMode: true });
|
||||
this.file = new fs.createWriteStream(path);
|
||||
this.ended = false;
|
||||
this.meta = {};
|
||||
}
|
||||
|
||||
_write(meta, enc, next) {
|
||||
if (!meta.type) {
|
||||
log.error("MetaWriteStream: no data type");
|
||||
return next();
|
||||
}
|
||||
//log.debug("MetaWriteStream: data type=" + meta.type);
|
||||
this.meta[meta.type] = meta.data;
|
||||
next();
|
||||
}
|
||||
|
||||
_final(next) {
|
||||
//log.debug("MetaWriteStream: end. meta=" + JSON.stringify(this.meta));
|
||||
this.file.end(JSON.stringify(this.meta));
|
||||
this.ended = true;
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
class MetaCache extends Writable {
|
||||
constructor(options) {
|
||||
super({ objectMode: true });
|
||||
this.meta = {};
|
||||
this.cacheLen = options.cacheLen;
|
||||
}
|
||||
|
||||
_write(meta, enc, next) {
|
||||
if (!meta.type) {
|
||||
log.error("MetaCache: no data type");
|
||||
return next();
|
||||
} else if (meta.validFrom > meta.validTo) {
|
||||
log.error("MetaCache: negative time window validFrom=" + meta.validFrom + " validTo=" + meta.validTo);
|
||||
return next();
|
||||
} else {
|
||||
//log.debug("MetaCache: _write: " + JSON.stringify(meta));
|
||||
}
|
||||
// events of this kind:
|
||||
// meta = { type: "metadata", validFrom: Date, validTo: Date, payload: { artist: "...", title : "...", cover: "..." } } ==> metadata for enhanced experience
|
||||
// meta = { type: "class", validFrom: Date, validTo: Date, payload: "todo" } ==> class of audio, for automatic channel hopping
|
||||
// meta = { type: "volume", validFrom: Date, validTo: Date, payload: [0.85, 0.89, 0.90, ...] } ==> normalized volume for audio player
|
||||
// meta = { type: "signal", validFrom: Date, validTo: Date, payload: [0.4, 0.3, ...] } ==> signal amplitude envelope for visualization
|
||||
|
||||
// are stored in the following structure:
|
||||
// this.meta = {
|
||||
// "metadata": [
|
||||
// { validFrom: ..., validTo: ..., payload: { ... } }, (merges the contiguous segments)
|
||||
// ...
|
||||
// ],
|
||||
// "class": [
|
||||
// { validFrom: ..., validTo: ..., payload: ... }, (merges the contiguous segments)
|
||||
// ...
|
||||
// ],
|
||||
// "signal": [
|
||||
// { validFrom: ..., validTo: ..., payload: [ ... ] },
|
||||
// ...
|
||||
// ]
|
||||
// }
|
||||
|
||||
switch (meta.type) {
|
||||
case "metadata":
|
||||
case "class":
|
||||
case "volume":
|
||||
if (!this.meta[meta.type]) {
|
||||
this.meta[meta.type] = [ { validFrom: meta.validFrom, validTo: meta.validTo, payload: meta.payload } ];
|
||||
} else {
|
||||
var samePayload = true;
|
||||
for (var key in meta.payload) {
|
||||
if ("" + meta.payload[key] && "" + meta.payload[key] !== "" + this.meta[meta.type][this.meta[meta.type].length-1].payload[key]) {
|
||||
samePayload = false;
|
||||
//log.debug("MetaCache: _write: different payload key=" + key + " new=" + meta.payload[key] + " vs old=" + this.meta[meta.type][this.meta[meta.type].length-1].payload[key]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (samePayload) {
|
||||
this.meta[meta.type][this.meta[meta.type].length-1].validTo = meta.validTo; // extend current segment validity
|
||||
} else {
|
||||
this.meta[meta.type][this.meta[meta.type].length-1].validTo = meta.validFrom; // create a new segment
|
||||
this.meta[meta.type].push({ validFrom: meta.validFrom, validTo: meta.validTo, payload: meta.payload });
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "signal":
|
||||
if (!this.meta[meta.type]) {
|
||||
this.meta[meta.type] = [ { validFrom: meta.validFrom, validTo: meta.validTo, payload: meta.payload } ];
|
||||
} else {
|
||||
this.meta[meta.type].push({ validFrom: meta.validFrom, validTo: meta.validTo, payload: meta.payload });
|
||||
}
|
||||
break;
|
||||
default:
|
||||
log.error("MetaCache: _write: unknown metadata type = " + meta.type);
|
||||
}
|
||||
|
||||
// clean old entries
|
||||
while (+this.meta[meta.type][0].validTo <= +new Date() - 1000 * this.cacheLen) {
|
||||
this.meta[meta.type].splice(0, 1);
|
||||
}
|
||||
|
||||
// fix overlapping entries
|
||||
for (var i=0; i<this.meta[meta.type].length-1; i++) {
|
||||
if (this.meta[meta.type][i].validTo > this.meta[meta.type][i+1].validFrom) {
|
||||
//var middle = (this.meta[meta.type][i].validTo + this.meta[meta.type][i+1].validFrom) / 2;
|
||||
var delta = (this.meta[meta.type][i].validTo - this.meta[meta.type][i+1].validFrom) / 2;
|
||||
log.debug("MetaCache: fix meta " + meta.type + " overlapping prevTo=" + this.meta[meta.type][i].validTo + " nextFrom=" + this.meta[meta.type][i+1].validFrom + " newBound=" + (this.meta[meta.type][i].validTo - delta));
|
||||
this.meta[meta.type][i].validTo -= delta;
|
||||
this.meta[meta.type][i+1].validFrom += delta;
|
||||
}
|
||||
}
|
||||
//log.debug("MetaCache: _write: meta[" + meta.type + "]=" + JSON.stringify(this.meta[meta.type]));
|
||||
next();
|
||||
}
|
||||
|
||||
read(since) {
|
||||
if (!since) {
|
||||
this.meta.now = +new Date();
|
||||
return this.meta;
|
||||
} else {
|
||||
var result = { now: +new Date() };
|
||||
var thrDate = result.now - since*1000;
|
||||
typeloop:
|
||||
for (var type in this.meta) {
|
||||
if (type == "now") continue typeloop;
|
||||
if (thrDate < this.meta[type][0].validFrom) {
|
||||
result[type] = this.meta[type];
|
||||
continue;
|
||||
} else {
|
||||
itemloop:
|
||||
for (var i=0; i<this.meta[type].length; i++) {
|
||||
if (this.meta[type][i].validFrom <= thrDate && thrDate < this.meta[type][i].validTo) {
|
||||
result[type] = this.meta[type].slice(i);
|
||||
break itemloop;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
log.warn("MetaCache: read since " + since + "s: no data found for type " + type);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function(radio, options) {
|
||||
var newDl = new StreamDl({ country: radio.country, name: radio.name, segDuration: options.segDuration });
|
||||
var dbs = null;
|
||||
newDl.on("error", function(err) {
|
||||
log.error("dl err=" + err);
|
||||
});
|
||||
newDl.on("metadata", function(metadata) {
|
||||
//metadataCallback(metadata);
|
||||
var db = new Db({
|
||||
country: radio.country,
|
||||
name: radio.name,
|
||||
ext: metadata.ext,
|
||||
bitrate: metadata.bitrate,
|
||||
cacheLen: options.cacheLen,
|
||||
path: __dirname
|
||||
});
|
||||
log.debug("DlFactory: " + radio.country + "_" + radio.name + " metadata=" + JSON.stringify(metadata));
|
||||
|
||||
var onClassPrediction = function(className, volume) {
|
||||
var now = +new Date();
|
||||
db.metaCache.write({
|
||||
type: "class",
|
||||
validFrom: now-500*options.segDuration,
|
||||
validTo: now+500*options.segDuration,
|
||||
payload: className
|
||||
});
|
||||
db.metaCache.write({
|
||||
type: "volume",
|
||||
validFrom: now-500*options.segDuration,
|
||||
validTo: now+500*options.segDuration,
|
||||
payload: volume
|
||||
});
|
||||
if (options.saveAudio && dbs && dbs.metadata) {
|
||||
dbs.metadata.write({ type: "class", data: className });
|
||||
dbs.metadata.write({ type: "volume", data: volume });
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(radio.liveStatus, {
|
||||
audioCache: db.audioCache,
|
||||
metaCache: db.metaCache,
|
||||
onClassPrediction: onClassPrediction
|
||||
});
|
||||
|
||||
newDl.on("data", function(dataObj) {
|
||||
//dataObj: { newSegment: newSegment, tBuffer: this.tBuffer, data: data
|
||||
if (!dataObj.newSegment) {
|
||||
if (options.saveAudio) dbs.audio.write(dataObj.data);
|
||||
} else {
|
||||
newDl.pause();
|
||||
if (options.saveAudio) {
|
||||
if (dbs) {
|
||||
dbs.audio.end();
|
||||
dbs.metadata.end()
|
||||
}
|
||||
dbs = db.newAudioSegment();
|
||||
Object.assign(radio.liveStatus, {
|
||||
currentPrefix: dbs.path,
|
||||
liveReadStream: dbs.audio
|
||||
});
|
||||
}
|
||||
|
||||
if (options.fetchMetadata) {
|
||||
Metadata.getMeta(radio.country, radio.name, function(err, parsedMeta, corsEnabled) {
|
||||
if (err) return log.warn("getMeta: error fetching title meta for radio " + radio.country + "_" + radio.name + " err=" + err);
|
||||
//log.debug(radio.country + "_" + radio.name + " meta=" + JSON.stringify(parsedMeta));
|
||||
if (options.saveAudio) {
|
||||
if (!dbs.metadata.ended) {
|
||||
dbs.metadata.write({ type: "metadata", data: parsedMeta });
|
||||
} else {
|
||||
log.warn("getMeta: could not write metadata, stream already ended");
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(radio.liveStatus, {
|
||||
metadata: parsedMeta
|
||||
});
|
||||
var now = +new Date();
|
||||
db.metaCache.write({ type: "metadata", validFrom: now-500*options.segDuration, validTo: now+500*options.segDuration, payload: parsedMeta });
|
||||
});
|
||||
}
|
||||
|
||||
if (options.saveAudio) dbs.audio.write(dataObj.data);
|
||||
newDl.resume();
|
||||
}
|
||||
db.audioCache.write(dataObj.data);
|
||||
});
|
||||
});
|
||||
return newDl;
|
||||
}
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
const { log } = require('abr-log')('listen');
|
||||
const { config } = require('../handlers/config');
|
||||
const { config, getRadio } = require('../handlers/config');
|
||||
|
||||
|
||||
var listenRequestDate = null;
|
||||
@@ -26,10 +26,11 @@ var getDeviceInfoExpress = function(request) {
|
||||
}
|
||||
|
||||
module.exports = (app) => app.get('/listen/:radio/:delay', function(request, response) {
|
||||
var radio = decodeURIComponent(request.params.radio);
|
||||
var delay = request.params.delay;
|
||||
const radio = decodeURIComponent(request.params.radio);
|
||||
const delay = request.params.delay;
|
||||
|
||||
if (!getRadio(radio)) {
|
||||
const radioObj = getRadio(...radio.split("_"));
|
||||
if (!radioObj) {
|
||||
response.writeHead(400);
|
||||
return response.end("radio not found");
|
||||
}
|
||||
@@ -44,7 +45,6 @@ module.exports = (app) => app.get('/listen/:radio/:delay', function(request, res
|
||||
listenRequestDate = state.requestDate;
|
||||
lastQueryRandomNum = queryRandomNum;
|
||||
|
||||
var radioObj = getRadio(radio);
|
||||
var initialBuffer = radioObj.liveStatus.audioCache.readLast(+delay+config.user.streamInitialBuffer,config.user.streamInitialBuffer);
|
||||
//log.debug("listen: readCursor set to " + radioObj.liveStatus.audioCache.readCursor);
|
||||
|
||||
|
||||
Generated
+12423
-6105
File diff suppressed because it is too large
Load Diff
+16
-10
@@ -3,22 +3,28 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"async": "^2.6.0",
|
||||
"classnames": "^2.2.5",
|
||||
"moment": "^2.20.1",
|
||||
"rc-checkbox": "^2.1.4",
|
||||
"react": "^16.1.1",
|
||||
"react-dom": "^16.1.1",
|
||||
"styled-components": "^2.4.0"
|
||||
"async": "^2.6.1",
|
||||
"classnames": "^2.2.6",
|
||||
"moment": "^2.22.2",
|
||||
"rc-checkbox": "^2.1.5",
|
||||
"react": "^16.6.0",
|
||||
"react-dom": "^16.6.0",
|
||||
"styled-components": "^2.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"react-scripts": "1.0.17"
|
||||
"react-scripts": "2.1.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"start": "PORT=9820 react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"homepage": "."
|
||||
"homepage": "./player",
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not ie <= 11",
|
||||
"not op_mini all"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<html>
|
||||
<body>
|
||||
Media debug page
|
||||
|
||||
<a href="#" id="test">TEST</a>
|
||||
<script>
|
||||
var audioElement = document.createElement('audio');
|
||||
|
||||
var play = function(url, callback) { // https://developers.google.com/web/updates/2017/06/play-request-was-interrupted
|
||||
console.log("play " + url);
|
||||
audioElement.src = url;
|
||||
//var playPromise =
|
||||
audioElement.play();
|
||||
/*if (playPromise !== undefined) {
|
||||
playPromise.then(_ => {
|
||||
if (callback) callback(null);
|
||||
})
|
||||
.catch(error => {
|
||||
if (callback) callback(error);
|
||||
});
|
||||
}*/
|
||||
callback();
|
||||
}
|
||||
|
||||
var startPlay = function() {
|
||||
play("http://192.168.0.28:9820/listen/France_RTL/0?t=" + Math.round(1000000*Math.random()), function(err) {
|
||||
if (err) console.log("play err=" + err);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("test").addEventListener("mousedown", startPlay);
|
||||
|
||||
</script>
|
||||
|
||||
</html>
|
||||
+116
-112
@@ -8,7 +8,7 @@ import DelaySVG from './DelaySVG.js';
|
||||
import Config from './Config.js';
|
||||
import Playlist from './Playlist.js';
|
||||
|
||||
import { load, loadScript, refreshStatus, HOST } from './load.js';
|
||||
import { loadScript, refreshStatus } from './load.js';
|
||||
import { play, stop, setVolume } from './audio.js';
|
||||
import styled from "styled-components";
|
||||
/*import * as moment from 'moment';*/
|
||||
@@ -140,85 +140,79 @@ class App extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
refreshStatusContainer(options) {
|
||||
async refreshStatusContainer(options) {
|
||||
if (this.state.stopUpdates) return;
|
||||
|
||||
//console.log("refresh status");
|
||||
var self = this;
|
||||
refreshStatus(this.state.config.radios, options, function(err, resParsed) {
|
||||
if (err) {
|
||||
self.play(null, null, function() {});
|
||||
return self.setState({ communicationError: true });
|
||||
}
|
||||
const resParsed = await refreshStatus(options.requestFullData);
|
||||
if (!resParsed) {
|
||||
this.play(null, null, function() {});
|
||||
return this.setState({ communicationError: true });
|
||||
}
|
||||
|
||||
//console.log("refresh status callback");
|
||||
var stateChange = { communicationError: false };
|
||||
var types = ["class", "metadata", "volume"];
|
||||
for (var i=0; i<resParsed.length; i++) { // for each radio
|
||||
var radio = resParsed[i].country + "_" + resParsed[i].name;
|
||||
stateChange[radio + "|available"] = resParsed[i].available;
|
||||
stateChange.clockDiff = +new Date() - resParsed[i].now;
|
||||
var stateChange = { communicationError: false };
|
||||
var types = ["class", "metadata", "volume"];
|
||||
for (var i=0; i<resParsed.length; i++) { // for each radio
|
||||
var radio = resParsed[i].country + "_" + resParsed[i].name;
|
||||
stateChange[radio + "|available"] = resParsed[i].available;
|
||||
stateChange.clockDiff = +new Date() - resParsed[i].now;
|
||||
|
||||
for (var j=0; j<types.length; j++) { // for each of ["class", "metadata", "volume"]
|
||||
if (!resParsed[i][types[j]]) {
|
||||
//console.log("refreshStatus: radio=" + radio + " has no field " + types[j]);
|
||||
continue;
|
||||
}
|
||||
var tO = resParsed[i][types[j]];
|
||||
tO[tO.length-1].validTo = null;
|
||||
|
||||
var rt = radio + "|" + types[j];
|
||||
stateChange[rt] = self.state[rt] || [];
|
||||
for (var itO=0; itO<tO.length; itO++) {
|
||||
var alreadyThere = false;
|
||||
var itS;
|
||||
for (itS=stateChange[rt].length-1; itS>=0; itS--) {
|
||||
if (stateChange[rt][itS].validTo && stateChange[rt][itS].validTo < +self.state.date - self.state.config.user.cacheLen*1000) {
|
||||
//if (types[j] === "class") console.log("refreshStatus: " + rt + " remove old item validTo=" + stateChange[rt][itS].validTo);
|
||||
stateChange[rt].splice(itS, 1); // remove old elements
|
||||
} else if (tO[itO].validFrom === stateChange[rt][itS].validFrom) {
|
||||
alreadyThere = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (alreadyThere && tO[itO].validTo !== null && stateChange[rt][itS].validTo === null) { // we overwrite the last element, because validTo was erased
|
||||
//if (types[j] === "class") console.log("refreshStatus: " + rt + " overwrite validFrom=" + tO[itO].validFrom);
|
||||
stateChange[rt][itS] = tO[itO];
|
||||
} else if (!alreadyThere) {
|
||||
//if (types[j] === "class") console.log("refreshStatus: " + rt + " unshift validFrom=" + tO[itO].validFrom);
|
||||
stateChange[rt].unshift(tO[itO]);
|
||||
}
|
||||
}
|
||||
//stateChange[radio + "|" + types[j]] = tO.reverse();
|
||||
for (var j=0; j<types.length; j++) { // for each of ["class", "metadata", "volume"]
|
||||
if (!resParsed[i][types[j]]) {
|
||||
//console.log("refreshStatus: radio=" + radio + " has no field " + types[j]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
var tO = resParsed[i][types[j]];
|
||||
tO[tO.length-1].validTo = null;
|
||||
|
||||
self.setState(stateChange, function() {
|
||||
self.showNotification();
|
||||
});
|
||||
var rt = radio + "|" + types[j];
|
||||
stateChange[rt] = self.state[rt] || [];
|
||||
for (var itO=0; itO<tO.length; itO++) {
|
||||
var alreadyThere = false;
|
||||
var itS;
|
||||
for (itS=stateChange[rt].length-1; itS>=0; itS--) {
|
||||
if (stateChange[rt][itS].validTo && stateChange[rt][itS].validTo < +self.state.date - self.state.config.user.cacheLen*1000) {
|
||||
//if (types[j] === "class") console.log("refreshStatus: " + rt + " remove old item validTo=" + stateChange[rt][itS].validTo);
|
||||
stateChange[rt].splice(itS, 1); // remove old elements
|
||||
} else if (tO[itO].validFrom === stateChange[rt][itS].validFrom) {
|
||||
alreadyThere = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (alreadyThere && tO[itO].validTo !== null && stateChange[rt][itS].validTo === null) { // we overwrite the last element, because validTo was erased
|
||||
//if (types[j] === "class") console.log("refreshStatus: " + rt + " overwrite validFrom=" + tO[itO].validFrom);
|
||||
stateChange[rt][itS] = tO[itO];
|
||||
} else if (!alreadyThere) {
|
||||
//if (types[j] === "class") console.log("refreshStatus: " + rt + " unshift validFrom=" + tO[itO].validFrom);
|
||||
stateChange[rt].unshift(tO[itO]);
|
||||
}
|
||||
}
|
||||
//stateChange[radio + "|" + types[j]] = tO.reverse();
|
||||
}
|
||||
}
|
||||
await this.setStateAsync(stateChange);
|
||||
this.showNotification();
|
||||
}
|
||||
|
||||
async setStateAsync(state) {
|
||||
return new Promise((resolve) => {
|
||||
this.setState(state, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
refreshConfig(callback) {
|
||||
var self = this;
|
||||
var onError = function(err) {
|
||||
console.log("problem parsing JSON from server: " + err);
|
||||
self.setState({ configError: true, configLoaded: true });
|
||||
async refreshConfig(callback) {
|
||||
try {
|
||||
const request = await fetch("config?t=" + Math.round(Math.random()*1000000));
|
||||
const res = await request.text();
|
||||
const config = JSON.parse(res);
|
||||
await this.setState({ config: config, configError: false, configLoaded: true });
|
||||
this.newRefreshStatusInterval(DELAYS.FETCH_UPDATES_IDLE, true);
|
||||
} catch (e) {
|
||||
console.log("problem refreshing config from server: " + e);
|
||||
this.setState({ configError: true, configLoaded: true });
|
||||
}
|
||||
|
||||
load("config?t=" + Math.round(Math.random()*1000000), function(err, res) {
|
||||
if (err) return onError(err);
|
||||
try {
|
||||
var config = JSON.parse(res);
|
||||
self.setState({ config: config, configLoaded: true }, function() {
|
||||
self.newRefreshStatusInterval(DELAYS.FETCH_UPDATES_IDLE, true);
|
||||
});
|
||||
} catch(e) {
|
||||
return onError(e.message);
|
||||
}
|
||||
|
||||
if (callback) callback();
|
||||
});
|
||||
if (callback) callback();
|
||||
}
|
||||
|
||||
|
||||
@@ -281,8 +275,8 @@ class App extends Component {
|
||||
}
|
||||
|
||||
acceptableContent(iRadio, classObj) {
|
||||
return ((this.state.config.radios[iRadio].content.ads || classObj.payload !== "AD") &&
|
||||
(this.state.config.radios[iRadio].content.speech || classObj.payload !== "SPEECH"))
|
||||
return ((this.state.config.radios[iRadio].content.ads || classObj.payload !== "0-ads") &&
|
||||
(this.state.config.radios[iRadio].content.speech || classObj.payload !== "1-speech"))
|
||||
}
|
||||
|
||||
checkCursor(radio, callback) {
|
||||
@@ -396,8 +390,10 @@ class App extends Component {
|
||||
}
|
||||
|
||||
setVolumeForRadio(radio) {
|
||||
var targetVolume = VOLUMES.DEFAULT;
|
||||
if (this.state[radio + "|volume"] && this.state[radio + "|volume"].length > 0) targetVolume = this.state[radio + "|volume"][0].payload;
|
||||
let targetVolume = VOLUMES.DEFAULT;
|
||||
if (this.state[radio + "|volume"] && this.state[radio + "|volume"].length > 0) {
|
||||
targetVolume = Math.pow(10, (Math.min(70-this.state[radio + "|volume"][0].payload,0))/20)
|
||||
}
|
||||
setVolume(targetVolume);
|
||||
}
|
||||
|
||||
@@ -519,7 +515,7 @@ class App extends Component {
|
||||
});
|
||||
|
||||
document.title = radio.split("_")[1] + " - Adblock Radio";
|
||||
var url = HOST + "listen/" + encodeURIComponent(radio) + "/" + (delay/1000) + "?t=" + Math.round(Math.random()*1000000000);
|
||||
var url = "listen/" + encodeURIComponent(radio) + "/" + (delay/1000) + "?t=" + Math.round(Math.random()*1000000000);
|
||||
play(url, function(err) {
|
||||
if (err) console.log("Play: error=" + err);
|
||||
if (callback) callback(err);
|
||||
@@ -564,32 +560,40 @@ class App extends Component {
|
||||
this.setState({ locale: lang });
|
||||
}
|
||||
|
||||
insertRadio(country, name, callback) {
|
||||
var self = this;
|
||||
load("config/radios/insert/" + encodeURIComponent(country) + "/" + encodeURIComponent(name) + "?t=" + Math.round(Math.random()*1000000), function(res) {
|
||||
self.refreshConfig(callback);
|
||||
});
|
||||
async insertRadio(country, name) {
|
||||
try {
|
||||
await fetch("config/radios/" + encodeURIComponent(country) + "/" + encodeURIComponent(name) + "?t=" + Math.round(Math.random()*1000000), { method: "PUT" });
|
||||
await this.refreshConfig();
|
||||
} catch (e) {
|
||||
console.log("could not insert radio " + country + "_" + name + ". err=" + e);
|
||||
}
|
||||
}
|
||||
|
||||
removeRadio(country, name, callback) {
|
||||
async removeRadio(country, name) {
|
||||
if (this.state.playingRadio === country + "_" + name) this.play(null, null, function() {});
|
||||
var self = this;
|
||||
load("config/radios/remove/" + encodeURIComponent(country) + "/" + encodeURIComponent(name) + "?t=" + Math.round(Math.random()*1000000), function(res) {
|
||||
self.refreshConfig(callback);
|
||||
});
|
||||
try {
|
||||
await fetch("config/radios/" + encodeURIComponent(country) + "/" + encodeURIComponent(name) + "?t=" + Math.round(Math.random()*1000000), { method: "DELETE" });
|
||||
await this.refreshConfig();
|
||||
} catch (e) {
|
||||
console.log("could not remove radio " + country + "_" + name + ". err=" + e);
|
||||
}
|
||||
}
|
||||
|
||||
toggleContent(country, name, contentType, enabled, callback) {
|
||||
var self = this;
|
||||
load("config/radios/content/" + encodeURIComponent(country) + "/" + encodeURIComponent(name) + "/" + encodeURIComponent(contentType) + "/" + (enabled ? "enable" : "disable") + "?t=" + Math.round(Math.random()*1000000), function(res) {
|
||||
self.refreshConfig(callback);
|
||||
});
|
||||
async toggleContent(country, name, contentType, enabled) {
|
||||
try {
|
||||
// /config/radios/:country/:name/content/:type/:enable
|
||||
await fetch("config/radios/" + encodeURIComponent(country) + "/" + encodeURIComponent(name) + "/content/" +
|
||||
encodeURIComponent(contentType) + "/" + (enabled ? "enable" : "disable") + "?t=" + Math.round(Math.random()*1000000), { method: "PUT" });
|
||||
await this.refreshConfig();
|
||||
} catch (e) {
|
||||
console.log("could not toggle content for radio " + country + "_" + name + " content=" + contentType + " enabled=" + enabled + " err=" + e);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let config = this.state.config;
|
||||
let lang = this.state.locale;
|
||||
var self = this;
|
||||
const self = this;
|
||||
if (!this.state.configLoaded) {
|
||||
return (
|
||||
<SoloMessage>
|
||||
@@ -606,10 +610,10 @@ class App extends Component {
|
||||
}
|
||||
|
||||
var statusText;
|
||||
if (self.state.playingRadio) {
|
||||
if (this.state.playingRadio) {
|
||||
var delayText = { en: "Live", fr: "En direct" }[lang];
|
||||
if (self.state.playingDelay > 0) {
|
||||
var delaySeconds = Math.round(self.state.playingDelay/1000); // + self.state.config.user.streamInitialBuffer);
|
||||
if (this.state.playingDelay > 0) {
|
||||
var delaySeconds = Math.round(this.state.playingDelay/1000);
|
||||
var delayMinutes = Math.floor(delaySeconds / 60);
|
||||
delaySeconds = delaySeconds % 60;
|
||||
var textDelay = (delayMinutes ? delayMinutes + " min" : "");
|
||||
@@ -618,7 +622,7 @@ class App extends Component {
|
||||
}
|
||||
statusText = (
|
||||
<span>
|
||||
{self.state.playingRadio.split("_")[1]}<br />
|
||||
{this.state.playingRadio.split("_")[1]}<br />
|
||||
<DelayText>{delayText}</DelayText>
|
||||
</span>
|
||||
)
|
||||
@@ -639,35 +643,35 @@ class App extends Component {
|
||||
|
||||
var buttons = (
|
||||
<StatusButtonsContainer>
|
||||
<PlaybackButton className={classNames({ inactive: !self.state.configEditMode })} src={iconConfig} alt={{ en: "Edit config", fr: "Configurer l'écoute" }[lang]} onClick={self.switchConfigEditMode} />
|
||||
<PlaybackButton className={classNames({ inactive: !self.state.playlistEditMode })} src={iconList} alt={{ en: "Edit playlist", fr: "Changer de liste de radios" }[lang]} onClick={self.switchPlaylistEditMode} />
|
||||
{/*<PlaybackButton className={classNames({ flip: true, inactive: !self.state.playingRadio || self.state.playingDelay >= self.state.config.user.cacheLen*1000 })} src={iconPlay} alt="Backward 30s" onClick={self.seekBackward} />*/}
|
||||
<PlaybackButton className={classNames({ inactive: !self.state.playingRadio })} src={iconStop} alt="Stop" onClick={() => self.play(null, null, null)} />
|
||||
{/*<PlaybackButton className={classNames({ inactive: !self.state.playingRadio || self.state.playingLive })} src={iconPlay} alt="Forward 30s" onClick={self.seekForward} />*/}
|
||||
<PlaybackButton className={classNames({ inactive: !this.state.configEditMode })} src={iconConfig} alt={{ en: "Edit config", fr: "Configurer l'écoute" }[lang]} onClick={this.switchConfigEditMode} />
|
||||
<PlaybackButton className={classNames({ inactive: !this.state.playlistEditMode })} src={iconList} alt={{ en: "Edit playlist", fr: "Changer de liste de radios" }[lang]} onClick={this.switchPlaylistEditMode} />
|
||||
{/*<PlaybackButton className={classNames({ flip: true, inactive: !this.state.playingRadio || this.state.playingDelay >= this.state.config.user.cacheLen*1000 })} src={iconPlay} alt="Backward 30s" onClick={this.seekBackward} />*/}
|
||||
<PlaybackButton className={classNames({ inactive: !this.state.playingRadio })} src={iconStop} alt="Stop" onClick={() => this.play(null, null, null)} />
|
||||
{/*<PlaybackButton className={classNames({ inactive: !this.state.playingRadio || this.state.playingLive })} src={iconPlay} alt="Forward 30s" onClick={this.seekForward} />*/}
|
||||
</StatusButtonsContainer>
|
||||
);
|
||||
|
||||
//console.log("Metadata props: date=" + (+self.state.date) + " clockDiff=" + self.state.clockDiff + " playingDelay=" + self.state.playingDelay);
|
||||
//console.log("Metadata props: date=" + (+this.state.date) + " clockDiff=" + this.state.clockDiff + " playingDelay=" + this.state.playingDelay);
|
||||
|
||||
let mainContents;
|
||||
if (self.state.configEditMode || !config.user.email) {
|
||||
if (this.state.configEditMode || !config.user.email) {
|
||||
mainContents = (
|
||||
<Config config={self.state.config}
|
||||
toggleContent={self.toggleContent}
|
||||
locale={self.state.locale}
|
||||
setLocale={self.setLocale} />
|
||||
<Config config={this.state.config}
|
||||
toggleContent={this.toggleContent}
|
||||
locale={this.state.locale}
|
||||
setLocale={this.setLocale} />
|
||||
);
|
||||
} else if (self.state.playlistEditMode || config.radios.length === 0) {
|
||||
} else if (this.state.playlistEditMode || config.radios.length === 0) {
|
||||
mainContents = (
|
||||
<Playlist config={self.state.config}
|
||||
insertRadio={self.insertRadio}
|
||||
removeRadio={self.removeRadio}
|
||||
locale={self.state.locale} />
|
||||
<Playlist config={this.state.config}
|
||||
insertRadio={this.insertRadio}
|
||||
removeRadio={this.removeRadio}
|
||||
locale={this.state.locale} />
|
||||
);
|
||||
} else {
|
||||
mainContents = (
|
||||
<RadioList>
|
||||
{self.state.communicationError &&
|
||||
{this.state.communicationError &&
|
||||
<SoloMessage>
|
||||
<p>{{ en: "The communication with the server is temporarily unavailable…", fr: "La connection au serveur est momentanément interrompue…" }[lang]}</p>
|
||||
</SoloMessage>
|
||||
@@ -720,12 +724,12 @@ class App extends Component {
|
||||
</AppView>
|
||||
<Controls>
|
||||
<MaxWidthContainer>
|
||||
{self.state.playingRadio &&
|
||||
{this.state.playingRadio &&
|
||||
<PlayingGif src={playing} />
|
||||
}
|
||||
{status}
|
||||
{buttons}
|
||||
{/*metaList={self.state[self.state.playingRadio + "|metadata"]}*/}
|
||||
{/*metaList={this.state[this.state.playingRadio + "|metadata"]}*/}
|
||||
|
||||
{/*<PlayerStatus settings={this.props.settings} bsw={this.props.bsw} condensed={this.props.condensed} playbackAction={this.togglePlayer} />*/}
|
||||
</MaxWidthContainer>
|
||||
|
||||
+10
-6
@@ -10,11 +10,15 @@ import FlagContainer from "./Flag.js";
|
||||
import defaultCover from "./img/default_radio_logo.svg";
|
||||
import userIcon from "./img/user_1085539.svg";
|
||||
import { colorByType } from "./colors.js";
|
||||
import { HOST } from './load.js';
|
||||
|
||||
|
||||
class Config extends Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.toggleContent = this.toggleContent.bind(this);
|
||||
}
|
||||
|
||||
translateContentName(type, lang) {
|
||||
switch (type) {
|
||||
case "ads": return { en: "ads", fr: "pubs" }[lang];
|
||||
@@ -24,10 +28,11 @@ class Config extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
/*toggleContent(country, name, contentType, enabled) {
|
||||
async toggleContent(country, name, contentType, enabled) {
|
||||
console.log("toggleContent radio=" + country + "_" + name + " contentType=" + contentType + " enable=" + enabled);
|
||||
this.props.toggleContent(country, name, contentType, enabled, this.componentDidMount);
|
||||
}*/
|
||||
await this.props.toggleContent(country, name, contentType, enabled);
|
||||
this.componentDidMount();
|
||||
}
|
||||
|
||||
render() {
|
||||
var lang = this.props.locale;
|
||||
@@ -70,7 +75,7 @@ class Config extends Component {
|
||||
<PlaylistItemConfigItem key={"item" + i + "config" + j}>
|
||||
<Checkbox
|
||||
checked={!radio.content[type] && !!self.props.config.user.email}
|
||||
onChange={(e) => self.props.toggleContent(radio.country, radio.name, type, !e.target.checked, self.componentDidMount)}
|
||||
onChange={(e) => self.toggleContent(radio.country, radio.name, type, !e.target.checked)}
|
||||
disabled={!self.props.config.user.email}
|
||||
/>
|
||||
{{ en: "skip " + self.translateContentName(type, lang), fr: "zapper les " + self.translateContentName(type, lang) }[lang]}
|
||||
@@ -97,7 +102,6 @@ class Config extends Component {
|
||||
);
|
||||
})}
|
||||
</ChoiceL10nContainer>
|
||||
<PreferencesItemTitle>{{ en: "Server path:", fr: "Serveur :"}[lang] + " " + HOST}</PreferencesItemTitle>
|
||||
|
||||
<PreferencesItemTitle>{{ en: "Connected to Adblock Radio as:", fr: "Connecté à Adblock Radio en tant que :"}[lang]}</PreferencesItemTitle>
|
||||
{loggedAs &&
|
||||
|
||||
@@ -51,9 +51,9 @@ class DelaySVG extends Component {
|
||||
for (var i=nClasses; i>=0; i--) {
|
||||
var cl = this.props.classList[i];
|
||||
switch (cl.payload) {
|
||||
case "AD": colorClass[i] = colors.RED; break;
|
||||
case "SPEECH": colorClass[i] = colors.GREEN; break;
|
||||
case "MUSIC": colorClass[i] = colors.BLUE; break;
|
||||
case "0-ads": colorClass[i] = colors.RED; break;
|
||||
case "1-speech": colorClass[i] = colors.GREEN; break;
|
||||
case "2-music": colorClass[i] = colors.BLUE; break;
|
||||
default: colorClass[i] = colors.GREY;
|
||||
}
|
||||
xStartClass[i] = Math.max(this.delayToX(this.props.width, +this.props.date-cl.validFrom), 0);
|
||||
|
||||
+18
-22
@@ -3,7 +3,6 @@
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import styled from "styled-components";
|
||||
import { load } from './load.js';
|
||||
import classNames from 'classnames';
|
||||
import 'rc-checkbox/assets/index.css';
|
||||
import defaultCover from "./img/default_radio_logo.svg";
|
||||
@@ -31,29 +30,26 @@ class Playlist extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let self = this;
|
||||
|
||||
load("config/radios/available?t=" + Math.round(Math.random()*1000000), function(err, res) {
|
||||
if (err) {
|
||||
return self.setState({ radiosLoaded: true, radiosError: true });
|
||||
}
|
||||
try {
|
||||
var radios = JSON.parse(res);
|
||||
self.setState({ radiosLoaded: true, radios: radios });
|
||||
} catch(e) {
|
||||
console.log("problem parsing JSON from server: " + e.message);
|
||||
self.setState({ radiosLoaded: true, radiosError: true });
|
||||
}
|
||||
});
|
||||
async componentDidMount() {
|
||||
try {
|
||||
const request = await fetch("config/radios/available?t=" + Math.round(Math.random()*1000000));
|
||||
const res = await request.text();
|
||||
var radios = JSON.parse(res);
|
||||
this.setState({ radiosLoaded: true, radios: radios, radiosError: false });
|
||||
} catch (e) {
|
||||
console.log("problem getting available radios. err=" + e.message);
|
||||
this.setState({ radiosLoaded: true, radiosError: true });
|
||||
}
|
||||
}
|
||||
|
||||
insert(country, name) {
|
||||
this.props.insertRadio(country, name, this.componentDidMount);
|
||||
async insert(country, name) {
|
||||
await this.props.insertRadio(country, name);
|
||||
this.componentDidMount();
|
||||
}
|
||||
|
||||
remove(country, name) {
|
||||
this.props.removeRadio(country, name, this.componentDidMount);
|
||||
async remove(country, name) {
|
||||
await this.props.removeRadio(country, name);
|
||||
this.componentDidMount();
|
||||
}
|
||||
|
||||
/*toggleContent(country, name, contentType, enabled) {
|
||||
@@ -94,10 +90,10 @@ class Playlist extends Component {
|
||||
<PlaylistItem className={classNames({ active: true })} key={"item" + i}>
|
||||
<PlaylistItemTopRow>
|
||||
<PlaylistItemLogo src={radio.favicon || defaultCover} alt="logo" />
|
||||
<PlaylistItemText onClick={function() { self.remove(radio.country, radio.name); }}>
|
||||
<PlaylistItemText onClick={() => self.remove(radio.country, radio.name)}>
|
||||
{radio.name}
|
||||
</PlaylistItemText>
|
||||
<RemoveIcon src={removeIcon} onClick={function() { self.remove(radio.country, radio.name); }} />
|
||||
<RemoveIcon src={removeIcon} onClick={() => self.remove(radio.country, radio.name)} />
|
||||
</PlaylistItemTopRow>
|
||||
</PlaylistItem>
|
||||
)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg width='200' height='200' fill="#000000" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve"><polygon points="97.5,61 97.5,50.6 66.4,50.6 66.4,2 60.1,2 60.1,50.6 43.3,50.6 43.3,2 37,2 37,47.1 15.8,25.9 24.8,17 2.5,17 2.5,39.3 11.4,30.4 31.6,50.6 2.5,50.6 2.5,61 60.1,61 60.1,81.3 47.6,81.3 63.3,97 79.1,81.3 66.4,81.3 66.4,61 "/></svg>
|
||||
|
After Width: | Height: | Size: 483 B |
+10
-52
@@ -1,59 +1,17 @@
|
||||
var getParameterByName = function(name, url) {
|
||||
if (!url) url = window.location.href;
|
||||
name = name.replace(/[[\]]/g, "\\$&"); //name = name.replace(/[\[\]]/g, "\\$&");
|
||||
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), results = regex.exec(url);
|
||||
if (!results) return null;
|
||||
if (!results[2]) return "";
|
||||
return decodeURIComponent(results[2].replace(/\+/g, " "));
|
||||
}
|
||||
|
||||
//var HOST = getParameterByName("dev") ? "http://localhost:9820/" : "https://bufferapi.s00.adblockradio.com/";
|
||||
//var HOST = getParameterByName("dev") ? "http://localhost:9820/" : "/bufferapi/";
|
||||
var HOST = getParameterByName("dev") ? "https://dome.storelli.fr/buffer/" : "";
|
||||
|
||||
exports.load = function(path, callback) {
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = function() {
|
||||
if (xhttp.readyState === 4 && xhttp.status === 200) {
|
||||
callback(null, xhttp.responseText); //, xhttp.getResponseHeader("Content-Type"));
|
||||
}
|
||||
};
|
||||
xhttp.onerror = function (e) {
|
||||
callback("load: request failed: " + e.message, null);
|
||||
};
|
||||
|
||||
xhttp.open("GET", HOST + path, true);
|
||||
|
||||
xhttp.timeout = 5000; // Set timeout to 4 seconds (4000 milliseconds)
|
||||
xhttp.ontimeout = function () {
|
||||
callback("load: timed out", null);
|
||||
export async function refreshStatus(requestFullData) {
|
||||
var since = requestFullData ? "900" : "10"; // TODO insert the real buffer length here instead of 900
|
||||
try {
|
||||
const request = await fetch("status/" + since + "?t=" + Math.round(Math.random()*1000000));
|
||||
const res = await request.text();
|
||||
return JSON.parse(res);
|
||||
} catch (e) {
|
||||
console.log("refreshStatus: could not load status update for radios. err=" + e);
|
||||
return null;
|
||||
}
|
||||
|
||||
xhttp.send();
|
||||
}
|
||||
|
||||
exports.HOST = HOST;
|
||||
|
||||
exports.refreshStatus = function(radios, options, callback) {
|
||||
var since = options.requestFullData ? "900" : "10";
|
||||
exports.load("status/" + since + "?t=" + Math.round(Math.random()*1000000), function(err, res) {
|
||||
if (err) {
|
||||
console.log("refreshStatus: could not load status update for radios (" + err + ")");
|
||||
return callback(err, null);
|
||||
}
|
||||
var resParsed = {};
|
||||
try {
|
||||
resParsed = JSON.parse(res);
|
||||
} catch(e) {
|
||||
console.log("problem parsing JSON from server: " + e.message);
|
||||
}
|
||||
|
||||
callback(null, resParsed);
|
||||
});
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/950087/how-do-i-include-a-javascript-file-in-another-javascript-file
|
||||
exports.loadScript = function(url, callback) {
|
||||
export function loadScript(url, callback) {
|
||||
// Adding the script tag to the head as suggested before
|
||||
var head = document.getElementsByTagName('head')[0];
|
||||
var script = document.createElement('script');
|
||||
|
||||
+27
-6
@@ -1,10 +1,31 @@
|
||||
var http = require('http');
|
||||
var express = require('express');
|
||||
var app = express();
|
||||
var server = http.createServer(app);
|
||||
server.listen(9820); // no "localhost" binding since this routine is intended to run in a Docker container
|
||||
const http = require('http');
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
|
||||
const { log } = require("abr-log")("app");
|
||||
const { config } = require("./config");
|
||||
|
||||
app.use('/', express.static('client/build'));
|
||||
server.listen(config.user.serverPort); // no "localhost" binding in case this routine would be run in a Docker container
|
||||
|
||||
log.info("Server listening on port " + config.user.serverPort);
|
||||
|
||||
const DEV = process.env.DEV;
|
||||
|
||||
//app.use('/', express.static('client/build'));
|
||||
|
||||
if (DEV) {
|
||||
log.warn('DEV MODE');
|
||||
// proxy everything but /config/*, /status/* and /listen/* requests (managed by this program)
|
||||
// everything else is routed to localhost:3000, the react dev server.
|
||||
const proxy = require('http-proxy-middleware');
|
||||
const apiProxy = proxy('!(/config|/config/**|/status/**|/listen/**)', { target: 'http://localhost:3000', ws: true, loglevel: 'warn' });
|
||||
app.use('/', apiProxy);
|
||||
|
||||
} else {
|
||||
log.warn('Server started in production mode.');
|
||||
//app.use('/login.html', express.static('webmin-src/build/login.html'));
|
||||
app.use('/', express.static('client/build'));
|
||||
}
|
||||
|
||||
exports.app = app;
|
||||
+55
-67
@@ -1,24 +1,35 @@
|
||||
'use strict';
|
||||
|
||||
const { log } = require('abr-log')('cache');
|
||||
const { Analyser } = require("../adblockradio/post-processing.js");
|
||||
const { Writable } = require("stream");
|
||||
const { Analyser } = require("../../adblockradio/post-processing.js");
|
||||
const { config } = require('./config');
|
||||
var dl = [];
|
||||
var lastPrediction = new Date();
|
||||
|
||||
|
||||
class AudioCache extends Writable {
|
||||
constructor(options) {
|
||||
super();
|
||||
this.cacheLen = options.cacheLen;
|
||||
this.bitrate = options.bitrate;
|
||||
this.bitrateValidated = false;
|
||||
this.bitrate = 16000; // bytes per second. default value, to be updated later
|
||||
this.flushAmount = 60 * this.bitrate;
|
||||
this.readCursor = null;
|
||||
this.buffer = Buffer.allocUnsafe(this.cacheLen * this.bitrate + 2*this.flushAmount).fill(0);
|
||||
this.writeCursor = 0;
|
||||
}
|
||||
|
||||
setBitrate(bitrate) {
|
||||
if (!isNaN(bitrate) && bitrate > 0 && this.bitrate != bitrate) {
|
||||
log.info("AudioCache: bitrate adjusted from " + this.bitrate + "bps to " + bitrate + "bps");
|
||||
|
||||
// if bitrate is higher than expected, expand the buffer accordingly.
|
||||
if (bitrate > this.bitrate) {
|
||||
var expandBuf = Buffer.allocUnsafe(this.cacheLen * (bitrate - this.bitrate)).fill(0);
|
||||
log.info("AudioCache: buffer expanded from " + this.buffer.length + " to " + (this.buffer.length + expandBuf.length) + " bytes");
|
||||
this.buffer = Buffer.concat([ this.buffer, expandBuf ]);
|
||||
}
|
||||
this.bitrate = bitrate;
|
||||
}
|
||||
}
|
||||
|
||||
_write(data, enc, next) {
|
||||
if (this.writeCursor + data.length > this.buffer.length) {
|
||||
log.warn("AudioCache: _write: buffer overflow wC=" + this.writeCursor + " dL=" + data.length + " bL=" + this.buffer.length);
|
||||
@@ -27,23 +38,6 @@ class AudioCache extends Writable {
|
||||
this.writeCursor += data.length;
|
||||
|
||||
//log.debug("AudioCache: _write: add " + data.length + " to buffer, new len=" + this.buffer.length);
|
||||
if (this.writeCursor >= this.flushAmount && !this.bitrateValidated) {
|
||||
var self = this;
|
||||
this.evalBitrate(this.buffer, function(bitrate) {
|
||||
if (!isNaN(bitrate) && bitrate > 0 && self.bitrate != bitrate) {
|
||||
log.info("AudioCache: bitrate adjusted from " + self.bitrate + "bps to " + bitrate + "bps");
|
||||
|
||||
// if bitrate is higher than expected, expand the buffer accordingly.
|
||||
if (bitrate > self.bitrate) {
|
||||
var expandBuf = Buffer.allocUnsafe(self.cacheLen * (bitrate - self.bitrate)).fill(0);
|
||||
log.info("AudioCache: buffer expanded from " + self.buffer.length + " to " + (self.buffer.length + expandBuf.length) + " bytes");
|
||||
self.buffer = Buffer.concat([ self.buffer, expandBuf ]);
|
||||
}
|
||||
self.bitrate = bitrate;
|
||||
}
|
||||
});
|
||||
this.bitrateValidated = true;
|
||||
}
|
||||
|
||||
if (this.writeCursor >= this.cacheLen * this.bitrate + this.flushAmount) {
|
||||
//log.debug("AudioCache: _write: cutting buffer at len = " + this.cacheLen * this.bitrate);
|
||||
@@ -111,6 +105,9 @@ class MetaCache extends Writable {
|
||||
if (!meta.type) {
|
||||
log.error("MetaCache: no data type");
|
||||
return next();
|
||||
} else if (!meta.payload) {
|
||||
log.warn("MetaCache: empty " + meta.type + " payload");
|
||||
return next();
|
||||
} else if (meta.validFrom > meta.validTo) {
|
||||
log.error("MetaCache: negative time window validFrom=" + meta.validFrom + " validTo=" + meta.validTo);
|
||||
return next();
|
||||
@@ -143,12 +140,15 @@ class MetaCache extends Writable {
|
||||
case "metadata":
|
||||
case "class":
|
||||
case "volume":
|
||||
if (!this.meta[meta.type]) {
|
||||
const curMeta = this.meta[meta.type];
|
||||
//log.debug("MetaCache: curMeta=" + JSON.stringify(curMeta));
|
||||
if (!curMeta) {
|
||||
this.meta[meta.type] = [ { validFrom: meta.validFrom, validTo: meta.validTo, payload: meta.payload } ];
|
||||
} else {
|
||||
var samePayload = true;
|
||||
|
||||
for (var key in meta.payload) {
|
||||
if ("" + meta.payload[key] && "" + meta.payload[key] !== "" + this.meta[meta.type][this.meta[meta.type].length-1].payload[key]) {
|
||||
if ("" + meta.payload[key] && "" + meta.payload[key] !== "" + curMeta[curMeta.length-1].payload[key]) {
|
||||
samePayload = false;
|
||||
//log.debug("MetaCache: _write: different payload key=" + key + " new=" + meta.payload[key] + " vs old=" + this.meta[meta.type][this.meta[meta.type].length-1].payload[key]);
|
||||
break;
|
||||
@@ -223,13 +223,12 @@ class MetaCache extends Writable {
|
||||
}
|
||||
|
||||
|
||||
const addRadio = function(country, name) {
|
||||
const startMonitoring = function(country, name) {
|
||||
const abr = new Analyser({
|
||||
country: country,
|
||||
name: name,
|
||||
config: {
|
||||
predInterval: 2,
|
||||
saveDuration: 5,
|
||||
predInterval: config.user.streamGranularity,
|
||||
enablePredictorHotlist: true,
|
||||
enablePredictorMl: true,
|
||||
saveAudio: false,
|
||||
@@ -239,38 +238,48 @@ const addRadio = function(country, name) {
|
||||
}
|
||||
});
|
||||
|
||||
abr.audioCache = new AudioCache({ bitrate: options.bitrate, cacheLen: config.user.cacheLen });
|
||||
abr.metaCache = new MetaCache({ cacheLen: config.user.cacheLen });
|
||||
const audioCache = new AudioCache({ cacheLen: config.user.cacheLen });
|
||||
const metaCache = new MetaCache({ cacheLen: config.user.cacheLen });
|
||||
|
||||
abr.on("data", function(obj) {
|
||||
//obj.liveResult.audio = "[redacted]";
|
||||
obj = obj.liveResult;
|
||||
//log.info("status=" + JSON.stringify(Object.assign(obj, { audio: undefined }), null, "\t"));
|
||||
|
||||
db.audioCache.write(dataObj.data);
|
||||
audioCache.setBitrate(obj.bitrate);
|
||||
if (obj.audio) audioCache.write(obj.audio);
|
||||
// todo update bitrate here. set audioCache in Object mode
|
||||
|
||||
const now = +new Date();
|
||||
abr.metaCache.write({
|
||||
const validFrom = now - 1000 * config.user.streamGranularity / 2;
|
||||
const validTo = now + 1000 * config.user.streamGranularity / 2;
|
||||
metaCache.write({
|
||||
type: "class",
|
||||
validFrom: now-500*options.segDuration,
|
||||
validTo: now+500*options.segDuration,
|
||||
payload: className
|
||||
validFrom: validFrom,
|
||||
validTo: validTo,
|
||||
payload: obj.class
|
||||
});
|
||||
abr.metaCache.write({
|
||||
metaCache.write({
|
||||
type: "volume",
|
||||
validFrom: now-500*options.segDuration,
|
||||
validTo: now+500*options.segDuration,
|
||||
payload: volume
|
||||
validFrom: validFrom,
|
||||
validTo: validTo,
|
||||
payload: obj.gain
|
||||
});
|
||||
abr.metaCache.write({
|
||||
metaCache.write({
|
||||
type: "metadata",
|
||||
validFrom: now-500*options.segDuration,
|
||||
validTo: now+500*options.segDuration,
|
||||
payload: parsedMeta
|
||||
validFrom: validFrom,
|
||||
validTo: validTo,
|
||||
payload: obj.metadata
|
||||
});
|
||||
});
|
||||
|
||||
dl.push(abr);
|
||||
return {
|
||||
predictor: abr,
|
||||
audioCache: audioCache,
|
||||
metaCache: metaCache,
|
||||
}
|
||||
|
||||
//dl.push(abr);
|
||||
|
||||
/*dl.push(DlFactory(config.radios[i], {
|
||||
fetchMetadata: FETCH_METADATA,
|
||||
@@ -280,29 +289,8 @@ const addRadio = function(country, name) {
|
||||
}));*/
|
||||
}
|
||||
|
||||
exports.startMonitoring = startMonitoring;
|
||||
|
||||
var updateDlList = function(forcePlaylistUpdate) {
|
||||
//var playlistChange = false || !!forcePlaylistUpdate;
|
||||
|
||||
// add missing sockets
|
||||
for (var i=0; i<config.radios.length; i++) {
|
||||
const alreadyThere = config.radios.filter(r => r.country === dl[j].country && r.name === dl[j].name).length > 0;
|
||||
if (!alreadyThere) {
|
||||
config.radios[i].liveStatus = {};
|
||||
log.info("updateDlList: start " + config.radios[i].country + "_" + config.radios[i].name);
|
||||
addRadio(config.radios[i].country, config.radios[i].name);
|
||||
}
|
||||
}
|
||||
|
||||
// remove obsolete ones.
|
||||
for (var j=dl.length-1; j>=0; j--) {
|
||||
const shouldBeThere = config.radios.filter(r => r.country === dl[j].country && r.name === dl[j].name).length > 0;
|
||||
if (!shouldBeThere) {
|
||||
log.info("updateDlList: stop " + dl[j].country + "_" + dl[j].name);
|
||||
dl[j].stopDl();
|
||||
dl.splice(j, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.updateDlList = updateDlList;
|
||||
//exports.updateDlList = updateDlList;
|
||||
+80
-16
@@ -1,17 +1,30 @@
|
||||
"use strict";
|
||||
|
||||
var { log } = require("./log.js")("config");
|
||||
var fs = require("fs");
|
||||
var { getRadioMetadata } = require("adblockradio-dl");
|
||||
var { getAvailable } = require("webradio-metadata");
|
||||
var jwt = require("jsonwebtoken");
|
||||
const { log } = require("abr-log")("config");
|
||||
const fs = require("fs");
|
||||
const { getRadioMetadata } = require("stream-tireless-baler");
|
||||
//const { getAvailable } = require("webradio-metadata");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const axios = require("axios");
|
||||
|
||||
|
||||
// list of listened radios:
|
||||
var config = new Object();
|
||||
try {
|
||||
var radiosText = fs.readFileSync("config/radios.json");
|
||||
config.radios = JSON.parse(radiosText);
|
||||
try {
|
||||
var radiosText = fs.readFileSync("config/radios.json");
|
||||
config.radios = JSON.parse(radiosText);
|
||||
} catch (e) {
|
||||
config.radios = [];
|
||||
log.warn("could not load radio playlist (this is fine on first startup)");
|
||||
}
|
||||
try {
|
||||
var availableText = fs.readFileSync("config/available.json");
|
||||
config.available = JSON.parse(availableText);
|
||||
} catch (e) {
|
||||
config.available = [];
|
||||
log.warn("could not load list of available radios (this is fine on first startup)");
|
||||
}
|
||||
var userText = fs.readFileSync("config/user.json");
|
||||
config.user = JSON.parse(userText);
|
||||
if (config.user.token) {
|
||||
@@ -24,13 +37,15 @@ try {
|
||||
} else {
|
||||
config.user.email = "";
|
||||
}
|
||||
|
||||
} catch(e) {
|
||||
//log.info("load config with radios " + config.radios.map(r => r.country + "_" + r.name).join(" "));
|
||||
} catch (e) {
|
||||
return log.error("cannot load config. err=" + e);
|
||||
}
|
||||
|
||||
exports.config = config;
|
||||
|
||||
const { updateDlList } = require("./cache");
|
||||
|
||||
var isRadioInConfig = function(country, name) {
|
||||
var isAlreadyThere = false;
|
||||
for (var i=0; i<config.radios.length; i++) {
|
||||
@@ -100,16 +115,16 @@ exports.toggleContent = function(country, name, type, enable, callback) {
|
||||
}
|
||||
|
||||
exports.getRadios = function() {
|
||||
var radios = [];
|
||||
for (var i=0; i<config.radios.length; i++) { // control on what data is exposed via the api
|
||||
var radio = config.radios[i];
|
||||
let radios = [];
|
||||
for (let i=0; i<config.radios.length; i++) { // control on what data is exposed via the api
|
||||
const radio = config.radios[i];
|
||||
radios.push({
|
||||
country: radio.country,
|
||||
name: radio.name,
|
||||
content: radio.content,
|
||||
url: radio.enabled,
|
||||
url: radio.enabled, // TODO duh?
|
||||
favicon: radio.favicon,
|
||||
codec: radio.codec
|
||||
codec: radio.codec,
|
||||
});
|
||||
}
|
||||
return radios;
|
||||
@@ -148,10 +163,59 @@ var saveRadios = function() {
|
||||
log.debug("saveRadios: config saved");
|
||||
}
|
||||
});
|
||||
|
||||
// refresh the list of monitored radios
|
||||
updateDlList();
|
||||
}
|
||||
|
||||
var getAvailableInactive = function() {
|
||||
var available = getAvailable();
|
||||
//updateDlList();
|
||||
|
||||
// function that calls an API to get metadata about a radio
|
||||
/*const getRadioMetadata = async function(country, name) {
|
||||
try {
|
||||
const API_PATH = "http://www.radio-browser.info/webservice/json/stations/bynameexact/";
|
||||
const result = await fetch(API_PATH + encodeURIComponent(name));
|
||||
const results = JSON.parse(result);
|
||||
const i = results.map(e => e.country).indexOf(country);
|
||||
|
||||
if (i >= 0) return results[i];
|
||||
log.error("getRadioMetadata: radio not found: " + results);
|
||||
return null;
|
||||
} catch (e) {
|
||||
log.warn("Could not get metadata for radio " + country + "_" + name + ". err=" + e);
|
||||
return null;
|
||||
}
|
||||
}*/
|
||||
|
||||
const saveAvailable = function() {
|
||||
fs.writeFile("config/available.json", JSON.stringify(config.available, null, '\t'), function(err) {
|
||||
if (err) {
|
||||
log.error("saveRadios: could not save available radios config. err=" + err);
|
||||
} else {
|
||||
log.debug("saveRadios: list of available radios saved");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const getAvailable = async function() {
|
||||
// fetch the list of available models on remote model repo
|
||||
const path = config.user.modelRepo + "list.json";
|
||||
try {
|
||||
const req = await axios.get(path);
|
||||
config.available = req.data;
|
||||
} catch (e) {
|
||||
log.warn('could not get list of available radios at path ' + path + '. e=' + e);
|
||||
}
|
||||
saveAvailable();
|
||||
}
|
||||
|
||||
getAvailable();
|
||||
setInterval(getAvailable, 1000 * 60 * 60); // refresh every hour
|
||||
|
||||
const getAvailableInactive = function() {
|
||||
|
||||
let available = config.available.slice();
|
||||
|
||||
// remove radios that are currently in playlist
|
||||
for (let i=available.length-1; i>=0; i--) {
|
||||
let itemInPlaylist = false;
|
||||
|
||||
@@ -4,111 +4,53 @@
|
||||
"use strict";
|
||||
|
||||
const { log } = require('abr-log')('main');
|
||||
const { config } = require('./handlers/config'); /*(function(country, name) {
|
||||
|
||||
const FETCH_METADATA = true;
|
||||
const SAVE_AUDIO = false;
|
||||
const SEG_DURATION = 10; // in seconds
|
||||
const LISTEN_BUFFER = 30; // in seconds
|
||||
var USE_ABRSDK = true;
|
||||
}, function(radioObj) {
|
||||
|
||||
var { config } = require("./handlers/config.js");
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
if (USE_ABRSDK && playlistChange) {
|
||||
var playlistArray = [];
|
||||
for (var i=0; i<config.radios.length; i++) {
|
||||
playlistArray.push(config.radios[i].country + "_" + config.radios[i].name);
|
||||
}
|
||||
|
||||
var predInterval = setInterval(function() {
|
||||
let age = +new Date() - lastPrediction;
|
||||
if (age > 15000) {
|
||||
log.warn("abrsdk: last prediction received " + Math.round(age/1000) + "s ago");
|
||||
clearInterval(predInterval);
|
||||
updateDlList(true);
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
abrsdk.sendPlaylist(playlistArray, config.user.token, function(err, validatedPlaylist) {
|
||||
if (err) {
|
||||
log.warn("abrsdk: sendPlaylist error = " + err);
|
||||
} else {
|
||||
if (playlistArray.length != validatedPlaylist.length) {
|
||||
log.warn("abrsdk: playlist not accepted. requested=" + JSON.stringify(playlistArray) + " validated=" + JSON.stringify(validatedPlaylist));
|
||||
} else {
|
||||
log.debug("abrsdk: playlist successfully updated");
|
||||
}
|
||||
|
||||
abrsdk.setPredictionCallback(function(predictions) {
|
||||
let age = +new Date() - lastPrediction;
|
||||
if (age > 15000) log.warn("abrsdk: received prediction after a blackout of " + Math.round(age/1000) + "s");
|
||||
lastPrediction = new Date();
|
||||
|
||||
var status, volume;
|
||||
for (var i=0; i<predictions.radios.length; i++) {
|
||||
switch (predictions.status[i]) {
|
||||
case abrsdk.statusList.STATUS_AD: status = "AD"; break;
|
||||
case abrsdk.statusList.STATUS_SPEECH: status = "SPEECH"; break;
|
||||
case abrsdk.statusList.STATUS_MUSIC: status = "MUSIC"; break;
|
||||
default: status = "not available";
|
||||
}
|
||||
// normalized volume to apply to the audio tag to have similar loudness between channels
|
||||
volume = Math.pow(10, (Math.min(abrsdk.GAIN_REF-predictions.gain[i],0))/20);
|
||||
// you can now plug the data to your radio player.
|
||||
//log.debug("abrsdk: " + predictions.radios[i] + " has status " + status + " and volume " + Math.round(volume*100)/100);
|
||||
var radio = getRadio(predictions.radios[i]);
|
||||
if (!radio || !radio.liveStatus || !radio.liveStatus.onClassPrediction) {
|
||||
log.error("abrsdk: cannot call prediction callback");
|
||||
} else {
|
||||
radio.liveStatus.onClassPrediction(status, volume);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (USE_ABRSDK && config.user.email) {
|
||||
log.info("abrsdk: token detected for email " + config.user.email);
|
||||
abrsdk.connectServer(function(err, isConnected) {
|
||||
//abrsdk._newSocket(["http://localhost:3066/"], 0, function(err, isConnected) {
|
||||
if (err) {
|
||||
log.error("abrsdk: connection error: " + err + ". switch off sdk");
|
||||
USE_ABRSDK = false;
|
||||
}
|
||||
if (isConnected) {
|
||||
updateDlList(true);
|
||||
} else {
|
||||
log.warn("SDK disconnected");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
updateDlList(false);
|
||||
}*/
|
||||
});*/
|
||||
const { startMonitoring } = require('./handlers/cache');
|
||||
|
||||
// start http server
|
||||
const { app } = require('handlers/app');
|
||||
const { app } = require('./handlers/app');
|
||||
|
||||
try {
|
||||
require('api/config')(app);
|
||||
require('api/content')(app);
|
||||
require('api/listen')(app);
|
||||
require('api/radios')(app);
|
||||
require('api/status')(app);
|
||||
require('./api/config')(app);
|
||||
require('./api/content')(app);
|
||||
require('./api/listen')(app);
|
||||
require('./api/radios')(app);
|
||||
require('./api/status')(app);
|
||||
} catch (e) {
|
||||
log.warn('API error. e=' + e);
|
||||
}
|
||||
|
||||
var terminateServer = function(signal) {
|
||||
log.info("received SIGTERM signal. exiting...");
|
||||
for (var i=0; i<dl.length; i++) {
|
||||
dl[i].stopDl();
|
||||
}
|
||||
}
|
||||
// starts downloading and analysing streams
|
||||
//updateDlList();
|
||||
|
||||
process.on('SIGTERM', terminateServer);
|
||||
process.on('SIGINT', terminateServer);
|
||||
const updateDlList = function() { //forcePlaylistUpdate) {
|
||||
log.info("refresh playlist");
|
||||
//var playlistChange = false || !!forcePlaylistUpdate;
|
||||
|
||||
const configList = config.radios.map(r => r.country + "_" + r.name);
|
||||
const currentList = config.radios.filter(r => r.liveStatus).map(r => r.country + "_" + r.name);
|
||||
|
||||
// add missing monitors
|
||||
for (var i=0; i<configList.length; i++) {
|
||||
const alreadyThere = currentList.includes(configList[i]); //.filter(r => r.country === dl[j].country && r.name === dl[j].name).length > 0;
|
||||
if (!alreadyThere) {
|
||||
log.info("updateDlList: start " + config.radios[i].country + "_" + config.radios[i].name);
|
||||
config.radios[i].liveStatus = startMonitoring(config.radios[i].country, config.radios[i].name);
|
||||
}
|
||||
}
|
||||
|
||||
// remove obsolete ones.
|
||||
for (var j=currentList-1; j>=0; j--) {
|
||||
const shouldBeThere = configList.includes(currentList[j]);
|
||||
if (!shouldBeThere) {
|
||||
log.info("updateDlList: stop " + dl[j].country + "_" + dl[j].name);
|
||||
const obj = config.radios.filter(r => r.country + "_" + r.name === currentList[j])[0];
|
||||
obj.predictor.stopDl();
|
||||
delete obj.liveStatus;
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1640
-907
File diff suppressed because it is too large
Load Diff
+9
-11
@@ -9,18 +9,16 @@
|
||||
"author": "Alexandre Storelli <a_npm@storelli.fr>",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"adblockradio-dl": "./adblockradio-dl",
|
||||
"adblockradio-sdk": "git://github.com/dest4/adblockradio-sdk.git#master",
|
||||
"async": "^2.6.0",
|
||||
"chalk": "^2.3.2",
|
||||
"express": "^4.16.3",
|
||||
"jsonwebtoken": "^8.2.0",
|
||||
"moment": "^2.21.0",
|
||||
"webradio-metadata": "^0.1.15",
|
||||
"winston": "^3.0.0-rc3",
|
||||
"winston-daily-rotate-file": "^3.0.1"
|
||||
"abr-log": "^1.0.2",
|
||||
"async": "^2.6.1",
|
||||
"axios": "^0.18.0",
|
||||
"express": "^4.16.4",
|
||||
"http-proxy-middleware": "^0.19.0",
|
||||
"jsonwebtoken": "^8.3.0",
|
||||
"stream-tireless-baler": "^1.0.14",
|
||||
"webradio-metadata": "^0.1.31"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^4.19.1"
|
||||
"eslint": "^5.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user