bookgrabs
Version:
Interactive CLI tool for LibGen ebook searches and downloads with batch processing support
131 lines (112 loc) • 4.37 kB
JavaScript
import fs from 'fs';
import { parseArgs, showUsage } from './src/config.js';
import { searchLibGen } from './src/search.js';
import { downloadBook, promptUser } from './src/download.js';
import { standardizeAuthorTitle } from './src/ai.js';
import { parseCSV, processBatchBooks, saveReportAndSummary } from './src/csv.js';
import { scoreResult, displayResults } from './src/utils.js';
// Global abort controller for handling cancellation
let globalAbortController = null;
// Signal handler for graceful exit
function handleSignal(signal) {
console.log(`\n\nReceived ${signal}. Cancelling operations and exiting...`);
if (globalAbortController) {
globalAbortController.abort();
}
// Give some time for cleanup
setTimeout(() => {
console.log('Exiting gracefully.');
process.exit(0);
}, 1000);
}
// Set up signal handlers
process.on('SIGINT', () => handleSignal('SIGINT'));
process.on('SIGTERM', () => handleSignal('SIGTERM'));
process.on('SIGQUIT', () => handleSignal('SIGQUIT'));
// Parse command line arguments
const { author: targetAuthor, title: targetTitle, query, csvFile, searchOptions } = parseArgs();
// Show usage if no query or CSV file provided
if (!query && !csvFile) {
showUsage();
process.exit(1);
}
// Main execution function
async function main() {
// Create global abort controller
globalAbortController = new AbortController();
if (csvFile) {
// Batch processing mode
try {
const csvContent = await fs.promises.readFile(csvFile, 'utf8');
const books = parseCSV(csvContent);
// Create log file for command line batch processing
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const logFileName = `batch_download_log_${timestamp}.txt`;
const { report, downloadedBooks, skippedCount } = await processBatchBooks(books, searchOptions, logFileName);
await saveReportAndSummary(report, downloadedBooks, logFileName);
} catch (error) {
if (error.name === 'AbortError' || error.message.includes('cancelled')) {
console.log('\nBatch processing cancelled by user.');
process.exit(0);
} else {
console.error(`Error reading CSV file "${csvFile}":`, error.message);
process.exit(1);
}
}
} else {
// Interactive mode
try {
const results = await searchLibGen(query, searchOptions, globalAbortController.signal);
if (results.length === 0) {
console.log('No results found.');
return;
}
// Score and sort results for interactive mode
const scoredResults = results.map(result => {
result.score = scoreResult(result, targetAuthor, targetTitle);
return result;
});
scoredResults.sort((a, b) => {
if (b.score !== a.score) {
return b.score - a.score;
}
return parseInt(b.year) - parseInt(a.year);
});
const topResults = scoredResults.slice(0, 10);
displayResults(topResults, targetAuthor, targetTitle);
const selectedBook = await promptUser(topResults);
if (selectedBook) {
// Standardize author and title using GPT
console.log('\nStandardizing author and title...');
const standardized = await standardizeAuthorTitle(selectedBook.author, selectedBook.title);
console.log(`Standardized: ${standardized.author} - ${standardized.title}`);
// Update the selected book with standardized values
const standardizedBook = {
...selectedBook,
author: standardized.author,
title: standardized.title
};
// Pass all scored results for potential retries
await downloadBook(standardizedBook, scoredResults, globalAbortController.signal);
}
} catch (error) {
if (error.name === 'AbortError' || error.message.includes('cancelled')) {
console.log('\nSearch cancelled by user.');
process.exit(0);
} else {
console.error('Unexpected error:', error);
process.exit(1);
}
}
}
}
// Run the main function
main().catch(error => {
if (error.name === 'AbortError' || error.message.includes('cancelled')) {
console.log('\nOperation cancelled by user.');
process.exit(0);
} else {
console.error('Unexpected error:', error);
process.exit(1);
}
});