├── .gitignore ├── LICENSE ├── README.md ├── cli ├── index.js ├── lib ├── auth.js ├── player.js ├── story.js └── ui.js ├── logo.txt ├── package.json └── version.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.swo 3 | *.swp 4 | .DS_Store 5 | .env 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Adafruit Industries 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NPR One CLI 2 | 3 | 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). 4 | 5 | ## Installation 6 | 7 | This package requires the latest stable version of [Node.js](https://nodejs.org) (v6.0 or higher due to the use of es6). 8 | 9 | ```sh 10 | $ node -v 11 | v6.2.0 12 | ``` 13 | 14 | Install `mplayer` on OS X using [homebrew](http://brew.sh/): 15 | 16 | ``` 17 | $ brew install mplayer 18 | ``` 19 | 20 | Install `mplayer` on Linux: 21 | 22 | ``` 23 | $ sudo apt-get install -y mplayer 24 | ``` 25 | 26 | Make sure you have the latest stable [node.js](https://nodejs.org/en/) installed (6.0 or higher), and then run: 27 | 28 | ``` 29 | npm install -g npr-one 30 | ``` 31 | 32 | ## Usage 33 | 34 | 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. 35 | 36 | ``` 37 | $ npr-one 38 | 39 | \\\\\\\\\\\\\\\\\\\\\\\\\\\>>>>>>>>>>>>>>>>>>>>>>>>>> \\\\\\\\\\\\\\\\\\\\\\\\\\ 40 | \\\\\\\\\\\\\\\\\\\\\\\\\\\>>>>>>>>>>>>>>>>>>>>>>>>>> \\\\\\\\\\\\\\\\\\\\\\\\\\ 41 | \\\\\\\\\\\\\>>\\\\\\\\\\\\>>>>>>>>>>>>>>>>>>>>>>>>>> \\\\\\\\\\\\\\>>>\\\\\\\\\ 42 | \\\\\\\ >\\\\\\\\>>>>>>> \>>>>>>>> \\\\\\\\ (\\\\\\\\ 43 | \\\\\\\ .>>>>= \\\\\\\\>>>>>>> =>>>> >>>>>>> \\\\\\\\ (>>>>\\\\\\\\\ 44 | \\\\\\\ )\\\\\ (\\\\\\\>>>>>>> >>>>>> )>>>>>> \\\\\\\\ .\\\\\\\\\\\\\\ 45 | \\\\\\\ )\\\\\ (\\\\\\\>>>>>>> >>>>>> )>>>>>> \\\\\\\\ )\\\\\\\\\\\\\\ 46 | \\\\\\\ )\\\\\ (\\\\\\\>>>>>>> >>>>>\ >>>>>>> \\\\\\\\ )\\\\\\\\\\\\\\ 47 | \\\\\\\ )\\\\\ (\\\\\\\>>>>>>> ->>>>>>>> \\\\\\\\ )\\\\\\\\\\\\\\ 48 | \\\\\\\>>>>\\\\\>>>>\\\\\\\>>>>>>> >>>>(>>>>>>>>>>> \\\\\\\\>>>>\\\\\\\\\\\\\\ 49 | \\\\\\\\\\\\\\\\\\\\\\\\\\\>>>>>>> >>>>>>>>>>>>>>>> \\\\\\\\\\\\\\\\\\\\\\\\\\ 50 | \\\\\\\\\\\\\\\\\\\\\\\\\\\>>>>>>>===>>>>>>>>>>>>>>>> \\\\\\\\\\\\\\\\\\\\\\\\\\ 51 | 52 | [downloaded] WYPR FM 53 | [downloaded] NPR thanks our sponsors 54 | [playing] WYPR FM 55 | [downloaded] Welcome To Czechia: Czech Republic Looks To Adopt Shorter Name 56 | [downloaded] Belgian Transport Minister Resigns Over Airport Security Debate 57 | [downloaded] Tax Season 58 | [downloaded] Adapting To A More Extreme Climate, Coastal Cities Get Creative 59 | [downloaded] NPR thanks our sponsors 60 | ``` 61 | 62 | ### Keyboard Controls 63 | 64 | ``` 65 | space play/pause 66 | ↑ volume up 67 | ↓ volume down 68 | ← rewind 15 seconds 69 | → skip to the next story 70 | i mark as interesting 71 | ``` 72 | 73 | ## License 74 | 75 | Copyright (c) 2016 Adafruit Industries. Licensed under the MIT license. 76 | 77 | The NPR logo is a registered trademark of NPR used with permission from NPR. All rights reserved. 78 | -------------------------------------------------------------------------------- /cli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | SOURCE="${BASH_SOURCE[0]}" 4 | while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink 5 | DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" 6 | SOURCE="$(readlink "$SOURCE")" 7 | [[ $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 8 | done 9 | DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" 10 | 11 | cd $DIR 12 | 13 | cat logo.txt 14 | 15 | _LATEST=$(npm view npr-one version) 16 | _CURRENT=$(node version.js) 17 | _RELEASE='/etc/os-release' 18 | 19 | if [ -f $_RELEASE ]; then 20 | 21 | source $_RELEASE 22 | 23 | if [ $ID == 'raspbian' ]; then 24 | sudo chown -R $(whoami) $(npm config get prefix)/{lib/node_modules,bin,share} 25 | fi 26 | 27 | fi 28 | 29 | if [ $_CURRENT != $_LATEST ]; then 30 | echo "updating..." 31 | npm install -g npr-one 32 | fi 33 | 34 | node index.js 35 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.title = 'npr-one'; 4 | 5 | if(process.platform != 'linux' && process.platform != 'darwin') { 6 | console.error('Your platform is not currently supported'); 7 | process.exit(1); 8 | } 9 | 10 | const NPR = require('npr-api'), 11 | chalk = require('chalk'), 12 | auth = require('./lib/auth'), 13 | fs = require('fs'), 14 | path = require('path'), 15 | config = path.join(process.env['HOME'], '.npr-one'), 16 | dotenv = require('dotenv').load({silent: true, path: config}), 17 | Player = require('./lib/player'), 18 | Story = require('./lib/story'), 19 | UI = require('./lib/ui'); 20 | 21 | const logo = fs.readFileSync(path.join(__dirname,'logo.txt'), 'utf8'); 22 | 23 | const npr = new NPR(), 24 | story = new Story(npr), 25 | player = new Player(); 26 | 27 | console.log('connecting to npr one...'); 28 | 29 | // silence swagger log output 30 | process.env.NODE_ENV = 'test'; 31 | 32 | npr.one 33 | .init() 34 | .then(auth.getToken.bind(auth, npr)) 35 | .then((token) => { 36 | process.stdout.write('\x1B[2J'); 37 | process.stdout.write('\x1B[0f'); 38 | console.log(logo); 39 | return npr.one.setAccessToken(token); 40 | }) 41 | .then(story.getRecommendations.bind(story)) 42 | .then(player.load.bind(player)) 43 | .then(() => { 44 | 45 | const ui = new UI({ 46 | touchThreshold: process.env.MPR121_TOUCH, 47 | releaseThreshold: process.env.MPR121_RELEASE 48 | }); 49 | 50 | ui.on('skip', player.skip.bind(player)); 51 | ui.on('pause', player.pause.bind(player)); 52 | ui.on('rewind', player.rewind.bind(player)); 53 | ui.on('interesting', player.interesting.bind(player)); 54 | ui.on('volumeup', player.increaseVolume.bind(player)); 55 | ui.on('volumedown', player.decreaseVolume.bind(player)); 56 | 57 | }) 58 | .catch(function(err) { 59 | console.error(err, err.stack); 60 | }); 61 | -------------------------------------------------------------------------------- /lib/auth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const inquirer = require('inquirer'), 4 | path = require('path'), 5 | config = path.join(process.env['HOME'], '.npr-one'), 6 | fs = require('fs'); 7 | 8 | let npr; 9 | 10 | const client_creds = [ 11 | { 12 | type: 'input', 13 | name: 'CLIENT_ID', 14 | message: 'NPR Application ID', 15 | }, 16 | { 17 | type: 'input', 18 | name: 'CLIENT_SECRET', 19 | message: 'NPR Application Secret', 20 | } 21 | ]; 22 | 23 | const device_code = [ 24 | { 25 | type: 'list', 26 | name: 'device', 27 | message: 'Authorize the NPR One CLI @ ', 28 | choices: ['Complete', 'Exit'] 29 | } 30 | ]; 31 | 32 | const requestDeviceCode = () => { 33 | 34 | return new Promise((resolve, reject) => { 35 | 36 | npr.one.authorization 37 | .generateDeviceCode({ 38 | client_id: process.env.CLIENT_ID, 39 | client_secret: process.env.CLIENT_SECRET, 40 | scope: 'listening.readonly listening.write identity.readonly' 41 | }) 42 | .then((res) => { 43 | 44 | device_code[0].message += `${res.verification_uri} using code: ${res.user_code}`; 45 | 46 | inquirer.prompt(device_code, (answers) => { 47 | 48 | if(answers.device === 'Exit') 49 | process.exit(); 50 | 51 | resolve(res.device_code); 52 | 53 | }); 54 | 55 | }) 56 | .catch(reject); 57 | 58 | }); 59 | 60 | }; 61 | 62 | const requestToken = (code) => { 63 | 64 | return new Promise((resolve, reject) => { 65 | 66 | npr.one.authorization 67 | .createToken({ 68 | grant_type: 'device_code', 69 | client_id: process.env.CLIENT_ID, 70 | client_secret: process.env.CLIENT_SECRET, 71 | code: code 72 | }) 73 | .then((res) => { 74 | process.env.NPR_ACCESS_TOKEN = res.access_token; 75 | resolve(res.access_token); 76 | }) 77 | .catch(reject); 78 | 79 | }); 80 | 81 | }; 82 | 83 | const getClientCreds = () => { 84 | 85 | return new Promise((resolve, reject) => { 86 | 87 | if(process.env.CLIENT_ID && process.env.CLIENT_SECRET) 88 | return resolve(); 89 | 90 | inquirer.prompt(client_creds, (answers) => { 91 | 92 | process.env.CLIENT_ID = answers.CLIENT_ID; 93 | process.env.CLIENT_SECRET = answers.CLIENT_SECRET; 94 | 95 | resolve(); 96 | 97 | }); 98 | 99 | }); 100 | 101 | }; 102 | 103 | exports.getToken = (api) => { 104 | 105 | npr = api; 106 | 107 | return new Promise((resolve, reject) => { 108 | 109 | if(process.env.NPR_ACCESS_TOKEN) 110 | return resolve(process.env.NPR_ACCESS_TOKEN); 111 | 112 | getClientCreds() 113 | .then(requestDeviceCode.bind(this)) 114 | .then(requestToken.bind(this)) 115 | .then((token) => { 116 | fs.writeFileSync(config, `NPR_ACCESS_TOKEN=${process.env.NPR_ACCESS_TOKEN}\n`); 117 | resolve(token); 118 | }) 119 | .catch(reject); 120 | 121 | }); 122 | 123 | }; 124 | 125 | -------------------------------------------------------------------------------- /lib/player.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Mplayer = require('mplayer'), 4 | chalk = require('chalk'), 5 | log = require('npmlog'), 6 | EventEmitter = require('events'); 7 | 8 | const resetLine = function() { 9 | process.stdout.clearLine(); 10 | process.stdout.cursorTo(0) 11 | }; 12 | 13 | class Player extends EventEmitter { 14 | 15 | constructor() { 16 | super(); 17 | 18 | log.addLevel('playing', 2001, {bg: 'black', fg: 'green'}, '[playing]'); 19 | log.addLevel('skipped', 2002, {bg: 'black', fg: 'red'}, '[skipped]'); 20 | log.addLevel('finished', 2003, {bg: 'black', fg: 'red'}, '[finished]'); 21 | log.addLevel('volume', 2004, {bg: 'black', fg: 'red'}, '[volume]'); 22 | log.addLevel('interesting', 2005, {bg: 'black', fg: 'red'}, '[interesting]'); 23 | 24 | this.story = null; 25 | this.volume = 100; 26 | this.time = 0; 27 | this.playing = false; 28 | 29 | this.player = new Mplayer(); 30 | this.player.on('stop', this.done.bind(this)); 31 | this.player.on('time', (time) => this.time = time); 32 | this.player.on('error', console.error); 33 | 34 | } 35 | 36 | load(story) { 37 | this.story = story; 38 | return this.play(); 39 | } 40 | 41 | play() { 42 | 43 | if(! this.story) return; 44 | 45 | this.story.start().then((file) => { 46 | 47 | this.player.openFile(file); 48 | 49 | resetLine(); 50 | log.playing(this.story.title); 51 | 52 | this.player.play(); 53 | this.time = 0; 54 | this.playing = true; 55 | 56 | }); 57 | 58 | } 59 | 60 | increaseVolume() { 61 | 62 | if(! this.player) return; 63 | 64 | this.volume += 10; 65 | 66 | if(this.volume > 100) 67 | this.volume = 100; 68 | 69 | this.player.volume(this.volume); 70 | 71 | resetLine(); 72 | log.volume(this.volume); 73 | 74 | } 75 | 76 | decreaseVolume() { 77 | 78 | if(! this.player) 79 | return; 80 | 81 | this.volume -= 10; 82 | 83 | if(this.volume < 10) 84 | this.volume = 10; 85 | 86 | this.player.volume(this.volume); 87 | 88 | resetLine(); 89 | log.volume(this.volume); 90 | 91 | } 92 | 93 | pause() { 94 | 95 | if(! this.player) return; 96 | 97 | if(this.playing) { 98 | this.player.pause(); 99 | this.playing = false; 100 | } else { 101 | this.player.play(); 102 | this.playing = true; 103 | } 104 | 105 | } 106 | 107 | rewind() { 108 | 109 | if(! this.player) return; 110 | 111 | if(this.time < 15) 112 | this.time = 15; 113 | 114 | this.player.seek(this.time-15); 115 | 116 | } 117 | 118 | skip() { 119 | 120 | if(! this.player) return; 121 | if(! this.story) return; 122 | if(! this.story.canSkip) return; 123 | 124 | resetLine(); 125 | log.skipped(this.story.title); 126 | 127 | this.story.next(this.time); 128 | this.player.stop(); 129 | 130 | } 131 | 132 | interesting() { 133 | 134 | if(! this.player) return; 135 | if(! this.story) return; 136 | if(this.story.interesting) return; 137 | 138 | resetLine(); 139 | log.interesting(this.story.title); 140 | 141 | this.story.markInteresting(this.time); 142 | 143 | } 144 | 145 | done() { 146 | 147 | if(! this.story.skipped) { 148 | resetLine(); 149 | log.finished(this.story.title); 150 | } 151 | 152 | this.story.finished(); 153 | this.time = 0; 154 | this.playing = false; 155 | this.play(); 156 | 157 | } 158 | 159 | } 160 | 161 | exports = module.exports = Player; 162 | -------------------------------------------------------------------------------- /lib/story.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const rimraf = require('rimraf'), 4 | touch = require('touch'), 5 | fs = require('fs'), 6 | url = require('url'), 7 | S = require('string'), 8 | Gauge = require('gauge'), 9 | chalk = require('chalk'), 10 | wget = require('wget-improved'); 11 | 12 | class Story { 13 | 14 | constructor(npr) { 15 | 16 | this.npr = npr; 17 | this.recommendations = []; 18 | this.completed = []; 19 | this.ratings = []; 20 | this.current = null; 21 | 22 | } 23 | 24 | download(rec) { 25 | 26 | return new Promise((resolve, reject) => { 27 | 28 | const filename = `/tmp/npr-${rec.attributes.uid}`; 29 | 30 | try { 31 | fs.accessSync(filename); 32 | rec.file = filename; 33 | rec.downloaded = true; 34 | rec.downloading = false; 35 | rec.download = Promise.resolve(rec); 36 | console.log(`${chalk.red.bgBlack('[downloaded]')} ${rec.attributes.title}`); 37 | return resolve(rec); 38 | } catch(e) {} 39 | 40 | touch.sync(`/tmp/npr-${rec.attributes.uid}`); 41 | rec.file = filename; 42 | rec.downloading = true; 43 | 44 | const bar = new Gauge(process.stderr, { 45 | cleanupOnExit: false, 46 | template: [ 47 | {value: chalk.green.bgBlack('[download]'), kerning: 1}, 48 | {type: 'section', kerning: 1, length: 20}, 49 | {type: 'progressbar' } 50 | ] 51 | }); 52 | 53 | wget.download(rec.links.audio[0].href, rec.file) 54 | .on('error', reject) 55 | .on('progress', (progress) => { 56 | bar.show(rec.attributes.title, progress); 57 | }) 58 | .on('end', () => { 59 | bar.disable(); 60 | console.log(`${chalk.red.bgBlack('[downloaded]')} ${rec.attributes.title}`); 61 | rec.downloaded = true; 62 | rec.downloading = false; 63 | resolve(rec); 64 | }); 65 | 66 | }); 67 | 68 | } 69 | 70 | fetchNew() { 71 | 72 | var next = this.recommendations.find((rec) => { 73 | return !rec.file && !rec.downloaded && !rec.downloading; 74 | }); 75 | 76 | if(! next) return; 77 | 78 | next.download = this.download(next); 79 | next.download.then(() => this.fetchNew()); 80 | 81 | return next.download; 82 | 83 | } 84 | 85 | getRecommendations() { 86 | 87 | return this.npr.one.listening.getRecommendations({ channel: 'npr' }) 88 | .then((rec) => { 89 | this.recommendations = rec.items; 90 | return this.fetchNew(); 91 | }) 92 | .then(() => this.recommendations[1].download) 93 | .then(() => { 94 | this.current = this.recommendations.shift(); 95 | return this; 96 | }); 97 | 98 | } 99 | 100 | sendRatings() { 101 | 102 | let args = url.parse(this.current.links.recommendations[0].href, true).query; 103 | args.body = this.ratings; 104 | 105 | return this.npr.one.listening.postRating(args) 106 | .then((res) => { 107 | res.items.forEach((rec) => { 108 | if(this.checkExisting(rec)) return; 109 | this.recommendations.push(rec); 110 | }); 111 | 112 | this.ratings = []; 113 | 114 | return this.fetchNew(); 115 | 116 | }) 117 | .catch(() => {}); 118 | 119 | } 120 | 121 | checkExisting(rec) { 122 | 123 | if(rec.attributes.uid == this.id) return true; 124 | if(rec.attributes.type == 'stationId') return true; 125 | 126 | const exists = this.recommendations.find((existing) => { 127 | return rec.attributes.uid == existing.attributes.uid; 128 | }); 129 | 130 | const completed = this.completed.find((existing) => { 131 | return rec.attributes.uid == existing.attributes.uid; 132 | }); 133 | 134 | if(exists || completed) 135 | return true; 136 | 137 | return false; 138 | 139 | } 140 | 141 | get id() { 142 | return this.current.attributes.uid; 143 | } 144 | 145 | get file() { 146 | return this.current.file; 147 | } 148 | 149 | get skipped() { 150 | return this.current.skipped; 151 | } 152 | 153 | get canSkip() { 154 | return this.current.attributes.skippable; 155 | } 156 | 157 | get interesting() { 158 | return this.current.interesting; 159 | } 160 | 161 | get title() { 162 | return this.current.attributes.title; 163 | } 164 | 165 | get rating() { 166 | return Object.assign({}, this.current.attributes.rating); 167 | } 168 | 169 | start() { 170 | 171 | const rating = this.rating; 172 | rating.timestamp = (new Date()).toISOString(); 173 | 174 | this.ratings.push(rating); 175 | 176 | this.sendRatings(); 177 | 178 | if(this.current.downloaded) 179 | return Promise.resolve(this.file); 180 | 181 | if(! this.current.downloading) 182 | this.current.download = this.download(this.current); 183 | 184 | return this.current.download.then(rec => rec.file); 185 | 186 | } 187 | 188 | markInteresting(sec) { 189 | 190 | if(this.interesting) return; 191 | 192 | const rating = this.rating; 193 | 194 | rating.rating = 'THUMBSUP'; 195 | rating.elapsed = sec; 196 | rating.timestamp = (new Date()).toISOString(); 197 | this.ratings.push(rating); 198 | 199 | this.current.interesting = true; 200 | 201 | } 202 | 203 | next(sec) { 204 | 205 | const rating = this.rating; 206 | 207 | rating.rating = 'SKIP'; 208 | rating.elapsed = Math.floor(sec); 209 | rating.timestamp = (new Date()).toISOString(); 210 | this.ratings.push(rating); 211 | 212 | this.current.skipped = true; 213 | 214 | } 215 | 216 | finished() { 217 | 218 | const rating = this.rating; 219 | 220 | if(! this.skipped) { 221 | rating.rating = 'COMPLETED'; 222 | rating.elapsed = rating.duration; 223 | rating.timestamp = (new Date()).toISOString(); 224 | this.ratings.push(rating); 225 | } 226 | 227 | rimraf(this.file, ()=>{}); 228 | this.completed.push(this.current); 229 | this.current = this.recommendations.shift(); 230 | 231 | } 232 | 233 | } 234 | 235 | exports = module.exports = Story; 236 | -------------------------------------------------------------------------------- /lib/ui.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EventEmitter = require('events'), 4 | keypress = require('keypress'); 5 | 6 | class UI extends EventEmitter { 7 | 8 | constructor(config) { 9 | 10 | super(); 11 | 12 | this.touchThreshold = config.touchThreshold || 24; 13 | this.releaseThreshold = config.releaseThreshold || 12; 14 | 15 | if(process.platform == 'linux' && process.arch == 'arm') 16 | this.mprInit(); 17 | 18 | this.keyboardInit(); 19 | 20 | } 21 | 22 | keyboardInit() { 23 | 24 | keypress(process.stdin); 25 | 26 | process.stdin.on('keypress', (ch, key) => { 27 | if(key && key.name == 'right') this.skip(); 28 | if(key && key.name == 'left') this.rewind(); 29 | if(key && key.name == 'up') this.volumeup(); 30 | if(key && key.name == 'down') this.volumedown(); 31 | if(key && key.name == 'space') this.pause(); 32 | if(key && key.name == 'i') this.interesting(); 33 | if(key && key.ctrl && key.name == 'c') process.exit(); 34 | }); 35 | 36 | process.stdin.setRawMode(true); 37 | process.stdin.resume(); 38 | 39 | } 40 | 41 | mprInit() { 42 | 43 | const MPR121 = require('adafruit-mpr121'), 44 | mpr121 = new MPR121(0x5A, 1); 45 | 46 | mpr121.setThresholds(this.touchThreshold, this.releaseThreshold); 47 | 48 | mpr121.on('touch', (pin) => { 49 | if(pin === 0) this.skip(); 50 | if(pin === 1) this.pause();; 51 | if(pin === 2) this.rewind(); 52 | if(pin === 3) this.interesting(); 53 | if(pin === 4) this.volumeup(); 54 | if(pin === 5) this.volumedown(); 55 | }); 56 | 57 | } 58 | 59 | skip(pressed) { 60 | this.emit('skip'); 61 | } 62 | 63 | pause(pressed) { 64 | this.emit('pause'); 65 | } 66 | 67 | rewind(pressed) { 68 | this.emit('rewind'); 69 | } 70 | 71 | interesting(pressed) { 72 | this.emit('interesting'); 73 | } 74 | 75 | volumeup(pressed) { 76 | this.emit('volumeup'); 77 | } 78 | 79 | volumedown(pressed) { 80 | this.emit('volumedown'); 81 | } 82 | 83 | } 84 | 85 | exports = module.exports = UI; 86 | -------------------------------------------------------------------------------- /logo.txt: -------------------------------------------------------------------------------- 1 |  2 | ██████████████████████████████████████████████████████████████████████████████ 3 | ██████████████████████████████████████████████████████████████████████████████ 4 | ██████████████████████████████████████████████████████████████████████████████ 5 | ███████▄▄▄▄▄███▄▄████████████████▄▄▄▄▄███▄▄█████████████████▄▄▄▄▄▄████████████ 6 | ███████████▄▄▄▄███▄██████████████████▄▄▄████▄████████████████████▄▄▄▄█████████ 7 | █████████████████████████████████████████▄████████████████████████████████████ 8 | ███████████████████████████████████████████████████████████████▄██████████████ 9 | █████████████████████████████████████████▄████████████████████████████████████ 10 | ████████████████████████████████████▄▄▄▄████▄█████████████████████████████████ 11 | ███████▄▄▄██████▄▄▄█████████████████▄███▄▄▄█████████████████▄▄▄▄██████████████ 12 | ██████████████████████████████████████████████████████████████████████████████ 13 | ██████████████████████████████████████████████████████████████████████████████ 14 | █████████████████████████████████▄▄▄██████████████████████████████████████████ 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "npr-one", 3 | "version": "1.6.1", 4 | "description": "A NPR One command line client", 5 | "main": "index.js", 6 | "bin": "./cli", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/adafruit/nprone_raspi.git" 10 | }, 11 | "keywords": [ 12 | "npr", 13 | "one", 14 | "radio", 15 | "cli", 16 | "raspberry", 17 | "pi", 18 | "raspi", 19 | "pi" 20 | ], 21 | "author": "Todd Treece ", 22 | "license": "MIT", 23 | "dependencies": { 24 | "chalk": "^1.1.3", 25 | "dotenv": "^1.2.0", 26 | "gauge": "^2.2.1", 27 | "inquirer": "^0.9.0", 28 | "keypress": "^0.2.1", 29 | "mplayer": "^2.0.1", 30 | "npmlog": "^2.0.3", 31 | "npr-api": "^2.0.0", 32 | "rimraf": "^2.5.2", 33 | "string": "^3.3.1", 34 | "touch": "^1.0.0", 35 | "wget-improved": "^1.3.0" 36 | }, 37 | "optionalDependencies": { 38 | "adafruit-mpr121": "^1.0.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /version.js: -------------------------------------------------------------------------------- 1 | const pkg = require('./package.json'); 2 | console.log(pkg.version); 3 | --------------------------------------------------------------------------------