UNPKG

bookgrabs

Version:

Interactive CLI tool for LibGen ebook searches and downloads with batch processing support

294 lines (247 loc) 9.29 kB
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 }); }