diff --git a/bin/tag-reader-dummy b/bin/tag-reader-dummy index 6edc405..2ab3d27 100755 --- a/bin/tag-reader-dummy +++ b/bin/tag-reader-dummy @@ -1,5 +1,5 @@ #!/bin/bash -while date +%s ; do - sleep 5 +while echo 1234567890 ; do + sleep 0.2 done diff --git a/jukebox/library/file-backend.js b/jukebox/library/file-backend.js new file mode 100644 index 0000000..5b2e880 --- /dev/null +++ b/jukebox/library/file-backend.js @@ -0,0 +1,23 @@ +"use strict"; + +const glob = require('glob'); + +class FileBackend { + constructor(config) { + this.config = config; + } + + find(tag, callback) { + glob('media/' + tag + ' - *.mp3', (err, files) => { + if (files.length > 0) { + callback(files[0]); + } + }); + } +} + +module.exports = function(config) { + return new FileBackend(config); +}; + +// vim:ts=2 sw=2 et: diff --git a/jukebox/library/index.js b/jukebox/library/index.js new file mode 100644 index 0000000..437de26 --- /dev/null +++ b/jukebox/library/index.js @@ -0,0 +1,72 @@ +"use strict"; + +const EventEmitter = require('events').EventEmitter; + +class Library extends EventEmitter { + constructor(config, backend) { + super(); + + this.log_size = 10; + this.config = config; + this.backend = backend; + this.log = []; + } + + find(tag) { + this.backend.find(tag, path => { + if (!path) { + this.updateLog(new NotFoundLogEntry(tag)); + return; + } + + this.updateLog(new FileLogEntry(tag, path)); + }); + } + + updateLog(entry) { + this.log.unshift(entry); + if (this.log.length > this.log_size) { + this.log.pop(); + } + this.emit('play', entry); + } + + getLastTag() { + if (this.log.length == 0) { + return; + } + + return this.log[0].tag; + } + + getLog() { + return this.log; + } +} + +class LogEntry { + constructor(tag) { + this.tag = tag; + } + + toString() { + return this.tag; + } +} + +class NotFoundLogEntry extends LogEntry { + toString() { + return `Unknown media: ${this.tag}` + } +} + +class FileLogEntry extends LogEntry { + constructor(tag, path) { + super(tag); + this.path = path; + } +} + +module.exports.Library = Library; + +// vim:set ts=2 sw=2 et: diff --git a/jukebox/media-player.js b/jukebox/media-player.js index a8014ef..72d6563 100644 --- a/jukebox/media-player.js +++ b/jukebox/media-player.js @@ -1,18 +1,53 @@ "use strict"; +const throttle = require('throttle-debounce/throttle'); + const ChildProcessEmitter = require('./child-process').ChildProcessEmitter; +const DEFAULT_PAUSE_THROTTLE = 2000; +const DEFAULT_PLAY_THROTTLE = 5000; + class MediaPlayer extends ChildProcessEmitter { - constructor(config, logger) { - super(config.mpg321, logger); - this.stderrFilters.push(line => { - return line.substr(0, 3) == '@F ' - }); - } + constructor(config, logger) { + super(config.mpg321, logger); + + this.stderrFilters.push(line => { + return line.substr(0, 3) == '@F ' + }); + + this.pauseThrottled = throttle(config.pause_throttle || DEFAULT_PAUSE_THROTTLE, () => { + this._pause(); + }); + + this.playFileThrottled = throttle(config.play_throttle || DEFAULT_PLAY_THROTTLE, (path) => { + this._playFile(path); + }); + + // default to unthrottled + this.pause = this._pauseFile; + this.playFile = this._playFile; + } + + throttle() { + this.pause = this.pauseThrottled; + this.playFile = this.playFileThrottled; + } + + stop() { + this.send('STOP'); + } + + _pause() { + this.send('PAUSE'); + } + + _playFile(path) { + this.send("LOAD " + path); + } } module.exports = function(config, logger) { - return new MediaPlayer(config, logger); + return new MediaPlayer(config, logger); }; // vim:set ts=2 sw=2 et: diff --git a/jukebox/views.js b/jukebox/views.js index d5ccf57..513fe8a 100644 --- a/jukebox/views.js +++ b/jukebox/views.js @@ -36,7 +36,6 @@ class IndexView extends View { } partials() { - console.log(views.log); return { log: views.log.template } diff --git a/package.json b/package.json index cb56ef6..ff5dd96 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "glob": "^7.1.1", "morgan": "^1.7.0", "mustache": "^2.3.0", + "throttle-debounce": "^1.0.1", "winston": "^2.3.1", "ws": "^2.2.2" }, diff --git a/player.js b/player.js index 38f1525..ef58a3f 100644 --- a/player.js +++ b/player.js @@ -2,9 +2,11 @@ const config = require('./config.json') -const glob = require('glob'); - const views = require('./jukebox/views')(__dirname + '/templates/'); +const library = require('./jukebox/library'); + +const MediaLibraryFileBackend = require('./jukebox/library/file-backend')(config); +const MediaLibrary = new library.Library(config, MediaLibraryFileBackend); const express = require('express'); const morgan = require('morgan') @@ -21,6 +23,8 @@ logger.level = config.debug ? 'debug' : 'info'; const MediaPlayer = require('./jukebox/media-player')(config, logger); const TagReader = require('./jukebox/tag-reader')(config, logger); +MediaPlayer.throttle(); + var app = express(); var server = require('http').createServer(); @@ -28,8 +32,6 @@ const WebSocket = require('ws'); const WebSocketServer = WebSocket.Server; var wss = new WebSocketServer({server: server}); -var play_log = []; - function exitHandler(options, err) { logger.debug('closing child tasks'); if (err) logger.debug(err.stack); @@ -45,8 +47,8 @@ function exitHandler(options, err) { process.on('exit', exitHandler.bind(null)); process.on('SIGINT', exitHandler.bind(null, {exit:true})); -TagReader.on('message', data => { - processLine(data); +TagReader.on('message', tag => { + MediaLibrary.find(tag); }); if (config.debug) { @@ -55,61 +57,28 @@ if (config.debug) { }); } -var last_tag = null; -var debounce_until = 0; - -function lookup(tag, cb) { - if (tag === config.stop_id) { - return cb('STOP'); - } - if (tag === config.pause_id) { - return cb('PAUSE', 2); - } - - glob('media/' + tag + ' - *.mp3', (er, files) => { - if (files.length > 0) { - appendLog(files[0]); - return cb("LOAD " + files[0], 5); - } - appendLog(tag); - }); -} - -function appendLog(message) { - if (play_log.length > 10) { - play_log.pop(); - } - - play_log.unshift(message); - +MediaLibrary.on('play', event => { var data = { - html: views.log.render({ play_log: play_log }), - tag: last_tag + html: views.log.render({ play_log: MediaLibrary.getLog() }), + tag: MediaLibrary.getLastTag() }; wss.broadcast(JSON.stringify(data)); -} +}); -function processLine(data) { - if (Date.now() / 1000 < debounce_until) { - return; +MediaLibrary.on('play', event => { + if (event.tag === config.stop_id) { + MediaPlayer.stop(); } - last_tag = String(data).trim(); - logger.debug("got tag from tag reader", { tag: last_tag }); + if (event.tag === config.pause_id) { + MediaPlayer.pause(); + } - // min 1 sec debounce - debounce_until = Date.now() / 1000 + 1; - - lookup(last_tag, function(action, wait_sec) { - wait_sec = wait_sec || 1; - debounce_until = Date.now() / 1000 + wait_sec; - if (action) { - logger.info("sending action to media player", { action: action, tag: last_tag }); - MediaPlayer.send(action + "\n"); - } - }); -} + if (event.path) { + MediaPlayer.playFile(event.path); + } +}); app.use(morgan('dev')) app.use(express.static(__dirname + '/static')) @@ -117,9 +86,9 @@ app.use(express.static(__dirname + '/static')) app.get('/', function (req, res, next) { try { var index = views.index.render({ - last_tag: last_tag + last_tag: MediaLibrary.getLastTag() , config: config - , play_log: play_log + , play_log: MediaLibrary.getLog() }); var html = views.base.render({