@tiahui/anitorrent-cli
Version:
CLI tool for video management with PeerTube and Storj S3
572 lines (492 loc) • 20.6 kB
JavaScript
const { Command } = require('commander');
const inquirer = require('inquirer');
const chalk = require('chalk');
const ora = require('ora');
const ConfigManager = require('../utils/config');
const { logger } = require('../utils/logger');
const Validators = require('../utils/validators');
const PeerTubeService = require('../services/peertube-service');
const SystemCheck = require('../utils/system-check');
const configCommand = new Command('config');
configCommand.description('Configuration management');
configCommand
.command('init')
.description('Create configuration template file')
.action(async () => {
try {
const config = new ConfigManager();
if (await config.exists()) {
const { overwrite } = await inquirer.prompt([{
type: 'confirm',
name: 'overwrite',
message: 'Configuration already exists. Overwrite?',
default: false
}]);
if (!overwrite) {
logger.info('Operation cancelled');
return;
}
}
await config.createTemplate();
logger.success('Configuration template created successfully');
logger.info(`Configuration saved to: ${config.getConfigPath()}`);
logger.info('Please run "anitorrent config setup" to configure your settings');
} catch (error) {
logger.error(`Failed to create configuration template: ${error.message}`);
process.exit(1);
}
});
configCommand
.command('setup')
.description('Interactive configuration setup')
.action(async () => {
try {
logger.header('AniTorrent CLI - Interactive Configuration');
const config = new ConfigManager();
const existingConfig = await config.exists();
if (existingConfig) {
logger.info('Existing configuration found. Checking for missing values...');
logger.separator();
}
const needsValue = (key, defaultValues = []) => {
const currentValue = config.get(key);
if (!currentValue) return true;
if (defaultValues.includes(currentValue)) return true;
return false;
};
const r2Questions = [
{
type: 'input',
name: 'R2_ACCESS_KEY_ID',
message: 'R2 Access Key ID (required):',
validate: input => input.trim() !== '' || 'Access Key ID is required',
when: () => needsValue('R2_ACCESS_KEY_ID', ['your_access_key_id'])
},
{
type: 'input',
name: 'R2_SECRET_ACCESS_KEY',
message: 'R2 Secret Access Key (required):',
validate: input => input.trim() !== '' || 'Secret Access Key is required',
when: () => needsValue('R2_SECRET_ACCESS_KEY', ['your_secret_access_key'])
},
{
type: 'input',
name: 'R2_ENDPOINT',
message: 'R2 Endpoint URL (required):',
validate: input => {
if (!input.trim()) return 'Endpoint URL is required';
if (!Validators.isValidUrl(input)) return 'Invalid URL format';
return true;
},
when: () => needsValue('R2_ENDPOINT', ['https://your-account-id.r2.cloudflarestorage.com'])
},
{
type: 'input',
name: 'R2_BUCKET_NAME',
message: 'R2 Bucket Name (required):',
validate: input => input.trim() !== '' || 'Bucket name is required',
when: () => needsValue('R2_BUCKET_NAME', ['your_bucket_name'])
},
{
type: 'input',
name: 'R2_PUBLIC_DOMAIN',
message: 'R2 Public Domain (optional):',
default: 'https://cdn.anitorrent.com',
validate: input => {
if (!input.trim()) return true;
return Validators.isValidUrl(input) || 'Invalid URL format';
},
when: () => needsValue('R2_PUBLIC_DOMAIN') && !config.get('R2_PUBLIC_DOMAIN')
}
];
const r2Answers = await inquirer.prompt(r2Questions);
const peertubeBaseQuestions = [
{
type: 'input',
name: 'PEERTUBE_API_URL',
message: 'PeerTube API URL (optional):',
default: 'https://peertube.anitorrent.com/api/v1',
validate: input => {
if (!input.trim()) return true;
return Validators.isValidUrl(input) || 'Invalid URL format';
},
when: () => needsValue('PEERTUBE_API_URL') && !config.get('PEERTUBE_API_URL')
}
];
const peertubeBaseAnswers = await inquirer.prompt(peertubeBaseQuestions);
Object.entries({...r2Answers, ...peertubeBaseAnswers}).forEach(([key, value]) => {
config.set(key, value);
});
let peertubeCredentials = {};
let userInfo;
let validCredentials = false;
let needsPeertubeCredentials = needsValue('PEERTUBE_USERNAME', ['your_username']) || needsValue('PEERTUBE_PASSWORD', ['your_password']);
if (needsPeertubeCredentials) {
while (!validCredentials) {
const credentialQuestions = [
{
type: 'input',
name: 'PEERTUBE_USERNAME',
message: 'PeerTube Username (required):',
validate: input => input.trim() !== '' || 'Username is required',
when: () => needsValue('PEERTUBE_USERNAME', ['your_username']),
default: () => config.get('PEERTUBE_USERNAME') !== 'your_username' ? config.get('PEERTUBE_USERNAME') : undefined
},
{
type: 'input',
name: 'PEERTUBE_PASSWORD',
message: 'PeerTube Password (required):',
validate: input => input.trim() !== '' || 'Password is required',
when: () => needsValue('PEERTUBE_PASSWORD', ['your_password'])
}
];
peertubeCredentials = await inquirer.prompt(credentialQuestions);
const spinner = ora('Validating PeerTube credentials...').start();
try {
Object.entries(peertubeCredentials).forEach(([key, value]) => {
config.set(key, value);
});
const peertubeService = new PeerTubeService(config.getPeerTubeConfig());
userInfo = await peertubeService.getCurrentUser();
validCredentials = true;
spinner.succeed('Credentials validated successfully');
logger.info(`Welcome ${userInfo.username}! (${userInfo.email})`);
} catch (error) {
spinner.fail('Invalid credentials');
logger.error('Username or password is incorrect. Please try again.');
validCredentials = false;
}
}
} else {
try {
const peertubeService = new PeerTubeService(config.getPeerTubeConfig());
userInfo = await peertubeService.getCurrentUser();
logger.info(`Using existing credentials for ${userInfo.username}! (${userInfo.email})`);
} catch (error) {
logger.error('Existing PeerTube credentials are invalid. Please reconfigure.');
process.exit(1);
}
}
let channelAnswer = {};
if (needsValue('DEFAULT_CHANNEL_ID', [''])) {
if (userInfo.videoChannels && userInfo.videoChannels.length > 0) {
const channelChoices = userInfo.videoChannels.map(channel => ({
name: `${channel.displayName} (ID: ${channel.id})`,
value: channel.id.toString()
}));
channelAnswer = await inquirer.prompt([
{
type: 'list',
name: 'DEFAULT_CHANNEL_ID',
message: 'Select default channel:',
choices: channelChoices,
default: channelChoices[0].value
}
]);
} else {
logger.warning('No channels found for this user');
channelAnswer = { DEFAULT_CHANNEL_ID: '' };
}
} else {
logger.info(`Using existing channel ID: ${config.get('DEFAULT_CHANNEL_ID')}`);
}
const apiKeysQuestions = [
{
type: 'input',
name: 'CLAUDE_API_KEY',
message: 'Claude API Key (for AI translation, optional):',
validate: input => {
if (!input.trim()) return true;
return input.trim().length > 10 || 'API key seems too short';
},
when: () => needsValue('CLAUDE_API_KEY', ['your_claude_api_key'])
},
{
type: 'input',
name: 'ANITORRENT_API_KEY',
message: 'AniTorrent API Key (for api.anitorrent.com, optional):',
validate: input => {
if (!input.trim()) return true;
return input.trim().length > 10 || 'API key seems too short';
},
when: () => needsValue('ANITORRENT_API_KEY', ['your_anitorrent_api_key'])
}
];
const apiKeysAnswers = await inquirer.prompt(apiKeysQuestions);
const finalQuestions = [
{
type: 'list',
name: 'DEFAULT_PRIVACY_LEVEL',
message: 'Default Privacy Level:',
choices: [
{ name: '1 - Public', value: '1' },
{ name: '2 - Unlisted', value: '2' },
{ name: '3 - Private', value: '3' },
{ name: '4 - Internal', value: '4' },
{ name: '5 - Password Protected', value: '5' }
],
default: () => config.get('DEFAULT_PRIVACY_LEVEL') || '5',
when: () => needsValue('DEFAULT_PRIVACY_LEVEL', ['5']) && !config.get('DEFAULT_PRIVACY_LEVEL')
},
{
type: 'input',
name: 'DEFAULT_VIDEO_PASSWORD',
message: 'Default Video Password (optional):',
default: () => config.get('DEFAULT_VIDEO_PASSWORD') || '12345',
when: () => needsValue('DEFAULT_VIDEO_PASSWORD', ['12345', 'AniTorrent108']) && !config.get('DEFAULT_VIDEO_PASSWORD')
}
];
const finalAnswers = await inquirer.prompt(finalQuestions);
Object.entries({...channelAnswer, ...apiKeysAnswers, ...finalAnswers}).forEach(([key, value]) => {
config.set(key, value);
});
await config.saveConfig();
const allAnswers = {...r2Answers, ...peertubeBaseAnswers, ...peertubeCredentials, ...channelAnswer, ...apiKeysAnswers, ...finalAnswers};
const configuredKeys = Object.keys(allAnswers);
logger.success('Configuration saved successfully');
logger.info(`Configuration saved to: ${config.getConfigPath()}`);
if (configuredKeys.length > 0) {
logger.info(`Updated configurations: ${configuredKeys.join(', ')}`);
} else {
logger.info('All configurations were already set - no changes needed');
}
logger.info('Configuration completed successfully!');
} catch (error) {
logger.error(`Setup failed: ${error.message}`);
process.exit(1);
}
});
configCommand
.command('check')
.description('Verify current configuration')
.action(async () => {
try {
const config = new ConfigManager();
if (!(await config.exists())) {
logger.error('Configuration not found');
logger.info('Run "anitorrent config init" or "anitorrent config setup" first');
process.exit(1);
}
logger.info('Checking configuration...');
try {
config.validateRequired();
logger.success('All required configuration variables are set');
} catch (error) {
logger.error(error.message);
process.exit(1);
}
try {
const r2Config = config.getR2Config();
Validators.validateR2Config(r2Config);
logger.success('R2 configuration is valid');
} catch (error) {
logger.error(`R2 configuration error: ${error.message}`);
process.exit(1);
}
try {
const peertubeConfig = config.getPeerTubeConfig();
Validators.validatePeerTubeConfig(peertubeConfig);
logger.success('PeerTube configuration is valid');
} catch (error) {
logger.error(`PeerTube configuration error: ${error.message}`);
process.exit(1);
}
logger.success('Configuration check passed!');
} catch (error) {
logger.error(`Configuration check failed: ${error.message}`);
process.exit(1);
}
});
configCommand
.command('show')
.description('Show current configuration (hides sensitive values)')
.action(async () => {
try {
const config = new ConfigManager();
if (!(await config.exists())) {
logger.error('Configuration not found');
logger.info('Run "anitorrent config init" or "anitorrent config setup" first');
process.exit(1);
}
logger.info('Current Configuration:');
logger.separator();
logger.info(`Config file: ${config.getConfigPath()}`);
logger.separator();
const configData = config.showConfig(true);
Object.entries(configData)
.filter(([key]) => key.startsWith('R2_') || key.startsWith('PEERTUBE_') || key.startsWith('DEFAULT_') || key.startsWith('CLAUDE_') || key.startsWith('ANITORRENT_'))
.forEach(([key, value]) => {
logger.info(`${key}: ${value}`, 1);
});
} catch (error) {
logger.error(`Failed to show configuration: ${error.message}`);
process.exit(1);
}
});
configCommand
.command('test')
.description('Test connections to services')
.action(async () => {
try {
const config = new ConfigManager();
config.validateRequired();
logger.header('Testing Service Connections');
const systemCheck = new SystemCheck();
const depsSpinner = ora('Checking system dependencies...').start();
try {
const dependencies = await systemCheck.checkAllDependencies();
if (dependencies.allAvailable) {
depsSpinner.succeed('All system dependencies are available');
} else {
depsSpinner.warn('Some system dependencies are missing');
const report = systemCheck.generateInstallationReport(dependencies);
for (const item of report) {
logger.warning(`${item.tool} is not fully installed`);
if (item.missing.length > 0) {
logger.info(`Missing commands: ${item.missing.join(', ')}`, 1);
}
logger.info(`Install command: ${item.installCommand}`, 1);
logger.separator();
}
}
} catch (error) {
depsSpinner.fail(`Dependency check failed: ${error.message}`);
}
const spinner = ora('Testing R2 connection...').start();
try {
const S3Service = require('../services/s3-service');
const s3Service = new S3Service(config.getR2Config());
spinner.succeed('R2 connection successful');
} catch (error) {
spinner.fail(`R2 connection failed: ${error.message}`);
}
const peertubeSpinner = ora('Testing PeerTube connection...').start();
try {
const peertubeService = new PeerTubeService(config.getPeerTubeConfig());
await peertubeService.getValidAccessToken();
peertubeSpinner.succeed('PeerTube connection successful');
} catch (error) {
peertubeSpinner.fail(`PeerTube connection failed: ${error.message}`);
}
const translationConfig = config.getTranslationConfig();
if (translationConfig.apiKey && translationConfig.apiKey !== 'your_claude_api_key') {
const claudeSpinner = ora('Testing Claude API connection...').start();
try {
const TranslationService = require('../services/translation-service');
const translationService = new TranslationService(translationConfig);
claudeSpinner.succeed('Claude API key configured');
} catch (error) {
claudeSpinner.fail(`Claude API test failed: ${error.message}`);
}
} else {
logger.info('Claude API key not configured (translation features disabled)');
}
const anitorrentConfig = config.getAniTorrentConfig();
if (anitorrentConfig.apiKey && anitorrentConfig.apiKey !== 'your_anitorrent_api_key') {
const anitorrentSpinner = ora('Testing AniTorrent API connection...').start();
try {
const AniTorrentService = require('../services/anitorrent-service');
const anitorrentService = new AniTorrentService(config);
const testResult = await anitorrentService.testConnection();
if (testResult.success) {
anitorrentSpinner.succeed('AniTorrent API connection successful');
} else {
anitorrentSpinner.fail(`AniTorrent API connection failed: ${testResult.message}`);
}
} catch (error) {
anitorrentSpinner.fail(`AniTorrent API test failed: ${error.message}`);
}
} else {
logger.info('AniTorrent API key not configured');
}
} catch (error) {
logger.error(`Connection test failed: ${error.message}`);
process.exit(1);
}
});
configCommand
.command('reset')
.description('Reset configuration')
.action(async () => {
try {
const config = new ConfigManager();
const { confirm } = await inquirer.prompt([{
type: 'confirm',
name: 'confirm',
message: 'Are you sure you want to reset the configuration? This will delete all saved settings.',
default: false
}]);
if (!confirm) {
logger.info('Operation cancelled');
return;
}
const fs = require('fs').promises;
try {
await fs.unlink(config.getConfigPath());
try {
await fs.unlink(config.tokenFile);
} catch (error) {
// Token file might not exist, ignore
}
logger.success('Configuration reset successfully');
} catch (error) {
if (error.code === 'ENOENT') {
logger.info('No configuration found to reset');
} else {
throw error;
}
}
} catch (error) {
logger.error(`Reset failed: ${error.message}`);
process.exit(1);
}
});
configCommand
.command('system-check')
.description('Check system dependencies and compatibility')
.action(async () => {
try {
const SystemCheck = require('../utils/system-check');
const systemCheck = new SystemCheck();
logger.header('System Compatibility Check');
logger.info(`Platform: ${systemCheck.platform}`);
if (systemCheck.isLinux) {
const distro = systemCheck.getDistribution();
logger.info(`Distribution: ${distro}`);
}
logger.separator();
const spinner = ora('Checking system dependencies...').start();
try {
const dependencies = await systemCheck.checkAllDependencies();
if (dependencies.allAvailable) {
spinner.succeed('All system dependencies are available');
logger.success('Your system is ready to use AniTorrent CLI!');
} else {
spinner.warn('Some system dependencies are missing');
const report = systemCheck.generateInstallationReport(dependencies);
logger.warning('Missing Dependencies:');
logger.separator();
for (const item of report) {
logger.error(`❌ ${item.tool}`);
if (item.missing.length > 0) {
logger.info(` Missing: ${item.missing.join(', ')}`, 1);
}
logger.info(` Install: ${item.installCommand}`, 1);
logger.separator();
}
logger.info('Please install the missing dependencies and run this command again.');
}
logger.separator();
logger.info('Detailed dependency status:');
logger.info(`FFmpeg: ${dependencies.ffmpeg.ffmpeg ? '✅' : '❌'} ffmpeg, ${dependencies.ffmpeg.ffprobe ? '✅' : '❌'} ffprobe`);
logger.info(`MKVToolNix: ${dependencies.mkvtoolnix.mkvmerge ? '✅' : '❌'} mkvmerge, ${dependencies.mkvtoolnix.mkvextract ? '✅' : '❌'} mkvextract`);
} catch (error) {
spinner.fail(`Dependency check failed: ${error.message}`);
logger.error('Unable to check system dependencies. Please ensure you have proper permissions.');
}
} catch (error) {
logger.error(`System check failed: ${error.message}`);
process.exit(1);
}
});
module.exports = configCommand;