mp3tag
Version:
A library for reading/writing mp3 tag data
283 lines (282 loc) • 10.8 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.run = exports.defineTask = exports.Task = exports.TaskType = void 0;
/** This module implements the logic to parse the given command line arguments
* into a task list.
*/
const _ = __importStar(require("lodash"));
const taskDefinitions = {};
function isTask(param) {
return taskDefinitions[param] !== undefined;
}
var TaskType;
(function (TaskType) {
TaskType["Source"] = "source";
TaskType["Sink"] = "sink";
TaskType["Read"] = "read";
TaskType["Write"] = "write";
TaskType["Option"] = "option";
TaskType["Help"] = "help"; // special category only for the help task
})(TaskType || (exports.TaskType = TaskType = {}));
;
class TaskDefinition {
key;
action;
options;
type;
/**
* @param key the task key, which is used to identify the task when parsing cmd args
* @param options the task's type and parameter options
* @param action the task function to execute
*/
constructor(key, options, action) {
this.key = key;
this.action = action;
this.options = TaskDefinition.verifyOptions(options);
this.type = this.options.type;
}
static verifyOptions(options) {
let min_args;
let max_args;
// First special case (no arguments at all)
if (options.min_args === undefined && options.max_args === undefined && options.args === undefined) {
min_args = 0;
max_args = 0;
}
else {
// Lower bound not defined
if (options.min_args === undefined) { // calc min_args
if (options.args !== undefined) {
min_args = options.args;
}
else {
min_args = 0; // only max_args or no args defined -> 0 args min
}
}
else {
min_args = options.min_args;
}
// Upper bound not defined
if (options.max_args === undefined) { // calc max_args
if (options.args !== undefined) {
max_args = options.args;
}
else {
max_args = 99; // min_args was defined so interpret as arbitrary number of arguments
}
}
else {
max_args = options.max_args;
}
}
// args is not part of the Options type
delete options.args;
return {
min_args,
max_args,
type: options.type ?? TaskType.Read, // default task type is read
...options // copy optional properties, which are set
};
}
/** 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 {
args = [];
constructor(definition) {
super(definition.key, definition.options, definition.action);
}
/** Runs a source task, which generates the tag data to pass to the next tasks
*
* @returns a promise, which resolves to the generated data of this source task
*/
runSource() {
return this.action();
}
/** Runs an option task, which receives no parameters and returns no data
*
* @returns a promise, which resolves once the option task has completed
*/
runOption() {
return this.action();
}
/** Executes this task with the current arguments
*
* @param data the data to pass to the task
*
* @return a promise, which resolves once the task has completed
*/
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 argv the argument list (excluding the task itself)
*/
readArgs(argv) {
// Read min_args amount of arguments
while (this.args.length < this.options.min_args) {
const 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 (argv.length > 0 && this.args.length < this.options.max_args && !isTask(argv[0])) {
this.args.push(argv.shift()); // argv cannot be empty
}
}
}
exports.Task = Task;
/** Define a task with the set of options and an action. The task is then added to the list
* of known tasks
*
* @param key the key, which identifies the task (eg. 'in' to use --in)
* @param options task's type and argument options
* @param 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
*/
function defineTask(key, options, action) {
taskDefinitions['--' + key] = new TaskDefinition(key, options, action);
}
exports.defineTask = defineTask;
;
/** Run method, which should be used to start the process
*
* @param argv the command line arguments passed to the script excluding the script call itself.
*
* @return {Promise<void>} resolves once, all tasks have completed
*/
async function run(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
}
// Special case... ignore all other tasks if help is specified
if (_.some(tasks, (task) => { return task.type === TaskType.Help; })) {
const helpTask = new Task(taskDefinitions['--help']);
await helpTask.runOption();
return;
}
// Ensure that we have exactly one source and at most one sink.
const catTasks = _.groupBy(tasks, task => task.type);
if (catTasks[TaskType.Source] === undefined || catTasks[TaskType.Source].length !== 1) {
throw new Error("Wrong arguments passed. A source is required.");
}
if (catTasks[TaskType.Sink] !== undefined && catTasks[TaskType.Sink].length > 1) {
throw new Error("Wrong arguments passed. At most one sink is allowed");
}
const sourceTask = catTasks[TaskType.Source][0];
const sinkTask = catTasks[TaskType.Sink] ? catTasks[TaskType.Sink][0] : undefined;
const optionTasks = catTasks[TaskType.Option] ?? [];
// Remove these special tasks form the general task list
_.remove(tasks, task => task.type === TaskType.Source || task.type === TaskType.Sink || task.type === TaskType.Option);
// Add the sink task back in as last task (must always be processed at the end)
if (sinkTask) {
tasks.push(sinkTask);
}
// Now define order of execution...
// first options, then source, then read or write, then sink
for (const optionTask of optionTasks) {
await optionTask.runOption();
}
// Load the tag data for the other tasks to manipulate
const tagData = await sourceTask.runSource();
// Now execute all remaining tasks in order
for (const task of tasks) {
// for all other tasks the current data is just passed in and the result ignored
await task.run(tagData);
}
// All tasks processed
}
exports.run = run;
const categories = [
{ key: TaskType.Help, description: 'Help' },
{ key: TaskType.Option, description: 'Options' },
{ key: TaskType.Source, description: 'File source commands' },
{ key: TaskType.Read, description: 'Reading commands' },
{ key: TaskType.Write, description: 'Modifying commands' },
{ key: TaskType.Sink, description: 'File target commands (save)' }
];
// Define help-task
defineTask('help', {
help_text: 'Display this help text',
type: TaskType.Help
}, async function () {
// Calculate necessary field size
let fieldSize = _.max(_.map(taskDefinitions, task => 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 => 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');
}
});
});