UNPKG

soundcloud-sync

Version:

Sync your SoundCloud likes to local files

177 lines (176 loc) 6.28 kB
#!/usr/bin/env node import process from 'node:process'; import soundCloudSync from "./index.js"; import logger from "./helpers/logger.js"; const config = { command: 'soundcloud-sync', options: [ { name: '--user', short: '-u', description: 'SoundCloud username to fetch likes from', required: true, value: true, }, { name: '--verify-timestamps', description: 'Verify and update timestamps of existing files to match like dates', }, { name: '--no-download', description: 'Skip downloading new tracks', }, { name: '--limit', short: '-l', description: 'Number of latest likes to fetch', value: true, validate: value => { const num = Number(value); if (Number.isNaN(num) || num <= 0) { return 'must be a positive number'; } return undefined; }, }, { name: '--folder', short: '-f', description: 'Output folder (default: ./music)', value: true, }, { name: '--help', short: '-h', description: 'Show this help message', }, ], examples: [ 'soundcloud-sync -u realies', 'soundcloud-sync --user realies --limit 100', 'soundcloud-sync -u realies -l 100 -f ./my-music', 'soundcloud-sync -u realies --verify-timestamps # verify timestamps of latest 50 likes', 'soundcloud-sync -u realies --verify-timestamps --no-download # only verify timestamps', 'soundcloud-sync -u realies --limit 100 --verify-timestamps --no-download # verify timestamps of latest 100 likes', ], }; /** * Parses and validates command-line arguments based on a configuration. */ class CliParser { /** * Creates a new CLI parser with the given configuration. * * @param cliConfig - The CLI configuration defining options and examples */ constructor(cliConfig) { this.config = cliConfig; this.optionMap = new Map(); // Build lookup maps for both long and short options config.options.forEach(opt => { this.optionMap.set(opt.name, opt); if (opt.short) { this.optionMap.set(opt.short, opt); } }); } /** * Parses command-line arguments into a key-value map. * * @param args - Raw command-line arguments to parse * @returns Parsed arguments mapped by option name * @throws Error if validation fails or required options are missing */ parse(args) { const parsed = {}; for (let i = 0; i < args.length; i += 1) { const arg = args[i]; const option = this.optionMap.get(arg); if (!option) { throw new Error(`Unknown argument: ${arg}`); } if (option.value) { if (i + 1 >= args.length || this.optionMap.has(args[i + 1])) { throw new Error(`${option.name} requires a value`); } const value = args[i + 1]; i += 1; if (option.validate) { const error = option.validate(value); if (error) { throw new Error(`${option.name} ${error}`); } } parsed[option.name] = value; } else { parsed[option.name] = true; } } // Check required options this.config.options.forEach(opt => { if (opt.required && !parsed[opt.name]) { throw new Error(`${opt.name} is required`); } }); return parsed; } /** * Formats the help message with options and examples. * * @returns Formatted help text with ANSI color codes */ formatHelp() { // Calculate the maximum length of the flags section for alignment const maxFlagsLength = this.config.options.reduce((max, opt) => { const flags = [opt.name, opt.short].filter(Boolean).join(', '); const valueHint = opt.value ? ' <value>' : ''; const required = opt.required ? ' (required)' : ''; return Math.max(max, (flags + valueHint + required).length); }, 0); const lines = ['Usage:', ` ${this.config.command} [options]`, '', 'Options:']; this.config.options.forEach(opt => { const flags = [opt.name, opt.short].filter(Boolean).join(', '); const valueHint = opt.value ? ' <value>' : ''; const required = opt.required ? ' (required)' : ''; const flagsPart = flags + valueHint + required; const padding = ' '.repeat(maxFlagsLength - flagsPart.length + 4); lines.push(` \x1b[36m${flagsPart}\x1b[0m${padding}${opt.description}`); }); if (this.config.examples.length > 0) { lines.push(''); lines.push('Examples:'); this.config.examples.forEach(example => { lines.push(` ${example}`); }); } return lines.join('\n'); } } // Main CLI execution (async () => { try { const cli = new CliParser(config); const args = process.argv.slice(2); if (args.length === 0) { logger.error(cli.formatHelp()); process.exit(1); } const parsed = cli.parse(args); if (parsed['--help']) { logger.error(cli.formatHelp()); process.exit(0); } await soundCloudSync({ username: parsed['--user'], folder: parsed['--folder'], limit: parsed['--limit'] ? Number(parsed['--limit']) : undefined, verifyTimestamps: parsed['--verify-timestamps'] === true, noDownload: parsed['--no-download'] === true, }); } catch (error) { logger.error('Error:', { error: error instanceof Error ? error.message : String(error) }); process.exit(1); } })();