bookgrabs
Version:
Interactive CLI tool for LibGen ebook searches and downloads with batch processing support
294 lines (247 loc) • 9.29 kB
JavaScript
import fs from 'fs';
import path from 'path';
import os from 'os';
// Get the BookGrabs directory in the user's home folder
export function getBookGrabsDirectory() {
return path.join(os.homedir(), 'BookGrabs');
}
// Ensure the BookGrabs directory exists
export async function ensureBookGrabsDirectory() {
const bookGrabsDir = getBookGrabsDirectory();
try {
await fs.promises.mkdir(bookGrabsDir, { recursive: true });
return bookGrabsDir;
} catch (error) {
console.error('Error creating BookGrabs directory:', error);
throw error;
}
}
// Detect if a title appears to be in English (basic heuristic)
export function isLikelyEnglish(title) {
// Common non-English patterns
const nonEnglishPatterns = [
/[àáâãäåæçèéêëìíîïñòóôõöøùúûüýÿ]/i, // Accented characters
/[αβγδεζηθικλμνξοπρστυφχψω]/i, // Greek
/[а-яё]/i, // Cyrillic
/[一-龯]/i, // Chinese/Japanese
/[ㄱ-ㅎ가-힣]/i, // Korean
/[ا-ي]/i, // Arabic
/[א-ת]/i, // Hebrew
/[ก-๙]/i, // Thai
/[ँ-ॿ]/i, // Devanagari (Hindi)
];
// Check for non-English patterns
for (const pattern of nonEnglishPatterns) {
if (pattern.test(title)) {
return false;
}
}
// French-specific patterns
if (title.includes("à l'") || title.includes("et le") || title.includes("et la") ||
title.includes("de la") || title.includes("du ") || title.includes("des ")) {
return false;
}
// German-specific patterns
if (title.includes("und der") || title.includes("und die") || title.includes("der ") ||
title.includes("die ") || title.includes("das ")) {
return false;
}
// Spanish-specific patterns
if (title.includes("y el") || title.includes("y la") || title.includes("de la") ||
title.includes("del ") || title.includes("los ") || title.includes("las ")) {
return false;
}
return true; // Likely English
}
// Filter results by file format preference (prioritize ebook formats)
export function filterByFormat(results) {
const ebookFormats = ['epub', 'pdf', 'mobi', 'azw3', 'azw', 'fb2', 'lit', 'pdb'];
const audioFormats = ['mp3', 'mp4', 'm4a', 'aac', 'flac', 'wav'];
const ebookResults = results.filter(result =>
result.ext && ebookFormats.includes(result.ext.toLowerCase())
);
const audioResults = results.filter(result =>
result.ext && audioFormats.includes(result.ext.toLowerCase())
);
const otherResults = results.filter(result =>
!result.ext || (!ebookFormats.includes(result.ext.toLowerCase()) && !audioFormats.includes(result.ext.toLowerCase()))
);
// Return ebooks first, then others, then audio last
return [...ebookResults, ...otherResults, ...audioResults];
}
// Filter results by language preference using actual language data
export function filterByLanguage(results, languagePreference) {
if (!languagePreference || languagePreference === 'any') {
return results;
}
const languageMap = {
'eng': 'English',
'fra': 'French',
'deu': 'German',
'spa': 'Spanish',
'ita': 'Italian',
'por': 'Portuguese',
'rus': 'Russian',
'chi': 'Chinese',
'jpn': 'Japanese'
};
const targetLanguage = languageMap[languagePreference] || languagePreference;
// Filter by exact language match first, then fallback to others
const exactMatches = results.filter(result =>
result.language && result.language.toLowerCase() === targetLanguage.toLowerCase()
);
const otherResults = results.filter(result =>
!result.language || result.language.toLowerCase() !== targetLanguage.toLowerCase()
);
// Return exact matches first, then others as fallback
return [...exactMatches, ...otherResults];
}
// Score results based on author and title matches
export function scoreResult(result, targetAuthor, targetTitle) {
let score = 0;
const resultAuthor = result.author.toLowerCase();
const resultTitle = result.title.toLowerCase();
// If no specific author/title targets, return neutral score
if (!targetAuthor && !targetTitle) {
return 1;
}
// Author matching with enhanced flexibility
if (targetAuthor) {
// Normalize author names for better matching
const normalizeAuthor = (author) => {
return author
.toLowerCase()
.replace(/[.,;]/g, '') // Remove punctuation
.replace(/\s+/g, ' ') // Normalize whitespace
.trim();
};
const normalizedTarget = normalizeAuthor(targetAuthor);
const normalizedResult = normalizeAuthor(resultAuthor);
// Direct match
if (normalizedResult.includes(normalizedTarget) || normalizedTarget.includes(normalizedResult)) {
score += 15; // Higher score for normalized match
}
// Handle "Last, First" vs "First Last" format
const targetParts = normalizedTarget.split(' ');
const resultParts = normalizedResult.split(' ');
// Check if result is in "Last, First" format and target is "First Last"
if (resultParts.length >= 2 && targetParts.length >= 2) {
const resultReversed = `${resultParts.slice(1).join(' ')} ${resultParts[0]}`;
const targetReversed = `${targetParts.slice(1).join(' ')} ${targetParts[0]}`;
if (resultReversed.includes(normalizedTarget) || targetReversed.includes(normalizedResult)) {
score += 15;
}
}
// Handle initials (e.g., "J.D." matches "JD" or "J D")
const expandInitials = (text) => {
return text.replace(/([a-z])\s*([a-z])/g, '$1 $2'); // "jd" -> "j d"
};
const targetExpanded = expandInitials(normalizedTarget);
const resultExpanded = expandInitials(normalizedResult);
if (targetExpanded !== normalizedTarget || resultExpanded !== normalizedResult) {
if (resultExpanded.includes(targetExpanded) || targetExpanded.includes(resultExpanded)) {
score += 12;
}
}
// Word-by-word matching for partial matches
for (const targetWord of targetParts) {
if (targetWord.length > 1) {
for (const resultWord of resultParts) {
if (resultWord.includes(targetWord) || targetWord.includes(resultWord)) {
score += 3;
}
}
}
}
}
// Title matching
if (targetTitle) {
if (resultTitle.includes(targetTitle) || targetTitle.includes(resultTitle)) {
score += 10; // High score for title match
}
// Partial title match
const titleWords = targetTitle.split(' ').filter(w => w.length > 2);
const resultTitleWords = resultTitle.split(' ');
for (const word of titleWords) {
if (resultTitleWords.some(rw => rw.includes(word) || word.includes(rw))) {
score += 3;
}
}
}
return score;
}
// Sanitize filename for safe file system usage
export function sanitizeFilename(filename) {
return filename.replace(/[^a-z0-9]/gi, '_').toLowerCase();
}
// Display results in a formatted way
export function displayResults(results, targetAuthor, targetTitle) {
console.log('Top 10 Results:');
results.forEach((res, index) => {
const languageInfo = res.language ? ` [${res.language}]` : '';
const formatInfo = res.ext ? ` [${res.ext.toUpperCase()}]` : '';
if (targetAuthor || targetTitle) {
console.log(`${index + 1}. ${res.author} - ${res.title} (${res.year})${languageInfo}${formatInfo} [Score: ${res.score}]`);
} else {
console.log(`${index + 1}. ${res.author} - ${res.title} (${res.year})${languageInfo}${formatInfo}`);
}
});
}
// Sleep utility function
export function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Environment configuration utilities
export function loadEnvConfig() {
try {
const envPath = '.env';
if (fs.existsSync(envPath)) {
const envContent = fs.readFileSync(envPath, 'utf-8');
const config = {};
envContent.split('\n').forEach(line => {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#')) {
const [key, ...valueParts] = trimmed.split('=');
if (key && valueParts.length > 0) {
config[key.trim()] = valueParts.join('=').trim();
}
}
});
return config;
}
} catch (error) {
console.error('Error loading .env file:', error);
}
return {};
}
export function saveEnvConfig(config) {
try {
const envPath = '.env';
const existingConfig = loadEnvConfig();
const mergedConfig = { ...existingConfig, ...config };
let envContent = '';
for (const [key, value] of Object.entries(mergedConfig)) {
envContent += `${key}=${value}\n`;
}
fs.writeFileSync(envPath, envContent);
// Update process.env to reflect the new values immediately
for (const [key, value] of Object.entries(config)) {
process.env[key] = value;
}
return true;
} catch (error) {
console.error('Error saving .env file:', error);
return false;
}
}
export function hasOpenAIKey() {
const config = loadEnvConfig();
return !!(config.OPEN_AI_KEY && config.OPEN_AI_KEY.trim());
}
export function getOpenAIKey() {
const config = loadEnvConfig();
return config.OPEN_AI_KEY || '';
}
export function setOpenAIKey(apiKey) {
return saveEnvConfig({ OPEN_AI_KEY: apiKey });
}