@tiahui/anitorrent-cli
Version:
CLI tool for video management with PeerTube and Storj S3
299 lines (254 loc) • 11.1 kB
JavaScript
const { Command } = require('commander');
const ora = require('ora');
const { Logger } = require('../utils/logger');
const Validators = require('../utils/validators');
const AudioService = require('../services/audio-service');
const audioCommand = new Command('audio');
audioCommand.description('Audio track management and extraction');
audioCommand
.command('list')
.description('List audio tracks from a video file')
.argument('<file>', 'video file path')
.action(async (file, options) => {
const isLogs = audioCommand.parent?.opts()?.logs || false;
const logger = new Logger({
verbose: false,
quiet: audioCommand.parent?.opts()?.quiet || false
});
try {
const pathValidation = await Validators.validateFilePath(file);
const videoFile = pathValidation.resolvedPath;
const fs = require('fs').promises;
try {
const stats = await fs.stat(videoFile);
if (!stats.isFile()) {
logger.error(`Path "${file}" is not a file`);
process.exit(1);
}
} catch (error) {
logger.error(`File not found: "${file}"`);
if (pathValidation.originalPath !== pathValidation.resolvedPath) {
logger.error(`Resolved path: "${pathValidation.resolvedPath}"`);
}
process.exit(1);
}
const audioService = new AudioService();
logger.header('Audio Track Information');
logger.info(`File: ${videoFile}`);
logger.separator();
const spinner = ora('Analyzing audio tracks...').start();
const audioTracks = await audioService.listAudioTracks(videoFile);
spinner.succeed(`Found ${audioTracks.length} audio tracks`);
if (audioTracks.length === 0) {
logger.warning('No audio tracks found in the video file');
return;
}
audioTracks.forEach((track, index) => {
logger.info(`Track ${track.trackNumber}:`);
logger.info(` Language: ${track.language}${track.languageDetail ? ` (${track.languageDetail})` : ''}`, 1);
logger.info(` Title: ${track.title}`, 1);
logger.info(` Codec: ${track.codec}`, 1);
logger.info(` Channels: ${track.channels}`, 1);
logger.info(` Sample Rate: ${track.sampleRate} Hz`, 1);
if (track.bitrate) {
logger.info(` Bitrate: ${Math.round(track.bitrate / 1000)} kbps`, 1);
}
if (isLogs) {
logger.info(` Stream Index: ${track.index}`, 1);
if (track.allTags) {
logger.info(` All Tags:`, 1);
Object.entries(track.allTags).forEach(([key, value]) => {
logger.info(` ${key}: ${value}`, 1);
});
}
if (track.disposition) {
const dispositionFlags = Object.entries(track.disposition)
.filter(([key, value]) => value === 1)
.map(([key]) => key);
if (dispositionFlags.length > 0) {
logger.info(` Disposition: ${dispositionFlags.join(', ')}`, 1);
}
}
}
if (index < audioTracks.length - 1) {
logger.separator();
}
});
} catch (error) {
logger.error(`Failed to list audio tracks: ${error.message}`);
process.exit(1);
}
});
audioCommand
.command('extract')
.description('Extract audio tracks from videos with Japanese default')
.argument('[file]', 'video file path (if not provided, extracts from all videos in folder)')
.option('--folder <path>', 'folder path to search for videos (default: current directory)')
.option('--track <number>', 'audio track number to extract')
.option('--format <format>', 'output audio format (mp3, aac, flac, wav, ogg)', 'mp3')
.option('--bitrate <bitrate>', 'audio bitrate (e.g., 192k, 256k, 320k)', '192k')
.option('--all', 'extract all audio tracks from the file')
.option('--prefix <prefix>', 'custom prefix for output files (default: video filename)')
.action(async (file, options) => {
const isLogs = audioCommand.parent?.opts()?.logs || false;
const logger = new Logger({
verbose: false,
quiet: audioCommand.parent?.opts()?.quiet || false
});
try {
let audioTrack = null;
if (options.track !== undefined) {
audioTrack = parseInt(options.track);
if (!Validators.isValidSubtitleTrack(audioTrack)) {
logger.error('Invalid audio track number');
process.exit(1);
}
}
const format = options.format.toLowerCase();
const bitrate = options.bitrate;
const validFormats = ['mp3', 'aac', 'flac', 'wav', 'ogg'];
if (!validFormats.includes(format)) {
logger.error(`Invalid format. Supported formats: ${validFormats.join(', ')}`);
process.exit(1);
}
const bitrateRegex = /^\d+k?$/i;
if (!bitrateRegex.test(bitrate)) {
logger.error('Invalid bitrate format. Use format like: 192k, 256k, 320k');
process.exit(1);
}
const audioService = new AudioService();
let folderPath = '.';
if (options.folder) {
const pathValidation = await Validators.validateFilePath(options.folder);
folderPath = pathValidation.resolvedPath;
const fs = require('fs').promises;
try {
const stats = await fs.stat(folderPath);
if (!stats.isDirectory()) {
logger.error(`Path "${options.folder}" is not a directory`);
process.exit(1);
}
} catch (error) {
logger.error(`Directory not found: "${options.folder}"`);
if (pathValidation.originalPath !== pathValidation.resolvedPath) {
logger.error(`Resolved path: "${pathValidation.resolvedPath}"`);
}
process.exit(1);
}
}
if (file) {
const pathValidation = await Validators.validateFilePath(file);
const videoFile = pathValidation.resolvedPath;
const fs = require('fs').promises;
try {
const stats = await fs.stat(videoFile);
if (!stats.isFile()) {
logger.error(`Path "${file}" is not a file`);
process.exit(1);
}
} catch (error) {
logger.error(`File not found: "${file}"`);
if (pathValidation.originalPath !== pathValidation.resolvedPath) {
logger.error(`Resolved path: "${pathValidation.resolvedPath}"`);
}
process.exit(1);
}
if (options.all) {
logger.header('Extract All Audio Tracks with Japanese Default');
logger.info(`File: ${videoFile}`);
logger.info(`Format: ${format.toUpperCase()}`);
logger.separator();
const spinner = ora('Extracting all audio tracks...').start();
const results = await audioService.extractAllAudioTracks(videoFile, folderPath, format, options.prefix);
spinner.succeed(`Extraction completed`);
const successful = results.filter(r => r.success).length;
const failed = results.filter(r => !r.success).length;
logger.success(`Extraction completed: ${successful} successful, ${failed} failed`);
if (isLogs) {
results.forEach(result => {
if (result.success) {
logger.info(`✓ Track ${result.track.trackNumber} (${result.track.language}) → ${result.outputFile}`, 1);
} else {
logger.info(`✗ Track ${result.track.trackNumber}: ${result.error}`, 1);
}
});
}
} else {
logger.header('Single Audio Track Extraction');
logger.info(`File: ${videoFile}`);
if (audioTrack !== null) {
logger.info(`Track: ${audioTrack}`);
} else {
logger.info('Track: Japanese (Default)');
}
logger.info(`Format: ${format.toUpperCase()}`);
logger.info(`Bitrate: ${bitrate}`);
logger.separator();
let targetTrack = audioTrack;
if (targetTrack === null) {
const tracks = await audioService.listAudioTracks(videoFile);
targetTrack = tracks.findIndex(t => t.language === 'jpn' || t.language === 'ja');
if (targetTrack === -1) {
targetTrack = 0;
}
logger.info(`Auto-detected track: ${targetTrack}`);
}
const tracks = await audioService.listAudioTracks(videoFile);
const nameWithoutExt = options.prefix || require('path').parse(videoFile).name;
let outputFile;
if (targetTrack < tracks.length) {
const track = tracks[targetTrack];
const langSuffix = audioService.getLanguageSuffix(track);
outputFile = langSuffix ? `${nameWithoutExt}_${langSuffix}.${format}` : `${nameWithoutExt}.${format}`;
} else {
outputFile = `${nameWithoutExt}.${format}`;
}
const spinner = ora('Extracting audio track...').start();
const result = await audioService.extractAudio(videoFile, outputFile, targetTrack, folderPath, format, bitrate);
if (result.success) {
spinner.succeed(`Audio extracted to: ${result.outputPath}`);
} else {
spinner.fail(`Extraction failed: ${result.error}`);
process.exit(1);
}
}
} else {
logger.header('Bulk Audio Extraction with Japanese Default');
logger.info(`Directory: ${folderPath === '.' ? 'Current directory' : folderPath}`);
if (audioTrack !== null) {
logger.info(`Audio track: ${audioTrack}`);
} else {
logger.info('Audio track: Japanese (Default)');
}
logger.info(`Format: ${format.toUpperCase()}`);
logger.info(`Bitrate: ${bitrate}`);
logger.separator();
const spinner = ora('Finding local video files...').start();
const localFiles = await audioService.getLocalVideoFiles(folderPath);
spinner.succeed(`Found ${localFiles.length} local video files`);
if (localFiles.length === 0) {
logger.warning(`No video files found in directory: ${folderPath}`);
return;
}
logger.info('Extracting audio...');
const results = await audioService.extractAllAudio(audioTrack, folderPath, format);
const successful = results.filter(r => r.success).length;
const failed = results.filter(r => !r.success).length;
logger.separator();
logger.success(`Extraction completed: ${successful} successful, ${failed} failed`);
if (isLogs) {
results.forEach(result => {
if (result.success) {
logger.info(`✓ ${result.filename} → ${result.outputFile}`, 1);
} else {
logger.info(`✗ ${result.filename}: ${result.error}`, 1);
}
});
}
}
} catch (error) {
logger.error(`Audio extraction failed: ${error.message}`);
process.exit(1);
}
});
module.exports = audioCommand;