From 48ed5168d477fe11fe4f21ae104e3750935b0943 Mon Sep 17 00:00:00 2001 From: Florrie Date: Sat, 15 Feb 2020 22:13:53 -0400 Subject: cli args (bass boost ur music) $ mtui --player sox --player-options bass +25 \; --- backend.js | 17 +++++++- crawlers.js | 2 +- general-util.js | 118 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ index.js | 39 ++++++++++++++++--- players.js | 33 ++++++++-------- todo.txt | 10 +++++ 6 files changed, 195 insertions(+), 24 deletions(-) diff --git a/backend.js b/backend.js index 379a16b..0cceeea 100644 --- a/backend.js +++ b/backend.js @@ -57,6 +57,7 @@ async function download(item, record) { class QueuePlayer extends EventEmitter { constructor({ + getPlayer, getRecordFor }) { super() @@ -68,11 +69,12 @@ class QueuePlayer extends EventEmitter { this.playedTrackToEnd = false this.timeData = null + this.getPlayer = getPlayer this.getRecordFor = getRecordFor } async setup() { - this.player = await getPlayer() + this.player = await this.getPlayer() if (!this.player) { return { @@ -560,9 +562,19 @@ class QueuePlayer extends EventEmitter { } class Backend extends EventEmitter { - constructor() { + constructor({ + playerName = null, + playerOptions = [] + } = {}) { super() + this.playerName = playerName; + this.playerOptions = playerOptions; + + if (playerOptions && !playerName) { + throw new Error(`Must specify playerName to specify playerOptions`); + } + this.queuePlayers = [] this.recordStore = new RecordStore() @@ -586,6 +598,7 @@ class Backend extends EventEmitter { async addQueuePlayer() { const queuePlayer = new QueuePlayer({ + getPlayer: () => getPlayer(this.playerName, this.playerOptions), getRecordFor: item => this.getRecordFor(item) }) diff --git a/crawlers.js b/crawlers.js index 578a9f2..92243c9 100644 --- a/crawlers.js +++ b/crawlers.js @@ -237,7 +237,7 @@ function getHTMLLinks(text) { function crawlLocal(dirPath, extensions = [ 'ogg', 'oga', - 'wav', 'mp3', 'mp4', 'm4a', 'aac', 'flac', + 'wav', 'mp3', 'mp4', 'm4a', 'aac', 'flac', 'opus', 'mod' ], isTop = true) { // If the passed path is a file:// URL, try to decode it: diff --git a/general-util.js b/general-util.js index 3aa4180..0a81cdc 100644 --- a/general-util.js +++ b/general-util.js @@ -192,3 +192,121 @@ module.exports.getTimeStrings = function({curHour, curMin, curSec, lenHour, lenM return module.exports.getTimeStringsFromSec(curSecTotal, lenSecTotal) } + +const parseOptions = async function(options, optionDescriptorMap) { + // This function is sorely lacking in comments, but the basic usage is + // as such: + // + // options is the array of options you want to process; + // optionDescriptorMap is a mapping of option names to objects that describe + // the expected value for their corresponding options. + // Returned is a mapping of any specified option names to their values, or + // a process.exit(1) and error message if there were any issues. + // + // Here are examples of optionDescriptorMap to cover all the things you can + // do with it: + // + // optionDescriptorMap: { + // 'telnet-server': {type: 'flag'}, + // 't': {alias: 'telnet-server'} + // } + // + // options: ['t'] -> result: {'telnet-server': true} + // + // optionDescriptorMap: { + // 'directory': { + // type: 'value', + // validate(name) { + // // const whitelistedDirectories = ['apple', 'banana'] + // if (whitelistedDirectories.includes(name)) { + // return true + // } else { + // return 'a whitelisted directory' + // } + // } + // }, + // 'files': {type: 'series'} + // } + // + // ['--directory', 'apple'] -> {'directory': 'apple'} + // ['--directory', 'artichoke'] -> (error) + // ['--files', 'a', 'b', 'c', ';'] -> {'files': ['a', 'b', 'c']} + // + // TODO: Be able to validate the values in a series option. + + const handleDashless = optionDescriptorMap[parseOptions.handleDashless] + const result = {} + for (let i = 0; i < options.length; i++) { + const option = options[i] + if (option.startsWith('--')) { + // --x can be a flag or expect a value or series of values + let name = option.slice(2).split('=')[0] // '--x'.split('=') = ['--x'] + let descriptor = optionDescriptorMap[name] + if (!descriptor) { + console.error(`Unknown option name: ${name}`) + process.exit(1) + } + if (descriptor.alias) { + name = descriptor.alias + descriptor = optionDescriptorMap[name] + } + if (descriptor.type === 'flag') { + result[name] = true + } else if (descriptor.type === 'value') { + let value = option.slice(2).split('=')[1] + if (!value) { + value = options[++i] + if (!value || value.startsWith('-')) { + value = null + } + } + if (!value) { + console.error(`Expected a value for --${name}`) + process.exit(1) + } + result[name] = value + } else if (descriptor.type === 'series') { + if (!options.slice(i).includes(';')) { + console.error(`Expected a series of values concluding with ; (\\;) for --${name}`) + process.exit(1) + } + const endIndex = i + options.slice(i).indexOf(';') + result[name] = options.slice(i + 1, endIndex) + i = endIndex + } + if (descriptor.validate) { + const validation = await descriptor.validate(result[name]) + if (validation !== true) { + console.error(`Expected ${validation} for --${name}`) + process.exit(1) + } + } + } else if (option.startsWith('-')) { + // mtui doesn't use any -x=y or -x y format optionuments + // -x will always just be a flag + let name = option.slice(1) + let descriptor = optionDescriptorMap[name] + if (!descriptor) { + console.error(`Unknown option name: ${name}`) + process.exit(1) + } + if (descriptor.alias) { + name = descriptor.alias + descriptor = optionDescriptorMap[name] + } + if (descriptor.type === 'flag') { + result[name] = true + } else { + console.error(`Use --${name} (value) to specify ${name}`) + process.exit(1) + } + } else if (handleDashless) { + handleDashless(option) + } + } + return result +} + +parseOptions.handleDashless = Symbol() + +module.exports.parseOptions = parseOptions diff --git a/index.js b/index.js index bb0daee..c40bbb9 100755 --- a/index.js +++ b/index.js @@ -3,6 +3,8 @@ // omg I am tired of code const { getAllCrawlersForArg } = require('./crawlers') +const { getPlayer } = require('./players') +const { parseOptions } = require('./general-util') const AppElement = require('./ui') const Backend = require('./backend') const TelnetServer = require('./telnet-server') @@ -50,7 +52,36 @@ process.on('unhandledRejection', error => { }) async function main() { - const backend = new Backend() + const playlistSources = [] + + const options = await parseOptions(process.argv.slice(2), { + 'player': { + type: 'value', + async validate(playerName) { + if (await getPlayer(playerName)) { + return true + } else { + return 'a known player identifier' + } + } + }, + 'player-options': { + type: 'series' + }, + [parseOptions.handleDashless](option) { + playlistSources.push(option) + } + }) + + if (options['player-options'] && !options['player']) { + console.error('--player must be specified in order to use --player-options') + process.exit(1) + } + + const backend = new Backend({ + playerName: options['player'], + playerOptions: options['player-options'] + }) const result = await backend.setup() if (result.error) { @@ -93,10 +124,8 @@ async function main() { }) const loadPlaylists = async () => { - for (let i = 2; i < process.argv.length; i++) { - if (!process.argv[i].startsWith('--')) { - await appElement.handlePlaylistSource(process.argv[i], true) - } + for (const source of playlistSources) { + await appElement.handlePlaylistSource(source, true) } } diff --git a/players.js b/players.js index 5d332b3..868129d 100644 --- a/players.js +++ b/players.js @@ -10,9 +10,11 @@ const util = require('util') const unlink = util.promisify(fs.unlink) class Player extends EventEmitter { - constructor() { + constructor(processOptions = []) { super() + this.processOptions = processOptions + this.disablePlaybackStatus = false this.isLooping = false this.isPaused = false @@ -78,7 +80,7 @@ module.exports.MPVPlayer = class extends Player { // The more powerful MPV player. MPV is virtually impossible for a human // being to install; if you're having trouble with it, try the SoX player. - this.process = spawn('mpv', this.getMPVOptions(file)) + this.process = spawn('mpv', this.getMPVOptions(file).concat(this.processOptions)) let lastPercent = 0 @@ -223,7 +225,7 @@ module.exports.SoXPlayer = class extends Player { // You don't get keyboard controls such as seeking or volume adjusting // with SoX, though. - this.process = spawn('play', [file]) + this.process = spawn('play', [file].concat(this.processOptions)) this.process.stdout.on('data', data => { process.stdout.write(data.toString()) @@ -276,19 +278,18 @@ module.exports.SoXPlayer = class extends Player { } } -module.exports.getPlayer = async function() { - if (await commandExists('mpv')) { - /* - if (await commandExists('socat')) { - return new module.exports.ControllableMPVPlayer() - } else { - return new module.exports.MPVPlayer() - } - */ - return new module.exports.ControllableMPVPlayer() - } else if (await commandExists('play')) { - return new module.exports.SoXPlayer() - } else { +module.exports.getPlayer = async function(name = null, options = []) { + if (await commandExists('mpv') && (name === null || name === 'mpv')) { + return new module.exports.ControllableMPVPlayer(options) + } else if (name === 'mpv') { + return null + } + + if (await commandExists('play') && (name === null || name === 'sox')) { + return new module.exports.SoXPlayer(options) + } else if (name === 'sox') { return null } + + return null } diff --git a/todo.txt b/todo.txt index 5e1fe65..418ee49 100644 --- a/todo.txt +++ b/todo.txt @@ -472,3 +472,13 @@ TODO: Update to work with IPC server mpv (and socat). TODO: Look into testing ^that on Windows. Remove mkfifo, since it's probably no longer necessary! + +TODO: Expand selection context menu by pressing the heading button! It should + show a list of the tracks contained within the selection. Selecting any + item should reveal that item in the main listing pane. + +TODO: Opening the selection contxt menu should show an option to either add or + remove the cursor-focused item from the selection - this would make + selection accessible when a keyboard or the shift key is inaccessible. + +TODO: Integrate the rest of the stuff that handles argv into parseOptions. -- cgit 1.3.0-6-gf8a5