// utils.ts import path from 'path'; import { v4 as uuidv4 } from 'uuid'; // Import UUID generator export function getBaseNameWithoutExtension(filePath: string): string { const baseName = path.basename(filePath); return path.parse(baseName).name; } export function generateUuid(): string { return uuidv4(); } export function isValidDate(dateString: string | undefined): boolean { if (!dateString) { return false; // Treat undefined as invalid date } // Check for common default date patterns (you might need to adjust these based on your OPF files) if (dateString.startsWith('0001-01-01') || dateString.startsWith('0101-01-01') || dateString === '0000-00-00' ) { return false; // It's a default/placeholder date } // You could add more sophisticated date validation here if needed return true; // Otherwise, assume it's a valid date } // Helper function to strip namespace prefixes during XML parsing with xml2js export function stripNamespacePrefix(name: string): string { const parts = name.split(':'); return parts.length > 1 ? parts[parts.length - 1] : name; } // logging.ts const LOG_PREFIX = '[MD-GEN]'; // --- Logging Categories (for filtering logs if needed) --- export enum LogCategory { START = 'START', END = 'END', INFO = 'INFO', WARNING = 'WARNING', ERROR = 'ERROR', OPF = 'OPF', API = 'API', API_OPENLIBRARY = 'API-OPENLIBRARY', // Specific category for OpenLibrary API messages API_GOOGLEBOOKS = 'API-GOOGLEBOOKS', // Specific category for GoogleBooks API messages <-- MAKE SURE THIS LINE IS EXACTLY AS SHOWN } export function logMessage(category: LogCategory, message: string, newLine: boolean = false, ...args: any[]): void { const categoryPrefix = `${LOG_PREFIX} [${category}]`; // Consistent category prefix const fullMessage = `${categoryPrefix} ${message} ${args.length > 0 ? args.join(' ') : ''}`; // Include category in log if (newLine) { console.log(fullMessage + '\n'); // Add extra newline if needed } else { console.log(fullMessage); } } // api_open_library.ts import { logMessage, LogCategory } from './logging'; const OPEN_LIBRARY_API_URL = 'https://openlibrary.org/isbn/'; // Base URL for Open Library ISBN API export async function fetchBookByISBN_OpenLibrary(isbn: string): Promise { // Promise initially, we'll refine later logMessage(LogCategory.API, `Starting Open Library API request for ISBN: ${isbn}...`); if (!isbn) { logMessage(LogCategory.API, 'ISBN is empty or undefined. Skipping Open Library API request.'); return null; // Early return if ISBN is missing } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 seconds timeout try { // const apiUrl = `\{OPEN\_LIBRARY\_API\_URL\}{isbn}.json`; // Construct API URL for ISBN lookup const apiUrl = `${OPEN_LIBRARY_API_URL}${isbn}.json`; logMessage(LogCategory.API, `Open Library API Fetch call - ISBN: ${isbn}`); // More specific log for Open Library API fetch const response = await fetch(apiUrl, { signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { logMessage(LogCategory.ERROR, `Open Library API request failed for ISBN: ${isbn}. Status: ${response.status} ${response.statusText}`); return null; // or throw new Error(`HTTP error! status: ${response.status}`); } logMessage(LogCategory.API, `Open Library API Response OK - ISBN: ${isbn}`); const responseData = await response.json(); logMessage(LogCategory.API, `Open Library API JSON parsing completed - ISBN: ${isbn}`); return responseData; // Return the raw JSON response data for now } catch (error: any) { // Capture error as any for now for simplicity clearTimeout(timeoutId); logMessage(LogCategory.ERROR, `Error during Open Library API request for ISBN: ${isbn}`, error as any); // Log error with category return null; // Handle fetch errors gracefully } } // api_google_books.ts import fetch, { Response } from 'node-fetch'; import { googleBooksApiKey, googleBooksApiUrl, apiTimeoutMs } from './config'; // Correct imports from config.ts and logging.ts import { logMessage, LogCategory } from './logging'; // Import logging functions const GOOGLE_BOOKS_API_URL = googleBooksApiUrl; // Use base URL from config const API_KEY = googleBooksApiKey; // Use API key from config (can be empty string if you don't have one) const API_TIMEOUT_MS = apiTimeoutMs; // API request timeout from config let timeoutId: NodeJS.Timeout; // Declare timeoutId in a wider scope if needed /** * Asynchronously fetches book data from Google Books API based on ISBN. * @param isbn - The ISBN of the book to search for. * @returns A Promise that resolves to the book data in JSON format, or null on error. */ export async function fetchBookByISBN(isbn: string): Promise { const queryUrl = `${GOOGLE_BOOKS_API_URL}?q=isbn:${isbn}&key=${API_KEY}`; return fetchData(queryUrl, `ISBN: ${isbn}`); } /** * Asynchronously fetches book data from Google Books API based on title and authors. * @param title - The title of the book to search for. * @param authors - An array of author names. * @returns A Promise that resolves to the book data in JSON format, or null on error. */ export async function fetchBookByTitle(title: string, authors: string[]): Promise { const authorQuery = authors.map(author => `author:"${author}"`).join('+'); // Format authors for query const queryString = `title:${title}+${authorQuery}`; // Construct the query string const queryUrl = `${GOOGLE_BOOKS_API_URL}?q=${queryString}&key=${API_KEY}`; // Combine base URL, query, and API key return fetchData(queryUrl, `Title Query, Query: ${queryString}`); } /** * Generic function to fetch data from Google Books API with timeout and error handling. * @param queryUrl - The complete API URL to fetch. * @param queryDescription - Description of the query for logging purposes (e.g., "ISBN: 1234567890", "Title Query: ..."). * @returns A Promise that resolves to the JSON response, or null on error. */ async function fetchData(queryUrl: string, queryDescription: string): Promise { logMessage(LogCategory.API_GOOGLEBOOKS, `Google Books API request for ${queryDescription}...`); // More specific log category <-- CORRECT USAGE of LogCategory.API_GOOGLEBOOKS logMessage(LogCategory.API_GOOGLEBOOKS, `Fetch call initiated - Type: ${queryDescription}, Query: ${queryUrl}`); // More specific log category and log full URL <-- CORRECT USAGE const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => { // Assign timeoutId here reject(new Error(`API request timed out after ${API_TIMEOUT_MS}ms`)); }, API_TIMEOUT_MS); }); try { const response = await Promise.race([fetch(queryUrl), timeoutPromise]) as Response; // Race fetch against timeout if (!response.ok) { const errorText = await response.text(); // Get error text from response logMessage(LogCategory.ERROR, `HTTP error ${response.status} during Google Books API request for ${queryDescription}. Response text: ${errorText}`); clearTimeout(timeoutId); // Clear timeout if response received return null; // Indicate failure } const data = await response.json(); clearTimeout(timeoutId); // Clear timeout if response received return data; // Return JSON data } catch (error: any) { // Catch and log errors during fetch clearTimeout(timeoutId); // Ensure timeout is cleared in case of fetch error as well let errorMessage = (error as any).message || 'Unknown fetch error'; // Try to get specific error message logMessage(LogCategory.ERROR, `Error during Google Books API request for ${queryDescription}. Error details: ${errorMessage}`, error as any); // Log error message with details return null; // Handle fetch errors gracefully } } // markdown.ts import fs from 'fs'; import path from 'path'; import { LocalBookMetadata } from './types'; // Import from types.ts const TEMPLATE_FILE_PATH = path.join(__dirname, '.', 'templates', 'book_note_template.md'); export function generateMarkdownFromTemplate(template: string, bookMetadata: LocalBookMetadata): string { const createdDate = new Date().toISOString(); const modifiedDate = new Date().toISOString(); // --- Helper function to format authors list --- const formatAuthorsList = (authors: string[] | undefined): string => { if (!authors || authors.length === 0) { return ' - "Unknown Author"'; // Default if no authors } return authors.map(author => ` - "${author}"`).join('\n'); }; // --- Helper function to format ISBNs list --- const formatIsbnsList = (isbns: { type: string, identifier: string }[] | undefined): string => { if (!isbns || isbns.length === 0) { return ' - ISBNs from Google Books API not found'; // Or "Not available" or similar } return isbns.map(isbn => ` - ${isbn.type}: ${isbn.identifier}`).join('\n'); }; // --- Helper function to format description as a block --- const formatDescriptionBlock = (description: string | undefined): string => { if (!description) { return ' Description from Google Books API not found.'; } return description; // Already a string, just return it }; // --- Helper function to format publisher and date lines (with "Not found" if missing) --- const formatLineWithNotFound = (value: string | undefined, source: string): string => { return value ? `- ${value}` : `- ${source} not found in ${source}`; // Indicate source of info }; const authorsListMarkdown = formatAuthorsList(bookMetadata.opfAuthors || bookMetadata.apiAuthors); // Prioritize OPF authors, fallback to API const isbnsListMarkdown = formatIsbnsList(bookMetadata.apiIsbns); // ISBNs will come from API only for now const descriptionBlockMarkdown = formatDescriptionBlock(bookMetadata.apiDescription); // --- Metadata source line --- const apiMetadataSourceLine = bookMetadata.apiSuccess ? `- Google Books API` : ''; // Indicate API data source if API was successful // --- Publisher and Date lines with "Not found" handling --- const opfPublisherLine = formatLineWithNotFound(bookMetadata.opfPublisher, 'OPF'); const opfPublishedDateLine = formatLineWithNotFound(bookMetadata.opfPublishedDate, 'OPF'); const apiPublisherLine = formatLineWithNotFound(bookMetadata.apiPublisher, 'Google Books API'); const apiPublishedDateLine = formatLineWithNotFound(bookMetadata.opfPublishedDate || bookMetadata.apiPublishedDate, 'Google Books API'); // Fallback to OPF Date if API Date is missing let markdownContent = template .replace(/{created_date}/g, createdDate) .replace(/{modified_date}/g, modifiedDate) .replace(/{book_id}/g, bookMetadata.opfUuid || 'UUID_NOT_FOUND') // Fallback if no UUID .replace(/{title}/g, bookMetadata.opfTitle || bookMetadata.folderTitle) // Fallback to folder title .replace(/{authors_list}/g, authorsListMarkdown) .replace(/{coverImagePath}/g, bookMetadata.coverImagePath || 'default_cover.jpg') // Use coverImagePath, default if missing .replace(/{opf_publisher_line}/g, opfPublisherLine) // OPF Publisher line .replace(/{opf_published_date_line}/g, opfPublishedDateLine) // OPF Published Date line .replace(/{api_publisher_line}/g, apiPublisherLine) // API Publisher line .replace(/{api_published_date_line}/g, apiPublishedDateLine) // API Published Date line .replace(/{api_description_block}/g, descriptionBlockMarkdown) // API Description block .replace(/{isbns_list}/g, isbnsListMarkdown) // ISBNs list .replace(/{api_metadata_source_line}/g, apiMetadataSourceLine); // API Metadata Source line return markdownContent; } // config.ts export const googleBooksApiUrl = 'https://www.googleapis.com/books/v1/volumes'; export const googleBooksApiKey = ''; // You can insert your Google Books API key here if you have one. Leave empty string if not. export const apiTimeoutMs = 5000; // API request timeout in milliseconds (e.g., 5000ms = 5 seconds) // book.ts export interface IndustryIdentifier { type: string; identifier: string; } export interface Book { isbn: string; title: string; authors?: string[]; publisher?: string; publishedDate?: string; description?: string; pageCount?: number; categories?: string[]; imageLinks?: { smallThumbnail?: string; thumbnail?: string; small?: string; medium?: string; large?: string; extraLarge?: string; }; language?: string; previewLink?: string; infoLink?: string; canonicalVolumeLink?: string; industryIdentifiers?: IndustryIdentifier[]; } // NEW INTERFACE - Google Books API Response ITEM export interface GoogleBooksApiResponseItem { // Interface for individual items in API response volumeInfo?: { // volumeInfo is optional, but we expect it to be there title: string; authors?: string[]; publisher?: string; publishedDate?: string; description?: string; pageCount?: number; categories?: string[]; imageLinks?: { smallThumbnail?: string; thumbnail?: string; small?: string; medium?: string; large?: string; extraLarge?: string; }; language?: string; previewLink?: string; infoLink?: string; canonicalVolumeLink?: string; industryIdentifiers?: IndustryIdentifier[]; }; } // Google Books API Response export interface GoogleBooksApiResponse { kind: string; totalItems: number; items?: GoogleBooksApiResponseItem[]; // 'items' is now an array of GoogleBooksApiResponseItem }