Compare commits
49 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53f718d7cb | ||
|
|
8e8b712a2c | ||
|
|
5f6758a0c4 | ||
|
|
779a33c275 | ||
|
|
7ab918401b | ||
|
|
f9c8cbc8b2 | ||
|
|
504ed539a0 | ||
|
|
006253dcbb | ||
|
|
732f77a0b7 | ||
|
|
1a2de1b9b8 | ||
|
|
15347a0fac | ||
|
|
a28bf3233c | ||
|
|
c9ec1e1c6a | ||
|
|
e578c9d7bc | ||
|
|
a677c110dc | ||
|
|
90f5e4a87a | ||
|
|
ac2ba5bce5 | ||
|
|
981f5d136c | ||
|
|
ecf942a3b9 | ||
|
|
f485556bc8 | ||
|
|
1dcfbe36c1 | ||
|
|
03ae7ab0dd | ||
|
|
d5a6d0c9d3 | ||
|
|
0116703708 | ||
|
|
0963d73936 | ||
|
|
78b223430b | ||
|
|
fa42d5acb5 | ||
|
|
59ca024174 | ||
|
|
069307c18c | ||
|
|
8f2878aade | ||
|
|
1323f7ea75 | ||
|
|
761d22482d | ||
|
|
96d1e7fbf9 | ||
|
|
26d512d56d | ||
|
|
d18f56c9fc | ||
|
|
1d2f816d98 | ||
|
|
674ad2a4fc | ||
|
|
14444c1709 | ||
|
|
b57a15918f | ||
|
|
15a7b1ba68 | ||
|
|
252a9ab148 | ||
|
|
bc177d90a2 | ||
|
|
5896c49d43 | ||
|
|
08e6afa682 | ||
|
|
5a44ba00ca | ||
|
|
6ffd098a74 | ||
|
|
dc891451d9 | ||
|
|
900ef29ee0 | ||
|
|
a8a53b8900 |
12 changed files with 703 additions and 130 deletions
|
|
@ -1,9 +0,0 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- '0.12'
|
||||
before_script:
|
||||
- npm install -g gulp
|
||||
notifications:
|
||||
email:
|
||||
on_success: change
|
||||
on_failure: change
|
||||
22
LICENSE
Normal file
22
LICENSE
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Adafruit Industries
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
74
README.md
74
README.md
|
|
@ -1,9 +1,77 @@
|
|||
# NPR One Raspberry Pi Radio [](https://travis-ci.org/adafruit/nprone_raspi)
|
||||
# NPR One CLI
|
||||
|
||||
This project uses the NPR One API to create a standalone NPR One streaming radio using a Raspberry Pi.
|
||||
This is a simple command line based NPR One client for OS X and Linux. A full tutorial with setup instructions can be found [in the Adafruit Learning System](https://learn.adafruit.com/raspberry-pi-zero-npr-one-radio).
|
||||
|
||||
## Installation
|
||||
|
||||
This package requires the latest stable version of [Node.js](https://nodejs.org) (v6.0 or higher due to the use of es6).
|
||||
|
||||
```sh
|
||||
$ node -v
|
||||
v6.2.0
|
||||
```
|
||||
npm install ¯\_(ツ)_/¯
|
||||
|
||||
Install `mplayer` on OS X using [homebrew](http://brew.sh/):
|
||||
|
||||
```
|
||||
$ brew install mplayer
|
||||
```
|
||||
|
||||
Install `mplayer` on Linux:
|
||||
|
||||
```
|
||||
$ sudo apt-get install -y mplayer
|
||||
```
|
||||
|
||||
Make sure you have the latest stable [node.js](https://nodejs.org/en/) installed (6.0 or higher), and then run:
|
||||
|
||||
```
|
||||
npm install -g npr-one
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Sign into the [NPR Dev Console](http://dev.npr.org/), create a new app, and use your App ID & Secret to authorize the CLI. The audio player will save your authorization and begin playing.
|
||||
|
||||
```
|
||||
$ npr-one
|
||||
|
||||
\\\\\\\\\\\\\\\\\\\\\\\\\\\>>>>>>>>>>>>>>>>>>>>>>>>>> \\\\\\\\\\\\\\\\\\\\\\\\\\
|
||||
\\\\\\\\\\\\\\\\\\\\\\\\\\\>>>>>>>>>>>>>>>>>>>>>>>>>> \\\\\\\\\\\\\\\\\\\\\\\\\\
|
||||
\\\\\\\\\\\\\>>\\\\\\\\\\\\>>>>>>>>>>>>>>>>>>>>>>>>>> \\\\\\\\\\\\\\>>>\\\\\\\\\
|
||||
\\\\\\\ >\\\\\\\\>>>>>>> \>>>>>>>> \\\\\\\\ (\\\\\\\\
|
||||
\\\\\\\ .>>>>= \\\\\\\\>>>>>>> =>>>> >>>>>>> \\\\\\\\ (>>>>\\\\\\\\\
|
||||
\\\\\\\ )\\\\\ (\\\\\\\>>>>>>> >>>>>> )>>>>>> \\\\\\\\ .\\\\\\\\\\\\\\
|
||||
\\\\\\\ )\\\\\ (\\\\\\\>>>>>>> >>>>>> )>>>>>> \\\\\\\\ )\\\\\\\\\\\\\\
|
||||
\\\\\\\ )\\\\\ (\\\\\\\>>>>>>> >>>>>\ >>>>>>> \\\\\\\\ )\\\\\\\\\\\\\\
|
||||
\\\\\\\ )\\\\\ (\\\\\\\>>>>>>> ->>>>>>>> \\\\\\\\ )\\\\\\\\\\\\\\
|
||||
\\\\\\\>>>>\\\\\>>>>\\\\\\\>>>>>>> >>>>(>>>>>>>>>>> \\\\\\\\>>>>\\\\\\\\\\\\\\
|
||||
\\\\\\\\\\\\\\\\\\\\\\\\\\\>>>>>>> >>>>>>>>>>>>>>>> \\\\\\\\\\\\\\\\\\\\\\\\\\
|
||||
\\\\\\\\\\\\\\\\\\\\\\\\\\\>>>>>>>===>>>>>>>>>>>>>>>> \\\\\\\\\\\\\\\\\\\\\\\\\\
|
||||
|
||||
[downloaded] WYPR FM
|
||||
[downloaded] NPR thanks our sponsors
|
||||
[playing] WYPR FM
|
||||
[downloaded] Welcome To Czechia: Czech Republic Looks To Adopt Shorter Name
|
||||
[downloaded] Belgian Transport Minister Resigns Over Airport Security Debate
|
||||
[downloaded] Tax Season
|
||||
[downloaded] Adapting To A More Extreme Climate, Coastal Cities Get Creative
|
||||
[downloaded] NPR thanks our sponsors
|
||||
```
|
||||
|
||||
### Keyboard Controls
|
||||
|
||||
```
|
||||
space play/pause
|
||||
↑ volume up
|
||||
↓ volume down
|
||||
← rewind 15 seconds
|
||||
→ skip to the next story
|
||||
i mark as interesting
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2016 Adafruit Industries. Licensed under the MIT license.
|
||||
|
||||
The NPR logo is a registered trademark of NPR used with permission from NPR. All rights reserved.
|
||||
|
|
|
|||
34
cli
Executable file
34
cli
Executable file
|
|
@ -0,0 +1,34 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
SOURCE="${BASH_SOURCE[0]}"
|
||||
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
|
||||
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
||||
SOURCE="$(readlink "$SOURCE")"
|
||||
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
|
||||
done
|
||||
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
||||
|
||||
cd $DIR
|
||||
|
||||
cat logo.txt
|
||||
|
||||
_LATEST=$(npm view npr-one version)
|
||||
_CURRENT=$(node version.js)
|
||||
_RELEASE='/etc/os-release'
|
||||
|
||||
if [ -f $_RELEASE ]; then
|
||||
|
||||
source $_RELEASE
|
||||
|
||||
if [ $ID == 'raspbian' ]; then
|
||||
sudo chown -R $(whoami) $(npm config get prefix)/{lib/node_modules,bin,share}
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
if [ $_CURRENT != $_LATEST ]; then
|
||||
echo "updating..."
|
||||
npm install -g npr-one
|
||||
fi
|
||||
|
||||
node index.js
|
||||
48
gulpfile.js
48
gulpfile.js
|
|
@ -1,48 +0,0 @@
|
|||
require('dotenv').load();
|
||||
|
||||
var gulp = require('gulp'),
|
||||
jshint = require('gulp-jshint'),
|
||||
mocha = require('gulp-mocha');
|
||||
|
||||
gulp.task('lint', function() {
|
||||
|
||||
var lint = jshint({
|
||||
"curly": false,
|
||||
"eqeqeq": true,
|
||||
"immed": true,
|
||||
"latedef": "nofunc",
|
||||
"newcap": false,
|
||||
"noarg": true,
|
||||
"sub": true,
|
||||
"undef": false,
|
||||
"unused": "var",
|
||||
"boss": true,
|
||||
"eqnull": true,
|
||||
"node": true,
|
||||
"-W086": true
|
||||
});
|
||||
|
||||
return gulp.src([
|
||||
'index.js',
|
||||
'lib/*.js',
|
||||
'test/*.js'
|
||||
]).pipe(lint)
|
||||
.pipe(jshint.reporter('jshint-stylish'));
|
||||
|
||||
});
|
||||
|
||||
gulp.task('test', function() {
|
||||
|
||||
return gulp.src('test/*.js', {read: false})
|
||||
.pipe(mocha())
|
||||
.once('error', function(err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
})
|
||||
.once('end', function() {
|
||||
process.exit();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
gulp.task('default', ['lint']);
|
||||
68
index.js
68
index.js
|
|
@ -1,13 +1,30 @@
|
|||
var npr = require('npr-api')(),
|
||||
fs = require('fs'),
|
||||
chalk = require('chalk'),
|
||||
auth = require('./lib/auth'),
|
||||
omx = require('node-omx')();
|
||||
wget = require('wget-improved');
|
||||
'use strict';
|
||||
|
||||
var logo = fs.readFileSync('./logo.txt', 'utf8');
|
||||
process.title = 'npr-one';
|
||||
|
||||
console.log(logo);
|
||||
if(process.platform != 'linux' && process.platform != 'darwin') {
|
||||
console.error('Your platform is not currently supported');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const NPR = require('npr-api'),
|
||||
chalk = require('chalk'),
|
||||
auth = require('./lib/auth'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
config = path.join(process.env['HOME'], '.npr-one'),
|
||||
dotenv = require('dotenv').load({silent: true, path: config}),
|
||||
Player = require('./lib/player'),
|
||||
Story = require('./lib/story'),
|
||||
UI = require('./lib/ui');
|
||||
|
||||
const logo = fs.readFileSync(path.join(__dirname,'logo.txt'), 'utf8');
|
||||
|
||||
const npr = new NPR(),
|
||||
story = new Story(npr),
|
||||
player = new Player();
|
||||
|
||||
console.log('connecting to npr one...');
|
||||
|
||||
// silence swagger log output
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
|
@ -15,22 +32,29 @@ process.env.NODE_ENV = 'test';
|
|||
npr.one
|
||||
.init()
|
||||
.then(auth.getToken.bind(auth, npr))
|
||||
.then(function(token) {
|
||||
.then((token) => {
|
||||
process.stdout.write('\x1B[2J');
|
||||
process.stdout.write('\x1B[0f');
|
||||
console.log(logo);
|
||||
return npr.one.setAccessToken(token);
|
||||
})
|
||||
.then(function() {
|
||||
return npr.one.listening.getRecommendations({ channel: 'npr' });
|
||||
})
|
||||
.then(function(recommendations) {
|
||||
// print out the first two recommendations to the console
|
||||
var tmp = '/tmp/test.mp4 ';
|
||||
wget.download(recommendations.items[0].links.audio[0].href, tmp)
|
||||
.on('end', function() {
|
||||
omx.play(tmp);
|
||||
omx.on('play', function() {console.log('play');});
|
||||
omx.on('stop', function() {console.log('stop');});
|
||||
});
|
||||
.then(story.getRecommendations.bind(story))
|
||||
.then(player.load.bind(player))
|
||||
.then(() => {
|
||||
|
||||
const ui = new UI({
|
||||
touchThreshold: process.env.MPR121_TOUCH,
|
||||
releaseThreshold: process.env.MPR121_RELEASE
|
||||
});
|
||||
|
||||
ui.on('skip', player.skip.bind(player));
|
||||
ui.on('pause', player.pause.bind(player));
|
||||
ui.on('rewind', player.rewind.bind(player));
|
||||
ui.on('interesting', player.interesting.bind(player));
|
||||
ui.on('volumeup', player.increaseVolume.bind(player));
|
||||
ui.on('volumedown', player.decreaseVolume.bind(player));
|
||||
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error(err,err.stack);
|
||||
console.error(err, err.stack);
|
||||
});
|
||||
|
|
|
|||
55
lib/auth.js
55
lib/auth.js
|
|
@ -1,46 +1,49 @@
|
|||
var inquirer = require('inquirer'),
|
||||
dotenv = require('dotenv').load(),
|
||||
fs = require('fs'),
|
||||
npr;
|
||||
'use strict';
|
||||
|
||||
var client_creds = [
|
||||
const inquirer = require('inquirer'),
|
||||
path = require('path'),
|
||||
config = path.join(process.env['HOME'], '.npr-one'),
|
||||
fs = require('fs');
|
||||
|
||||
let npr;
|
||||
|
||||
const client_creds = [
|
||||
{
|
||||
type: 'input',
|
||||
name: 'CLIENT_ID',
|
||||
message: 'NPR OAuth Client ID',
|
||||
message: 'NPR Application ID',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'CLIENT_SECRET',
|
||||
message: 'NPR OAuth Client Secret',
|
||||
message: 'NPR Application Secret',
|
||||
}
|
||||
];
|
||||
|
||||
var device_code = [
|
||||
const device_code = [
|
||||
{
|
||||
type: 'list',
|
||||
name: 'device',
|
||||
message: 'Authorize this Pi @ ',
|
||||
message: 'Authorize the NPR One CLI @ ',
|
||||
choices: ['Complete', 'Exit']
|
||||
}
|
||||
];
|
||||
|
||||
var requestDeviceCode = function() {
|
||||
const requestDeviceCode = () => {
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
npr.one.authorization
|
||||
.generateDeviceCode({
|
||||
client_id: process.env.CLIENT_ID,
|
||||
client_secret: process.env.CLIENT_SECRET,
|
||||
scope: 'listening.write identity.readonly'
|
||||
scope: 'listening.readonly listening.write identity.readonly'
|
||||
})
|
||||
.then(function(res) {
|
||||
.then((res) => {
|
||||
|
||||
device_code[0].message += res.verification_uri;
|
||||
device_code[0].message += ' using code: ' + res.user_code;
|
||||
device_code[0].message += `${res.verification_uri} using code: ${res.user_code}`;
|
||||
|
||||
inquirer.prompt(device_code, function(answers) {
|
||||
inquirer.prompt(device_code, (answers) => {
|
||||
|
||||
if(answers.device === 'Exit')
|
||||
process.exit();
|
||||
|
|
@ -56,9 +59,9 @@ var requestDeviceCode = function() {
|
|||
|
||||
};
|
||||
|
||||
var requestToken = function(code) {
|
||||
const requestToken = (code) => {
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
npr.one.authorization
|
||||
.createToken({
|
||||
|
|
@ -67,7 +70,7 @@ var requestToken = function(code) {
|
|||
client_secret: process.env.CLIENT_SECRET,
|
||||
code: code
|
||||
})
|
||||
.then(function(res) {
|
||||
.then((res) => {
|
||||
process.env.NPR_ACCESS_TOKEN = res.access_token;
|
||||
resolve(res.access_token);
|
||||
})
|
||||
|
|
@ -77,14 +80,14 @@ var requestToken = function(code) {
|
|||
|
||||
};
|
||||
|
||||
var getClientCreds = function() {
|
||||
const getClientCreds = () => {
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
if(process.env.CLIENT_ID && process.env.CLIENT_SECRET)
|
||||
return resolve();
|
||||
|
||||
inquirer.prompt(client_creds, function(answers) {
|
||||
inquirer.prompt(client_creds, (answers) => {
|
||||
|
||||
process.env.CLIENT_ID = answers.CLIENT_ID;
|
||||
process.env.CLIENT_SECRET = answers.CLIENT_SECRET;
|
||||
|
|
@ -97,11 +100,11 @@ var getClientCreds = function() {
|
|||
|
||||
};
|
||||
|
||||
exports.getToken = function(api) {
|
||||
exports.getToken = (api) => {
|
||||
|
||||
npr = api;
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
if(process.env.NPR_ACCESS_TOKEN)
|
||||
return resolve(process.env.NPR_ACCESS_TOKEN);
|
||||
|
|
@ -109,8 +112,8 @@ exports.getToken = function(api) {
|
|||
getClientCreds()
|
||||
.then(requestDeviceCode.bind(this))
|
||||
.then(requestToken.bind(this))
|
||||
.then(function(token) {
|
||||
fs.writeFileSync('.env', 'NPR_ACCESS_TOKEN=' + process.env.NPR_ACCESS_TOKEN + '\n');
|
||||
.then((token) => {
|
||||
fs.writeFileSync(config, `NPR_ACCESS_TOKEN=${process.env.NPR_ACCESS_TOKEN}\n`);
|
||||
resolve(token);
|
||||
})
|
||||
.catch(reject);
|
||||
|
|
|
|||
161
lib/player.js
Normal file
161
lib/player.js
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
'use strict';
|
||||
|
||||
const Mplayer = require('mplayer'),
|
||||
chalk = require('chalk'),
|
||||
log = require('npmlog'),
|
||||
EventEmitter = require('events');
|
||||
|
||||
const resetLine = function() {
|
||||
process.stdout.clearLine();
|
||||
process.stdout.cursorTo(0)
|
||||
};
|
||||
|
||||
class Player extends EventEmitter {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
log.addLevel('playing', 2001, {bg: 'black', fg: 'green'}, '[playing]');
|
||||
log.addLevel('skipped', 2002, {bg: 'black', fg: 'red'}, '[skipped]');
|
||||
log.addLevel('finished', 2003, {bg: 'black', fg: 'red'}, '[finished]');
|
||||
log.addLevel('volume', 2004, {bg: 'black', fg: 'red'}, '[volume]');
|
||||
log.addLevel('interesting', 2005, {bg: 'black', fg: 'red'}, '[interesting]');
|
||||
|
||||
this.story = null;
|
||||
this.volume = 100;
|
||||
this.time = 0;
|
||||
this.playing = false;
|
||||
|
||||
this.player = new Mplayer();
|
||||
this.player.on('stop', this.done.bind(this));
|
||||
this.player.on('time', (time) => this.time = time);
|
||||
this.player.on('error', console.error);
|
||||
|
||||
}
|
||||
|
||||
load(story) {
|
||||
this.story = story;
|
||||
return this.play();
|
||||
}
|
||||
|
||||
play() {
|
||||
|
||||
if(! this.story) return;
|
||||
|
||||
this.story.start().then((file) => {
|
||||
|
||||
this.player.openFile(file);
|
||||
|
||||
resetLine();
|
||||
log.playing(this.story.title);
|
||||
|
||||
this.player.play();
|
||||
this.time = 0;
|
||||
this.playing = true;
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
increaseVolume() {
|
||||
|
||||
if(! this.player) return;
|
||||
|
||||
this.volume += 10;
|
||||
|
||||
if(this.volume > 100)
|
||||
this.volume = 100;
|
||||
|
||||
this.player.volume(this.volume);
|
||||
|
||||
resetLine();
|
||||
log.volume(this.volume);
|
||||
|
||||
}
|
||||
|
||||
decreaseVolume() {
|
||||
|
||||
if(! this.player)
|
||||
return;
|
||||
|
||||
this.volume -= 10;
|
||||
|
||||
if(this.volume < 10)
|
||||
this.volume = 10;
|
||||
|
||||
this.player.volume(this.volume);
|
||||
|
||||
resetLine();
|
||||
log.volume(this.volume);
|
||||
|
||||
}
|
||||
|
||||
pause() {
|
||||
|
||||
if(! this.player) return;
|
||||
|
||||
if(this.playing) {
|
||||
this.player.pause();
|
||||
this.playing = false;
|
||||
} else {
|
||||
this.player.play();
|
||||
this.playing = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
rewind() {
|
||||
|
||||
if(! this.player) return;
|
||||
|
||||
if(this.time < 15)
|
||||
this.time = 15;
|
||||
|
||||
this.player.seek(this.time-15);
|
||||
|
||||
}
|
||||
|
||||
skip() {
|
||||
|
||||
if(! this.player) return;
|
||||
if(! this.story) return;
|
||||
if(! this.story.canSkip) return;
|
||||
|
||||
resetLine();
|
||||
log.skipped(this.story.title);
|
||||
|
||||
this.story.next(this.time);
|
||||
this.player.stop();
|
||||
|
||||
}
|
||||
|
||||
interesting() {
|
||||
|
||||
if(! this.player) return;
|
||||
if(! this.story) return;
|
||||
if(this.story.interesting) return;
|
||||
|
||||
resetLine();
|
||||
log.interesting(this.story.title);
|
||||
|
||||
this.story.markInteresting(this.time);
|
||||
|
||||
}
|
||||
|
||||
done() {
|
||||
|
||||
if(! this.story.skipped) {
|
||||
resetLine();
|
||||
log.finished(this.story.title);
|
||||
}
|
||||
|
||||
this.story.finished();
|
||||
this.time = 0;
|
||||
this.playing = false;
|
||||
this.play();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
exports = module.exports = Player;
|
||||
235
lib/story.js
Normal file
235
lib/story.js
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
'use strict';
|
||||
|
||||
const rimraf = require('rimraf'),
|
||||
touch = require('touch'),
|
||||
fs = require('fs'),
|
||||
url = require('url'),
|
||||
S = require('string'),
|
||||
Gauge = require('gauge'),
|
||||
chalk = require('chalk'),
|
||||
wget = require('wget-improved');
|
||||
|
||||
class Story {
|
||||
|
||||
constructor(npr) {
|
||||
|
||||
this.npr = npr;
|
||||
this.recommendations = [];
|
||||
this.completed = [];
|
||||
this.ratings = [];
|
||||
this.current = null;
|
||||
|
||||
}
|
||||
|
||||
download(rec) {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
const filename = `/tmp/npr-${rec.attributes.uid}`;
|
||||
|
||||
try {
|
||||
fs.accessSync(filename);
|
||||
rec.file = filename;
|
||||
rec.downloaded = true;
|
||||
rec.downloading = false;
|
||||
rec.download = Promise.resolve(rec);
|
||||
console.log(`${chalk.red.bgBlack('[downloaded]')} ${rec.attributes.title}`);
|
||||
return resolve(rec);
|
||||
} catch(e) {}
|
||||
|
||||
touch.sync(`/tmp/npr-${rec.attributes.uid}`);
|
||||
rec.file = filename;
|
||||
rec.downloading = true;
|
||||
|
||||
const bar = new Gauge(process.stderr, {
|
||||
cleanupOnExit: false,
|
||||
template: [
|
||||
{value: chalk.green.bgBlack('[download]'), kerning: 1},
|
||||
{type: 'section', kerning: 1, length: 20},
|
||||
{type: 'progressbar' }
|
||||
]
|
||||
});
|
||||
|
||||
wget.download(rec.links.audio[0].href, rec.file)
|
||||
.on('error', reject)
|
||||
.on('progress', (progress) => {
|
||||
bar.show(rec.attributes.title, progress);
|
||||
})
|
||||
.on('end', () => {
|
||||
bar.disable();
|
||||
console.log(`${chalk.red.bgBlack('[downloaded]')} ${rec.attributes.title}`);
|
||||
rec.downloaded = true;
|
||||
rec.downloading = false;
|
||||
resolve(rec);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
fetchNew() {
|
||||
|
||||
var next = this.recommendations.find((rec) => {
|
||||
return !rec.file && !rec.downloaded && !rec.downloading;
|
||||
});
|
||||
|
||||
if(! next) return;
|
||||
|
||||
next.download = this.download(next);
|
||||
next.download.then(() => this.fetchNew());
|
||||
|
||||
return next.download;
|
||||
|
||||
}
|
||||
|
||||
getRecommendations() {
|
||||
|
||||
return this.npr.one.listening.getRecommendations({ channel: 'npr' })
|
||||
.then((rec) => {
|
||||
this.recommendations = rec.items;
|
||||
return this.fetchNew();
|
||||
})
|
||||
.then(() => this.recommendations[1].download)
|
||||
.then(() => {
|
||||
this.current = this.recommendations.shift();
|
||||
return this;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
sendRatings() {
|
||||
|
||||
let args = url.parse(this.current.links.recommendations[0].href, true).query;
|
||||
args.body = this.ratings;
|
||||
|
||||
return this.npr.one.listening.postRating(args)
|
||||
.then((res) => {
|
||||
res.items.forEach((rec) => {
|
||||
if(this.checkExisting(rec)) return;
|
||||
this.recommendations.push(rec);
|
||||
});
|
||||
|
||||
this.ratings = [];
|
||||
|
||||
return this.fetchNew();
|
||||
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
}
|
||||
|
||||
checkExisting(rec) {
|
||||
|
||||
if(rec.attributes.uid == this.id) return true;
|
||||
if(rec.attributes.type == 'stationId') return true;
|
||||
|
||||
const exists = this.recommendations.find((existing) => {
|
||||
return rec.attributes.uid == existing.attributes.uid;
|
||||
});
|
||||
|
||||
const completed = this.completed.find((existing) => {
|
||||
return rec.attributes.uid == existing.attributes.uid;
|
||||
});
|
||||
|
||||
if(exists || completed)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this.current.attributes.uid;
|
||||
}
|
||||
|
||||
get file() {
|
||||
return this.current.file;
|
||||
}
|
||||
|
||||
get skipped() {
|
||||
return this.current.skipped;
|
||||
}
|
||||
|
||||
get canSkip() {
|
||||
return this.current.attributes.skippable;
|
||||
}
|
||||
|
||||
get interesting() {
|
||||
return this.current.interesting;
|
||||
}
|
||||
|
||||
get title() {
|
||||
return this.current.attributes.title;
|
||||
}
|
||||
|
||||
get rating() {
|
||||
return Object.assign({}, this.current.attributes.rating);
|
||||
}
|
||||
|
||||
start() {
|
||||
|
||||
const rating = this.rating;
|
||||
rating.timestamp = (new Date()).toISOString();
|
||||
|
||||
this.ratings.push(rating);
|
||||
|
||||
this.sendRatings();
|
||||
|
||||
if(this.current.downloaded)
|
||||
return Promise.resolve(this.file);
|
||||
|
||||
if(! this.current.downloading)
|
||||
this.current.download = this.download(this.current);
|
||||
|
||||
return this.current.download.then(rec => rec.file);
|
||||
|
||||
}
|
||||
|
||||
markInteresting(sec) {
|
||||
|
||||
if(this.interesting) return;
|
||||
|
||||
const rating = this.rating;
|
||||
|
||||
rating.rating = 'THUMBSUP';
|
||||
rating.elapsed = sec;
|
||||
rating.timestamp = (new Date()).toISOString();
|
||||
this.ratings.push(rating);
|
||||
|
||||
this.current.interesting = true;
|
||||
|
||||
}
|
||||
|
||||
next(sec) {
|
||||
|
||||
const rating = this.rating;
|
||||
|
||||
rating.rating = 'SKIP';
|
||||
rating.elapsed = Math.floor(sec);
|
||||
rating.timestamp = (new Date()).toISOString();
|
||||
this.ratings.push(rating);
|
||||
|
||||
this.current.skipped = true;
|
||||
|
||||
}
|
||||
|
||||
finished() {
|
||||
|
||||
const rating = this.rating;
|
||||
|
||||
if(! this.skipped) {
|
||||
rating.rating = 'COMPLETED';
|
||||
rating.elapsed = rating.duration;
|
||||
rating.timestamp = (new Date()).toISOString();
|
||||
this.ratings.push(rating);
|
||||
}
|
||||
|
||||
rimraf(this.file, ()=>{});
|
||||
this.completed.push(this.current);
|
||||
this.current = this.recommendations.shift();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
exports = module.exports = Story;
|
||||
85
lib/ui.js
Normal file
85
lib/ui.js
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
'use strict';
|
||||
|
||||
const EventEmitter = require('events'),
|
||||
keypress = require('keypress');
|
||||
|
||||
class UI extends EventEmitter {
|
||||
|
||||
constructor(config) {
|
||||
|
||||
super();
|
||||
|
||||
this.touchThreshold = config.touchThreshold || 24;
|
||||
this.releaseThreshold = config.releaseThreshold || 12;
|
||||
|
||||
if(process.platform == 'linux' && process.arch == 'arm')
|
||||
this.mprInit();
|
||||
|
||||
this.keyboardInit();
|
||||
|
||||
}
|
||||
|
||||
keyboardInit() {
|
||||
|
||||
keypress(process.stdin);
|
||||
|
||||
process.stdin.on('keypress', (ch, key) => {
|
||||
if(key && key.name == 'right') this.skip();
|
||||
if(key && key.name == 'left') this.rewind();
|
||||
if(key && key.name == 'up') this.volumeup();
|
||||
if(key && key.name == 'down') this.volumedown();
|
||||
if(key && key.name == 'space') this.pause();
|
||||
if(key && key.name == 'i') this.interesting();
|
||||
if(key && key.ctrl && key.name == 'c') process.exit();
|
||||
});
|
||||
|
||||
process.stdin.setRawMode(true);
|
||||
process.stdin.resume();
|
||||
|
||||
}
|
||||
|
||||
mprInit() {
|
||||
|
||||
const MPR121 = require('adafruit-mpr121'),
|
||||
mpr121 = new MPR121(0x5A, 1);
|
||||
|
||||
mpr121.setThresholds(this.touchThreshold, this.releaseThreshold);
|
||||
|
||||
mpr121.on('touch', (pin) => {
|
||||
if(pin === 0) this.skip();
|
||||
if(pin === 1) this.pause();;
|
||||
if(pin === 2) this.rewind();
|
||||
if(pin === 3) this.interesting();
|
||||
if(pin === 4) this.volumeup();
|
||||
if(pin === 5) this.volumedown();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
skip(pressed) {
|
||||
this.emit('skip');
|
||||
}
|
||||
|
||||
pause(pressed) {
|
||||
this.emit('pause');
|
||||
}
|
||||
|
||||
rewind(pressed) {
|
||||
this.emit('rewind');
|
||||
}
|
||||
|
||||
interesting(pressed) {
|
||||
this.emit('interesting');
|
||||
}
|
||||
|
||||
volumeup(pressed) {
|
||||
this.emit('volumeup');
|
||||
}
|
||||
|
||||
volumedown(pressed) {
|
||||
this.emit('volumedown');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
exports = module.exports = UI;
|
||||
40
package.json
40
package.json
|
|
@ -1,11 +1,9 @@
|
|||
{
|
||||
"name": "nprone-raspi",
|
||||
"version": "1.0.1",
|
||||
"description": "A NPR One client for the Raspberry Pi",
|
||||
"name": "npr-one",
|
||||
"version": "1.6.1",
|
||||
"description": "A NPR One command line client",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "gulp"
|
||||
},
|
||||
"bin": "./cli",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/adafruit/nprone_raspi.git"
|
||||
|
|
@ -14,6 +12,7 @@
|
|||
"npr",
|
||||
"one",
|
||||
"radio",
|
||||
"cli",
|
||||
"raspberry",
|
||||
"pi",
|
||||
"raspi",
|
||||
|
|
@ -21,24 +20,21 @@
|
|||
],
|
||||
"author": "Todd Treece <todd@uniontownlabs.org>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/adafruit/nprone_raspi/issues"
|
||||
},
|
||||
"homepage": "https://github.com/adafruit/nprone_raspi#readme",
|
||||
"devDependencies": {
|
||||
"gulp": "^3.9.0",
|
||||
"gulp-jshint": "^1.11.2",
|
||||
"gulp-mocha": "^2.1.3",
|
||||
"jshint-stylish": "^2.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"bunyan": "^1.4.0",
|
||||
"chalk": "^1.1.0",
|
||||
"chalk": "^1.1.3",
|
||||
"dotenv": "^1.2.0",
|
||||
"es6-shim": "^0.32.2",
|
||||
"gauge": "^2.2.1",
|
||||
"inquirer": "^0.9.0",
|
||||
"node-omx": "^0.2.1",
|
||||
"swagger-client": "^2.1.2",
|
||||
"wget-improved": "^1.1.1"
|
||||
"keypress": "^0.2.1",
|
||||
"mplayer": "^2.0.1",
|
||||
"npmlog": "^2.0.3",
|
||||
"npr-api": "^2.0.0",
|
||||
"rimraf": "^2.5.2",
|
||||
"string": "^3.3.1",
|
||||
"touch": "^1.0.0",
|
||||
"wget-improved": "^1.3.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"adafruit-mpr121": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
version.js
Normal file
2
version.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
const pkg = require('./package.json');
|
||||
console.log(pkg.version);
|
||||
Loading…
Reference in a new issue