UNPKG

mp3tag

Version:

A library for reading/writing mp3 tag data

268 lines (219 loc) 8.33 kB
/** This module implements the logic to parse the given command line arguments * into a task list. */ const _ = require('lodash'); const taskDefinitions = {}; function isTask(param) { return typeof(param) === 'string' && taskDefinitions[param] !== undefined; } /** @typedef {{args:number, min_args:number, max_args:number, type:string}} Options */ /** @typedef {(data:TaskData)=>Promise<TaskData>} ActionFn */ class TaskDefinition { /** * @param {string} key the task key, which is used to identify the task when parsing cmd args * @param {Options} options the task's type and parameter options * @param {ActionFn} action the task function to execute */ constructor(key, options, action) { this.key = key; this.options = this.verifyOptions(options); this.type = options.type; this.action = action; } verifyOptions(options) { options = options || {}; // First special case (no arguments at all) if (options.min_args === undefined && options.max_args === undefined && options.args === undefined) { options.min_args = 0; options.max_args = 0; } // Lower bound not defined if (options.min_args === undefined) { // calc min_args if (options.args !== undefined) { options.min_args = options.args; } else { options.min_args = 0; // only max_args or no args defined -> 0 args min } } // Upper bound not defined if (options.max_args === undefined) { // calc max_args if (options.args !== undefined) { options.max_args = options.args; } else { options.max_args = 99; // min_args was defined so interpret as arbitrary number of arguments } } if (typeof(options.type) !== 'string') { options.type = 'read'; } return options; } /** Generates the string for the first column (--<key> <arg_display>) for the command * If fieldSize is given, then the string is filled up with spaces until it reaches the length. */ to_string(fieldSize) { let result = ' --' + this.key; if (typeof(this.options.arg_display) === 'string') { result += ' ' + this.options.arg_display; } if (fieldSize) { while (result.length < fieldSize) { result += ' '; } } return result; } description() { if (typeof(this.options.help_text) === 'string') { return this.options.help_text; } return '<no description>'; } } class Task extends TaskDefinition { constructor(definition) { super(definition.key, definition.options, definition.action); this.args = []; } /** Executes this task with the current arguments * * @param {TagData?} data the optional data to pass to the task * * @return {Promise<TagData>} resolves to the result of the task's action */ run(data) { return this.action(data); } /** Reads this tasks arguments from the given argument list. * The number of arguments is defined by min_args and max_args. * Everything, which is not a task keyword itself, can be passed as argument * to the task. * * @param {string[]} argv the argument list (excluding the task itself) */ readArgs(argv) { //Read min_args amount of arguments while (this.args.length < this.options.min_args) { var param = argv[0]; if (isTask(param)) { throw new Error(`Not enough arguments found for '${this.key}' task`); } this.args.push(param); argv.shift(); } //Now read up to max_args optional arguments while(this.args.length < this.options.max_args && !isTask(argv[0])) { this.args.push(argv.shift()); } } } // Define module module = module.exports = {}; /** Define a task with the set of options and an action. The task is then added to the list * of known tasks * * @param {string} key the key, which identifies the task (eg. 'in' to use --in) * @param {Options} options task's type and argument options * @param {ActionFn} action the action to execute... * Source actions don't receive an object, but return one and * Sinks receive an object, but don't return anything * options don't receive and don't return anything */ module.defineTask = function(key, options, action) { taskDefinitions['--'+key] = new TaskDefinition(key, options, action); }; /** Run method, which should be used to start the process * * @param {string[]} argv the command line arguments passed to the script excluding the script call itself. * * @return {Promise<void>} resolves once, all tasks have completed */ module.run = async function(argv) { let tasks = []; while(argv.length > 0) { const key = argv.shift(); if (taskDefinitions[key] !== undefined) { const task = new Task(taskDefinitions[key]); task.readArgs(argv); tasks.push(task); } else { throw new Error("Passed unknown task '" + key + "' as argument."); } } // Validate and execute task list if (tasks.length == 0) { tasks.push(new Task(taskDefinitions['--help'])); // Execute help if no commands are passed } let sourceTask; let sinkTask; let optionTasks; //Special case... ignore all other tasks if help is specified if (_.some(tasks, (task) => { return task.type === 'help'; })) { tasks = [ new Task(taskDefinitions['--help']) ]; } else { //Ensure that we have exactly one source and at most one sink. const catTasks = _.groupBy(tasks, (task) => { return task.type; }); if (catTasks['source'] === undefined || catTasks['source'].length !== 1) { throw new Error("Wrong arguments passed. A source is required."); } if (catTasks['sink'] !== undefined && catTasks['sink'].length > 1) { throw new Error("Wrong arguments passed. At most one sink is allowed"); } sourceTask = catTasks['source'][0]; sinkTask = catTasks['sink'] ? catTasks['sink'][0] : undefined; optionTasks = catTasks['option']; _.remove(tasks, (task) => { return task.type === 'source' || task.type === 'sink' || task.type === 'option'; }); //Now define order of execution... // first options, then source, then read or then write, then sink const sortedTasks = optionTasks || []; sortedTasks.push(sourceTask); _.each(tasks, (task) => { sortedTasks.push(task) }); if (sinkTask) { sortedTasks.push(sinkTask); } tasks = sortedTasks; } let data; // The data to manipulate (gets set by the source task) // Now execute all tasks in order for (const task of tasks) { if (task.type === 'source') { // source task will set the data to pass to the following tasks data = await task.run(); } else { // for all other tasks the current data is just passed in and the result ignored await task.run(data); } } // All tasks processed } const categories = [ {key:'help', description:'Help'}, {key:'option', description:'Options'}, {key:'source', description:'File source commands'}, {key:'read', description:'Reading commands'}, {key:'write', description:'Modifying commands'}, {key:'sink', description:'File target commands (save)'} ]; // Define help-task module.defineTask('help', { help_text: 'Display this help text', type:'help' }, async function() { // Calculate necessary field size let fieldSize = _.max(_.map(taskDefinitions, (task) => { return task.to_string().length; } )); fieldSize += 5; // Some additional space between the argument and it's description // Group all commands by category const cat_commands = _.groupBy(taskDefinitions, (task) => { return task.type; }); // Display all commands, ordered by category _.each(categories, (cat) => { if (cat_commands[cat.key] !== undefined && cat_commands[cat.key].length > 0) { console.log(cat.description + ':') _.each(cat_commands[cat.key], (command) => { console.log(command.to_string(fieldSize) + command.description()); }) console.log('\n'); } }); });