revamp continued. still WIP

This commit is contained in:
Alexandre Storelli
2018-11-06 23:56:58 +01:00
parent 496d4ca496
commit b20b14cb3f
17 changed files with 14487 additions and 7853 deletions
-434
View File
@@ -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
View File
@@ -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);
+12423 -6105
View File
File diff suppressed because it is too large Load Diff
+16 -10
View File
@@ -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"
]
}
+35
View File
@@ -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
View File
@@ -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
View File
@@ -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}
/>
&nbsp; {{ 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 &&
+3 -3
View File
@@ -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
View File
@@ -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>
)
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+39 -97
View File
@@ -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;
}
}
}
+1640 -907
View File
File diff suppressed because it is too large Load Diff
+9 -11
View File
@@ -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"
}
}