improved logging, toggleContent, UI tweaks
This commit is contained in:
+1
-2
@@ -1,8 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
var { Writable, Duplex } = require("stream");
|
||||
var log = require("loglevel");
|
||||
log.setLevel("debug");
|
||||
var log = require("./log.js")("DlFactory");
|
||||
var cp = require("child_process");
|
||||
var fs = require("fs");
|
||||
var { getMeta } = require("webradio-metadata");
|
||||
|
||||
Generated
+65
-5
@@ -77,6 +77,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"add-dom-event-listener": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/add-dom-event-listener/-/add-dom-event-listener-1.0.2.tgz",
|
||||
"integrity": "sha1-j67SxBAIchzxEdodMNmVuFvkK+0=",
|
||||
"requires": {
|
||||
"object-assign": "4.1.1"
|
||||
}
|
||||
},
|
||||
"address": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/address/-/address-1.0.3.tgz",
|
||||
@@ -1234,7 +1242,6 @@
|
||||
"version": "6.26.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
|
||||
"integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"core-js": "2.5.1",
|
||||
"regenerator-runtime": "0.11.0"
|
||||
@@ -1243,8 +1250,7 @@
|
||||
"core-js": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.1.tgz",
|
||||
"integrity": "sha1-rmh03GaTd4m4B1T/VCjfZoGcpQs=",
|
||||
"dev": true
|
||||
"integrity": "sha1-rmh03GaTd4m4B1T/VCjfZoGcpQs="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -5629,6 +5635,11 @@
|
||||
"integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=",
|
||||
"dev": true
|
||||
},
|
||||
"lodash._getnative": {
|
||||
"version": "3.9.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz",
|
||||
"integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U="
|
||||
},
|
||||
"lodash._reinterpolate": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
|
||||
@@ -5653,6 +5664,26 @@
|
||||
"integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.isarguments": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
|
||||
"integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo="
|
||||
},
|
||||
"lodash.isarray": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz",
|
||||
"integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U="
|
||||
},
|
||||
"lodash.keys": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz",
|
||||
"integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=",
|
||||
"requires": {
|
||||
"lodash._getnative": "3.9.1",
|
||||
"lodash.isarguments": "3.1.0",
|
||||
"lodash.isarray": "3.0.4"
|
||||
}
|
||||
},
|
||||
"lodash.memoize": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||
@@ -8146,6 +8177,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"rc-checkbox": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-2.1.4.tgz",
|
||||
"integrity": "sha512-a5rDubpIsNzx/8u7yk8rD19RyZOM3uxqwXsUUfRzNhO+wD3Y0xntqRXqNPZ/dYQqXWoK/oRJrqghst1/8pFqZQ==",
|
||||
"requires": {
|
||||
"babel-runtime": "6.26.0",
|
||||
"classnames": "2.2.5",
|
||||
"prop-types": "15.6.0",
|
||||
"rc-util": "4.3.1"
|
||||
}
|
||||
},
|
||||
"rc-util": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/rc-util/-/rc-util-4.3.1.tgz",
|
||||
"integrity": "sha512-OVNMKLePnwn0dCX/Gpc+/kGEDpmMo1Rfesg9xFcAckRd+D+YwVqV+dUJMHugP+4nRtbXi55o0HwPlkKIApYfQA==",
|
||||
"requires": {
|
||||
"add-dom-event-listener": "1.0.2",
|
||||
"babel-runtime": "6.26.0",
|
||||
"prop-types": "15.6.0",
|
||||
"shallowequal": "0.2.2"
|
||||
}
|
||||
},
|
||||
"react": {
|
||||
"version": "16.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-16.1.1.tgz",
|
||||
@@ -8409,8 +8462,7 @@
|
||||
"regenerator-runtime": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz",
|
||||
"integrity": "sha512-/aA0kLeRb5N9K0d4fw7ooEbI+xDe+DKD499EQqygGqeS8N3xto15p09uY2xj7ixP81sNPXvRLnAQIqdVStgb1A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-/aA0kLeRb5N9K0d4fw7ooEbI+xDe+DKD499EQqygGqeS8N3xto15p09uY2xj7ixP81sNPXvRLnAQIqdVStgb1A=="
|
||||
},
|
||||
"regenerator-transform": {
|
||||
"version": "0.10.1",
|
||||
@@ -8894,6 +8946,14 @@
|
||||
"safe-buffer": "5.1.1"
|
||||
}
|
||||
},
|
||||
"shallowequal": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-0.2.2.tgz",
|
||||
"integrity": "sha1-HjL9W8q2rWiKSBLLDMBO/HXHAU4=",
|
||||
"requires": {
|
||||
"lodash.keys": "3.1.2"
|
||||
}
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
|
||||
|
||||
+2
-1
@@ -5,6 +5,7 @@
|
||||
"dependencies": {
|
||||
"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"
|
||||
@@ -18,5 +19,5 @@
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"homepage" : "https://www.adblockradio.com/buffer"
|
||||
"homepage": "https://www.adblockradio.com/buffer"
|
||||
}
|
||||
|
||||
+79
-58
@@ -40,6 +40,7 @@ class App extends Component {
|
||||
this.refreshConfig = this.refreshConfig.bind(this);
|
||||
this.insertRadio = this.insertRadio.bind(this);
|
||||
this.removeRadio = this.removeRadio.bind(this);
|
||||
this.toggleContent = this.toggleContent.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -169,6 +170,13 @@ class App extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let config = this.state.config;
|
||||
var self = this;
|
||||
@@ -225,65 +233,76 @@ class App extends Component {
|
||||
|
||||
//console.log("Metadata props: date=" + (+self.state.date) + " clockDiff=" + self.state.clockDiff + " playingDelay=" + self.state.playingDelay);
|
||||
|
||||
let mainContents;
|
||||
if (self.state.playlistEditMode || config.radios.length === 0) {
|
||||
mainContents = (
|
||||
<Playlist config={self.state.config}
|
||||
insertRadio={self.insertRadio}
|
||||
removeRadio={self.removeRadio}
|
||||
toggleContent={self.toggleContent} />
|
||||
);
|
||||
} else {
|
||||
mainContents = (
|
||||
<RadioList>
|
||||
{config.radios.map(function(radioObj, i) {
|
||||
var radio = radioObj.country + "_" + radioObj.name;
|
||||
var playing = self.state.playingRadio === radio;
|
||||
|
||||
var liveMetadata;
|
||||
|
||||
var metaList = self.state[radio + "|metadata"];
|
||||
if (metaList) {
|
||||
var targetDate = self.state.date - (playing ? self.state.playingDelay : self.defaultDelay(radio));
|
||||
for (let j=0; j<metaList.length; j++) {
|
||||
if (metaList[j].validFrom - 1000 <= targetDate &&
|
||||
(!metaList[j].validTo || (targetDate < +metaList[j].validTo - 1000)))
|
||||
{
|
||||
liveMetadata = metaList[j];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<RadioItem className={classNames({ playing: playing })}
|
||||
id={"RadioItem" + i}
|
||||
key={"RadioItem" + i}>
|
||||
|
||||
<RadioItemTopLine onClick={function() { self.play(radio); }}>
|
||||
<RadioLogo src={radioObj.favicon} alt="logo" />
|
||||
{liveMetadata &&
|
||||
<MetadataItem>
|
||||
<MetadataText>
|
||||
{liveMetadata.payload.artist} - {liveMetadata.payload.title}
|
||||
</MetadataText>
|
||||
<MetadataCover src={liveMetadata.payload.cover || defaultCover} alt="logo" />
|
||||
</MetadataItem>
|
||||
}
|
||||
</RadioItemTopLine>
|
||||
|
||||
{metaList &&
|
||||
<DelayCanvas playingDelay={self.state.playingDelay}
|
||||
availableCache={self.state[radio + "|available"]}
|
||||
classList={self.state[radio + "|class"]}
|
||||
date={new Date(+self.state.date - self.state.clockDiff)}
|
||||
playing={playing}
|
||||
cacheLen={self.state.config.user.cacheLen}
|
||||
width={self.state.canvasWidth || 100}
|
||||
playCallback={function(delay) { self.play(radio, delay); }} />
|
||||
}
|
||||
|
||||
</RadioItem>
|
||||
)
|
||||
})}
|
||||
</RadioList>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<AppParent>
|
||||
<AppView>
|
||||
<RadioList>
|
||||
{config.radios.map(function(radioObj, i) {
|
||||
var radio = radioObj.country + "_" + radioObj.name;
|
||||
var playing = self.state.playingRadio === radio;
|
||||
var showMetadata = !self.state.playlistEditMode;
|
||||
var liveMetadata;
|
||||
if (showMetadata) {
|
||||
var metaList = self.state[radio + "|metadata"];
|
||||
if (metaList) {
|
||||
for (let j=0; j<metaList.length; j++) {
|
||||
if (metaList[j].validFrom - 1000 <= (self.state.date-self.defaultDelay(radio)) &&
|
||||
(!metaList[j].validTo || (self.state.date-self.defaultDelay(radio) < +metaList[j].validTo - 1000)))
|
||||
{
|
||||
liveMetadata = metaList[j];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<RadioItem className={classNames({ playing: playing })}
|
||||
id={"RadioItem" + i}
|
||||
key={"RadioItem" + i}>
|
||||
|
||||
<RadioItemTopLine onClick={function() { self.play(radio); }}>
|
||||
<RadioLogo src={radioObj.favicon} alt="logo" />
|
||||
{showMetadata && liveMetadata &&
|
||||
<MetadataItem>
|
||||
<MetadataText>
|
||||
{liveMetadata.payload.artist} - {liveMetadata.payload.title}
|
||||
</MetadataText>
|
||||
<MetadataCover src={liveMetadata.payload.cover || defaultCover} alt="logo" />
|
||||
</MetadataItem>
|
||||
}
|
||||
</RadioItemTopLine>
|
||||
|
||||
{showMetadata && self.state[radio + "|metadata"] &&
|
||||
<DelayCanvas playingDelay={self.state.playingDelay}
|
||||
availableCache={self.state[radio + "|available"]}
|
||||
classList={self.state[radio + "|class"]}
|
||||
date={new Date(+self.state.date - self.state.clockDiff)}
|
||||
playing={playing}
|
||||
cacheLen={self.state.config.user.cacheLen}
|
||||
width={self.state.canvasWidth || 100}
|
||||
playCallback={function(delay) { self.play(radio, delay); }} />
|
||||
}
|
||||
|
||||
</RadioItem>
|
||||
)
|
||||
})}
|
||||
</RadioList>
|
||||
{(self.state.playlistEditMode || config.radios.length === 0) &&
|
||||
<Playlist config={self.state.config}
|
||||
insertRadio={self.insertRadio}
|
||||
removeRadio={self.removeRadio} />
|
||||
}
|
||||
{mainContents}
|
||||
</AppView>
|
||||
<Controls>
|
||||
{status}
|
||||
@@ -312,7 +331,8 @@ const AppParent = styled.div`
|
||||
|
||||
const AppView = styled.div`
|
||||
display: flex;
|
||||
height: calc(100% - 100px);
|
||||
height: calc(100% - 60px);
|
||||
margin: 0 10px;
|
||||
`;
|
||||
|
||||
const RadioList = styled.div`
|
||||
@@ -355,7 +375,7 @@ const RadioLogo = styled.img`
|
||||
const MetadataItem = styled.div`
|
||||
flex-grow: 1;
|
||||
margin: 0 0 0 15px;
|
||||
padding: 10px;
|
||||
padding: 0 10px;
|
||||
flex-shrink: 1;
|
||||
background: #eee;
|
||||
display: flex;
|
||||
@@ -365,6 +385,7 @@ const MetadataItem = styled.div`
|
||||
const MetadataText = styled.p`
|
||||
flex-grow: 1;
|
||||
align-self: center;
|
||||
margin: 10px 0;
|
||||
`
|
||||
|
||||
const MetadataCover = styled.img`
|
||||
|
||||
+43
-11
@@ -5,6 +5,8 @@ import PropTypes from "prop-types";
|
||||
import styled from "styled-components";
|
||||
import { load } from './load.js';
|
||||
import classNames from 'classnames';
|
||||
import Checkbox from 'rc-checkbox';
|
||||
import 'rc-checkbox/assets/index.css';
|
||||
import defaultCover from "./img/default_radio_logo.svg";
|
||||
|
||||
class Playlist extends Component {
|
||||
@@ -41,6 +43,11 @@ class Playlist extends Component {
|
||||
this.props.removeRadio(country, name, this.componentDidMount);
|
||||
}
|
||||
|
||||
/*toggleContent(country, name, contentType, enabled) {
|
||||
console.log("toggleContent radio=" + country + "_" + name + " contentType=" + contentType + " enable=" + enabled);
|
||||
this.props.toggleContent(country, name, contentType, enabled, this.componentDidMount);
|
||||
}*/
|
||||
|
||||
render() {
|
||||
var self = this;
|
||||
if (!this.state.radiosLoaded) {
|
||||
@@ -67,11 +74,23 @@ class Playlist extends Component {
|
||||
}
|
||||
{current.map(function(radio, i) {
|
||||
return (
|
||||
<PlaylistItem className={classNames({ active: true })} key={"item" + i} onClick={function() { self.remove(radio.country, radio.name); }}>
|
||||
<PlaylistItemText>
|
||||
{radio.name}
|
||||
</PlaylistItemText>
|
||||
<PlaylistItemLogo src={radio.favicon || defaultCover} alt="logo" />
|
||||
<PlaylistItem className={classNames({ active: true })} key={"item" + i}>
|
||||
<PlaylistItemTopRow>
|
||||
<PlaylistItemLogo src={radio.favicon || defaultCover} alt="logo" onClick={function() { self.remove(radio.country, radio.name); }} />
|
||||
<PlaylistItemText onClick={function() { self.remove(radio.country, radio.name); }}>
|
||||
{radio.name}
|
||||
</PlaylistItemText>
|
||||
</PlaylistItemTopRow>
|
||||
<PlaylistItemConfigContainer>
|
||||
<label>
|
||||
<Checkbox
|
||||
checked={!radio.content.ads}
|
||||
onChange={(e) => self.props.toggleContent(radio.country, radio.name, "ads", !e.target.checked, self.componentDidMount)}
|
||||
disabled={!self.props.config.user.token}
|
||||
/>
|
||||
skip ads
|
||||
</label>
|
||||
</PlaylistItemConfigContainer>
|
||||
</PlaylistItem>
|
||||
)
|
||||
})}
|
||||
@@ -83,10 +102,12 @@ class Playlist extends Component {
|
||||
{available.map(function(radio, i) {
|
||||
return (
|
||||
<PlaylistItem className={classNames({ active: !playlistFull })} key={"item" + i} onClick={function() { if (!playlistFull) self.insert(radio.country, radio.name); }}>
|
||||
<PlaylistItemText>
|
||||
{radio.name}
|
||||
</PlaylistItemText>
|
||||
<PlaylistItemLogo src={radio.favicon || defaultCover} alt="logo" />
|
||||
<PlaylistItemTopRow>
|
||||
<PlaylistItemLogo src={radio.favicon || defaultCover} alt="logo" />
|
||||
<PlaylistItemText>
|
||||
{radio.name}
|
||||
</PlaylistItemText>
|
||||
</PlaylistItemTopRow>
|
||||
</PlaylistItem>
|
||||
)
|
||||
})}
|
||||
@@ -118,6 +139,7 @@ const PlaylistItem = styled.div`
|
||||
flex-shrink: 0;
|
||||
background: #eee;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: not-allowed;
|
||||
|
||||
&.active {
|
||||
@@ -125,16 +147,26 @@ const PlaylistItem = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const PlaylistItemTopRow = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 10px;
|
||||
`;
|
||||
|
||||
const PlaylistItemText = styled.p`
|
||||
flex-grow: 1;
|
||||
align-self: center;
|
||||
`
|
||||
`;
|
||||
|
||||
const PlaylistItemLogo = styled.img`
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
align-self: center;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
`;
|
||||
|
||||
const PlaylistItemConfigContainer = styled.div`
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
export default Playlist;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use strict";
|
||||
|
||||
var log = require("loglevel");
|
||||
var log = require("./log.js")("config");
|
||||
var fs = require("fs");
|
||||
var { getRadioMetadata } = require("../adblockradio-dl/dl.js");
|
||||
var { getAvailable } = require("webradio-metadata");
|
||||
@@ -30,7 +30,7 @@ var isRadioInConfig = function(country, name) {
|
||||
return isAlreadyThere;
|
||||
}
|
||||
|
||||
var insertRadio = function(country, name, callback) {
|
||||
exports.insertRadio = function(country, name, callback) {
|
||||
if (isRadioInConfig()) return callback("Radio already in the list");
|
||||
if (config.radios.length >= config.user.maxRadios) return callback("Playlist is already full");
|
||||
|
||||
@@ -43,7 +43,7 @@ var insertRadio = function(country, name, callback) {
|
||||
"country": country,
|
||||
"name": name,
|
||||
"content": {
|
||||
"ads": true,
|
||||
"ads": false,
|
||||
"speech": true,
|
||||
"music": true
|
||||
},
|
||||
@@ -55,7 +55,7 @@ var insertRadio = function(country, name, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
var removeRadio = function(country, name, callback) {
|
||||
exports.removeRadio = function(country, name, callback) {
|
||||
|
||||
for (var i=0; i<config.radios.length; i++) {
|
||||
if (config.radios[i].country == country && config.radios[i].name == name) {
|
||||
@@ -67,10 +67,21 @@ var removeRadio = function(country, name, callback) {
|
||||
return callback("Radio not in the list");
|
||||
}
|
||||
|
||||
exports.insertRadio = insertRadio;
|
||||
exports.removeRadio = removeRadio;
|
||||
exports.toggleContent = function(country, name, type, enable, callback) {
|
||||
if (enable != "enable" && enable != "disable") {
|
||||
return callback("keywords allowed are 'enable' and 'disable'");
|
||||
}
|
||||
for (var i=0; i<config.radios.length; i++) {
|
||||
if (config.radios[i].country == country && config.radios[i].name == name) {
|
||||
config.radios[i].content[type] = (enable == "enable") ? true : false;
|
||||
saveRadios();
|
||||
return callback(null);
|
||||
}
|
||||
}
|
||||
return callback("Radio not in the list");
|
||||
}
|
||||
|
||||
var getRadios = function() {
|
||||
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];
|
||||
@@ -87,12 +98,11 @@ var getRadios = function() {
|
||||
return radios;
|
||||
}
|
||||
|
||||
exports.getRadios = getRadios;
|
||||
|
||||
|
||||
var getUserConfig = function() {
|
||||
var result = {};
|
||||
Object.assign(result, {
|
||||
token: config.user.token ? true : false,
|
||||
cacheLen: config.user.cacheLen,
|
||||
streamInitialBuffer: config.user.streamInitialBuffer,
|
||||
streamGranularity: config.user.streamGranularity,
|
||||
@@ -104,7 +114,7 @@ var getUserConfig = function() {
|
||||
exports.getUserConfig = getUserConfig;
|
||||
|
||||
var saveRadios = function() {
|
||||
var exportedRadios = getRadios();
|
||||
var exportedRadios = exports.getRadios();
|
||||
fs.writeFile("config/radios.json", JSON.stringify(exportedRadios), function(err) {
|
||||
if (err) {
|
||||
log.error("saveRadios: could not save radio config. err=" + err);
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
var log = require("loglevel");
|
||||
log.setLevel("debug");
|
||||
var log = require("./log.js")("master");
|
||||
var cp = require("child_process");
|
||||
var findDataFiles = require("./findDataFiles.js");
|
||||
var async = require("async");
|
||||
@@ -17,7 +16,7 @@ const SEG_DURATION = 10; // in seconds
|
||||
const LISTEN_BUFFER = 30; // in seconds
|
||||
var USE_ABRSDK = true;
|
||||
|
||||
var { config, getRadios, getUserConfig, insertRadio, removeRadio, getAvailableInactive } = require("./config.js");
|
||||
var { config, getRadios, getUserConfig, insertRadio, removeRadio, toggleContent, getAvailableInactive } = require("./config.js");
|
||||
|
||||
var dl = [];
|
||||
var updateDlList = function() {
|
||||
@@ -73,7 +72,30 @@ var updateDlList = function() {
|
||||
} 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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -84,28 +106,6 @@ if (USE_ABRSDK) {
|
||||
if (err) {
|
||||
log.error("abrsdk: connection error: " + err + ". switch off sdk");
|
||||
USE_ABRSDK = false;
|
||||
} else {
|
||||
abrsdk.setPredictionCallback(function(predictions) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
updateDlList();
|
||||
});
|
||||
@@ -132,10 +132,10 @@ app.get('/config/radios/insert/:country/:name', function(request, response) {
|
||||
if (err) {
|
||||
log.error("/config/insert/" + country + "/" + name + ": err=" + err);
|
||||
response.writeHead(400);
|
||||
response.end();
|
||||
response.end("err=" + err);
|
||||
} else {
|
||||
response.writeHead(200);
|
||||
response.end();
|
||||
response.end("OK");
|
||||
updateDlList();
|
||||
}
|
||||
});
|
||||
@@ -149,10 +149,10 @@ app.get('/config/radios/remove/:country/:name', function(request, response) {
|
||||
if (err) {
|
||||
log.error("/config/remove/" + country + "/" + name + ": err=" + err);
|
||||
response.writeHead(400);
|
||||
response.end();
|
||||
response.end("err=" + err);
|
||||
} else {
|
||||
response.writeHead(200);
|
||||
response.end();
|
||||
response.end("OK");
|
||||
updateDlList();
|
||||
}
|
||||
});
|
||||
@@ -163,6 +163,24 @@ app.get('/config/radios/available', function(request, response) {
|
||||
response.json(getAvailableInactive());
|
||||
});
|
||||
|
||||
app.get('/config/radios/content/:country/:name/:type/:enable', function(request, response) {
|
||||
response.set({ 'Access-Control-Allow-Origin': '*' });
|
||||
var country = decodeURIComponent(request.params.country);
|
||||
var name = decodeURIComponent(request.params.name);
|
||||
var type = decodeURIComponent(request.params.type);
|
||||
var enable = decodeURIComponent(request.params.enable);
|
||||
toggleContent(country, name, type, enable, function(err) {
|
||||
if (err) {
|
||||
log.error("/config/radios/content/" + country + "/" + name + "/" + type + "/" + enable + ": err=" + err);
|
||||
response.writeHead(400);
|
||||
response.end("err=" + err);
|
||||
} else {
|
||||
response.writeHead(200);
|
||||
response.end("OK");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var getRadio = function(country, name) {
|
||||
if (name) { // both parameters used
|
||||
for (var j=0; j<config.radios.length; j++) {
|
||||
@@ -190,7 +208,7 @@ app.get('/:action/:radio/:delay', function(request, response) {
|
||||
|
||||
if (!getRadio(radio) || !getRadio(radio).enable) {
|
||||
response.writeHead(400);
|
||||
return response.end();
|
||||
return response.end("radio not found");
|
||||
}
|
||||
|
||||
switch(action) {
|
||||
@@ -209,7 +227,7 @@ app.get('/:action/:radio/:delay', function(request, response) {
|
||||
if (!initialBuffer) {
|
||||
log.error("/listen/" + radio + "/" + delay + ": initialBuffer not available");
|
||||
response.writeHead(400);
|
||||
return response.end();
|
||||
return response.end("buffer not available");
|
||||
}
|
||||
|
||||
log.info("listen: send initial buffer of " + initialBuffer.length + " bytes");
|
||||
@@ -274,21 +292,21 @@ app.get('/:action/:radio/:delay', function(request, response) {
|
||||
if (!radio) {
|
||||
log.error("/metadata/" + radio + "/" + delay + ": radio not available");
|
||||
response.writeHead(400);
|
||||
return response.end();
|
||||
return response.end("radio not found");
|
||||
} else if (!radio.liveStatus) {
|
||||
log.error("/metadata/" + radio + "/" + delay + ": radio.liveStatus not available");
|
||||
response.writeHead(400);
|
||||
return response.end();
|
||||
return response.end("radio not ready");
|
||||
} else if (!radio.liveStatus.metaCache) {
|
||||
log.error("/metadata/" + radio + "/" + delay + ": metadata not available");
|
||||
response.writeHead(400);
|
||||
return response.end();
|
||||
return response.end("metadata not available");
|
||||
}
|
||||
response.json(radio.liveStatus.metaCache.read());
|
||||
break;
|
||||
|
||||
default:
|
||||
response.writeHead(400);
|
||||
response.end();
|
||||
response.end("unknown route");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
var log = require("loglevel");
|
||||
log.setLevel("debug");
|
||||
var prefix = require('loglevel-plugin-prefix');
|
||||
var chalk = require('chalk');
|
||||
|
||||
// #### logging decoration ####
|
||||
// see https://github.com/kutuluk/loglevel-plugin-prefix
|
||||
const colors = {
|
||||
TRACE: chalk.magenta,
|
||||
DEBUG: chalk.cyan,
|
||||
INFO: chalk.blue,
|
||||
WARN: chalk.yellow,
|
||||
ERROR: chalk.red,
|
||||
};
|
||||
|
||||
prefix.reg(log);
|
||||
log.enableAll();
|
||||
|
||||
prefix.apply(log, {
|
||||
format(level, name, timestamp) {
|
||||
return `${chalk.gray(`[${timestamp}]`)} ${colors[level.toUpperCase()](level)} ${chalk.green(`${name}:`)}`;
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = function(moduleName) {
|
||||
return log.getLogger(moduleName || "root");
|
||||
}
|
||||
Generated
+41
-45
@@ -37,8 +37,11 @@
|
||||
}
|
||||
},
|
||||
"adblockradio-sdk": {
|
||||
"version": "git://github.com/dest4/adblockradio-sdk.git#149fde37e278f45c77c5c8c09e17321a9cf2152b",
|
||||
"version": "git://github.com/dest4/adblockradio-sdk.git#52e27c7a8f38690fe16a5915c74bc23be29bebee",
|
||||
"requires": {
|
||||
"chalk": "2.3.0",
|
||||
"loglevel": "1.6.1",
|
||||
"loglevel-plugin-prefix": "0.8.3",
|
||||
"socket.io-client": "2.0.4"
|
||||
}
|
||||
},
|
||||
@@ -261,7 +264,6 @@
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz",
|
||||
"integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "3.2.0",
|
||||
"escape-string-regexp": "1.0.5",
|
||||
@@ -272,7 +274,6 @@
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz",
|
||||
"integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "1.9.1"
|
||||
}
|
||||
@@ -281,7 +282,6 @@
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz",
|
||||
"integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "2.0.0"
|
||||
}
|
||||
@@ -325,7 +325,6 @@
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz",
|
||||
"integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "1.1.3"
|
||||
}
|
||||
@@ -333,8 +332,7 @@
|
||||
"color-name": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
|
||||
"dev": true
|
||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
|
||||
},
|
||||
"component-bind": {
|
||||
"version": "1.0.0",
|
||||
@@ -374,12 +372,9 @@
|
||||
"integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
|
||||
},
|
||||
"content-security-policy-builder": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/content-security-policy-builder/-/content-security-policy-builder-1.1.0.tgz",
|
||||
"integrity": "sha1-2R8bB2I2wRmFDH3umSS/VeBXcrM=",
|
||||
"requires": {
|
||||
"dashify": "0.2.2"
|
||||
}
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/content-security-policy-builder/-/content-security-policy-builder-2.0.0.tgz",
|
||||
"integrity": "sha512-j+Nhmj1yfZAikJLImCvPJFE29x/UuBi+/MWqggGGc515JKaZrjuei2RhULJmy0MsstW3E3htl002bwmBNMKr7w=="
|
||||
},
|
||||
"content-type": {
|
||||
"version": "1.0.4",
|
||||
@@ -418,11 +413,6 @@
|
||||
"resolved": "https://registry.npmjs.org/dasherize/-/dasherize-2.0.0.tgz",
|
||||
"integrity": "sha1-bYCcnNDPe7iVLYD8hPoT1H3bEwg="
|
||||
},
|
||||
"dashify": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/dashify/-/dashify-0.2.2.tgz",
|
||||
"integrity": "sha1-agdBWgHJH69KMuONnfunH2HLIP4="
|
||||
},
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
@@ -487,9 +477,9 @@
|
||||
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
|
||||
},
|
||||
"encodeurl": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz",
|
||||
"integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA="
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
|
||||
},
|
||||
"engine.io-client": {
|
||||
"version": "3.1.4",
|
||||
@@ -529,8 +519,7 @@
|
||||
"escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
|
||||
"dev": true
|
||||
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
|
||||
},
|
||||
"eslint": {
|
||||
"version": "4.15.0",
|
||||
@@ -675,7 +664,7 @@
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "1.1.2",
|
||||
"encodeurl": "1.0.1",
|
||||
"encodeurl": "1.0.2",
|
||||
"escape-html": "1.0.3",
|
||||
"etag": "1.8.1",
|
||||
"finalhandler": "1.1.0",
|
||||
@@ -752,7 +741,7 @@
|
||||
"integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=",
|
||||
"requires": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "1.0.1",
|
||||
"encodeurl": "1.0.2",
|
||||
"escape-html": "1.0.3",
|
||||
"on-finished": "2.3.0",
|
||||
"parseurl": "1.3.2",
|
||||
@@ -864,19 +853,18 @@
|
||||
"has-flag": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz",
|
||||
"integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=",
|
||||
"dev": true
|
||||
"integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE="
|
||||
},
|
||||
"helmet": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/helmet/-/helmet-3.9.0.tgz",
|
||||
"integrity": "sha512-czCyS77TyanWlfVSoGlb9GBJV2Q2zJayKxU5uBw0N1TzDTs/qVNh1SL8Q688KU0i0Sb7lQ/oLtnaEqXzl2yWvA==",
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/helmet/-/helmet-3.10.0.tgz",
|
||||
"integrity": "sha512-wVu5jSeImztLqNQPc4hqGr1DG0Ki2UJVmQ1KTugIrtl1f4Zw5SqVqh6QPyw5b6/Jo/iAnyTt+pcehB0RdEJsbw==",
|
||||
"requires": {
|
||||
"dns-prefetch-control": "0.1.0",
|
||||
"dont-sniff-mimetype": "1.0.0",
|
||||
"expect-ct": "0.1.0",
|
||||
"frameguard": "3.0.0",
|
||||
"helmet-csp": "2.6.0",
|
||||
"helmet-csp": "2.7.0",
|
||||
"hide-powered-by": "1.0.0",
|
||||
"hpkp": "2.0.0",
|
||||
"hsts": "2.1.0",
|
||||
@@ -887,15 +875,15 @@
|
||||
}
|
||||
},
|
||||
"helmet-csp": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/helmet-csp/-/helmet-csp-2.6.0.tgz",
|
||||
"integrity": "sha512-n/oW9l6RtO4f9YvphsNzdvk1zITrSN7iRT8ojgrJu/N3mVdHl9zE4OjbiHWcR64JK32kbqx90/yshWGXcjUEhw==",
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/helmet-csp/-/helmet-csp-2.7.0.tgz",
|
||||
"integrity": "sha512-IGIAkWnxjRbgMXFA2/kmDqSIrIaSfZ6vhMHlSHw7jm7Gm9nVVXqwJ2B1YEpYrJsLrqY+w2Bbimk7snux9+sZAw==",
|
||||
"requires": {
|
||||
"camelize": "1.0.0",
|
||||
"content-security-policy-builder": "1.1.0",
|
||||
"content-security-policy-builder": "2.0.0",
|
||||
"dasherize": "2.0.0",
|
||||
"lodash.reduce": "4.6.0",
|
||||
"platform": "1.3.4"
|
||||
"platform": "1.3.5"
|
||||
}
|
||||
},
|
||||
"hide-powered-by": {
|
||||
@@ -1111,6 +1099,11 @@
|
||||
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.1.tgz",
|
||||
"integrity": "sha1-4PyVEztu8nbNyIh82vJKpvFW+Po="
|
||||
},
|
||||
"loglevel-plugin-prefix": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.3.tgz",
|
||||
"integrity": "sha512-oFDEE3krjFTlyXfdT6shN4HsIlDrbPyleI2WIgRfn//oUEVfe8dP7goO9ktTSdOQC08CTKshKHHdZXe4Dy9TxQ=="
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz",
|
||||
@@ -1328,9 +1321,9 @@
|
||||
}
|
||||
},
|
||||
"platform": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.4.tgz",
|
||||
"integrity": "sha1-bw+xftqqSPIUQrOpdcBjEw8cPr0="
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.5.tgz",
|
||||
"integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q=="
|
||||
},
|
||||
"pluralize": {
|
||||
"version": "7.0.0",
|
||||
@@ -1503,7 +1496,7 @@
|
||||
"debug": "2.6.9",
|
||||
"depd": "1.1.2",
|
||||
"destroy": "1.0.4",
|
||||
"encodeurl": "1.0.1",
|
||||
"encodeurl": "1.0.2",
|
||||
"escape-html": "1.0.3",
|
||||
"etag": "1.8.1",
|
||||
"fresh": "0.5.2",
|
||||
@@ -1520,7 +1513,7 @@
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz",
|
||||
"integrity": "sha512-hSMUZrsPa/I09VYFJwa627JJkNs0NrfL1Uzuup+GqHfToR2KcsXFymXSV90hoyw3M+msjFuQly+YzIH/q0MGlQ==",
|
||||
"requires": {
|
||||
"encodeurl": "1.0.1",
|
||||
"encodeurl": "1.0.2",
|
||||
"escape-html": "1.0.3",
|
||||
"parseurl": "1.3.2",
|
||||
"send": "0.16.1"
|
||||
@@ -1742,12 +1735,15 @@
|
||||
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
|
||||
},
|
||||
"webradio-metadata": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/webradio-metadata/-/webradio-metadata-0.1.6.tgz",
|
||||
"integrity": "sha512-345c6QzlnstTB9CZ4s535P2DwqSD0R1Uw/E9yHd4hwCpw3jNITelnxQ7biK78zXIKTDy4Po1dkKUwqUVHyiNOA==",
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/webradio-metadata/-/webradio-metadata-0.1.7.tgz",
|
||||
"integrity": "sha512-0aS0WAxt+ptSxYtIzOmXDlRQODw4pG3Lm+8d06AKF4WQF1YzjsCCAuoRY5wK0Sj7wPdGI98pPevpPCpsjbrA3w==",
|
||||
"requires": {
|
||||
"chalk": "2.3.0",
|
||||
"express": "4.16.2",
|
||||
"helmet": "3.9.0",
|
||||
"helmet": "3.10.0",
|
||||
"loglevel": "1.6.1",
|
||||
"loglevel-plugin-prefix": "0.8.3",
|
||||
"xml2js": "0.4.19"
|
||||
}
|
||||
},
|
||||
|
||||
+3
-1
@@ -11,8 +11,10 @@
|
||||
"dependencies": {
|
||||
"adblockradio-sdk": "git://github.com/dest4/adblockradio-sdk.git#master",
|
||||
"async": "^2.6.0",
|
||||
"chalk": "^2.3.0",
|
||||
"loglevel": "^1.6.1",
|
||||
"webradio-metadata": "^0.1.6"
|
||||
"loglevel-plugin-prefix": "^0.8.3",
|
||||
"webradio-metadata": "^0.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^4.15.0"
|
||||
|
||||
Reference in New Issue
Block a user