UNPKG

@kaminaduck/scryfall-mcp-server

Version:

A Model Context Protocol (MCP) server that provides access to the Scryfall API for Magic: The Gathering card data

274 lines 10.7 kB
/** * Database bulk operations for the Scryfall card database. * * This module provides utilities for bulk operations such as: * - Verifying database integrity * - Scanning directories for image files * - Cleaning the database * - Generating reports */ import { readFile, readdir } from 'node:fs/promises'; import { existsSync, statSync } from 'node:fs'; import { join, dirname, basename, extname } from 'node:path'; import { createCardDatabase } from './database.js'; import { logger } from './logger.js'; export class BulkOperations { db = null; async init() { if (!this.db) { this.db = await createCardDatabase(); } } /** * Verify database integrity by checking if all referenced files exist. */ async verifyDatabaseIntegrity(verbose = false) { await this.init(); const cards = this.db.getAllCards(); const totalRecords = cards.length; let missingFiles = 0; logger.info(`Verifying ${totalRecords} database records...`); for (const card of cards) { const filename = card.filename; if (!existsSync(filename)) { missingFiles++; if (verbose) { logger.warn(`Missing file: ${filename} for card ${card.card_name}`); } } } if (missingFiles === 0) { logger.info('All files referenced in the database exist.'); } else { logger.warn(`Found ${missingFiles} missing files out of ${totalRecords} records.`); } return { totalRecords, missingFiles }; } /** * Scan a directory for image files and optionally add them to the database. */ async scanDirectoryForImages(directory, updateDb = false, verbose = false) { if (!existsSync(directory)) { logger.error(`Directory ${directory} does not exist.`); return { totalFiles: 0, addedToDb: 0 }; } await this.init(); let totalFiles = 0; let addedToDb = 0; logger.info(`Scanning directory ${directory} for image files...`); const imageFiles = await this.walkDirectoryForImages(directory); for (const filePath of imageFiles) { totalFiles++; try { // Extract set name from directory const setName = basename(dirname(filePath)).replace(/_/g, ' '); // Extract card name from filename const fileName = basename(filePath); let cardName = basename(fileName, extname(fileName)).replace(/_/g, ' '); // Check if there's a corresponding JSON file with more info const jsonFilePath = join(dirname(filePath), `${basename(fileName, extname(fileName))}.json`); let cardId; let setCode; let imageUrl; if (existsSync(jsonFilePath)) { try { const jsonContent = await readFile(jsonFilePath, 'utf-8'); const cardData = JSON.parse(jsonContent); cardId = cardData.id; setCode = cardData.set; imageUrl = cardData.image_uris?.art_crop; // Use the actual card name from the JSON if available if (cardData.name) { cardName = cardData.name; } } catch (error) { if (verbose) { logger.error(`Error parsing JSON file: ${jsonFilePath}`, error); } } } // Create a unique identifier for the card const cardVersionId = `${cardName}_${setCode || ''}_${cardId || ''}_art_crop`; if (updateDb) { if (!this.db.cardExists(cardVersionId)) { this.db.addCard(cardVersionId, filePath, cardId, setCode, imageUrl); addedToDb++; if (verbose) { logger.info(`Added to database: ${filePath}`); } } else if (verbose) { logger.debug(`Already in database: ${filePath}`); } } else if (verbose) { logger.debug(`Found image: ${filePath}`); } } catch (error) { if (verbose) { logger.error(`Error processing ${filePath}:`, error); } } } logger.info(`Found ${totalFiles} image files.`); if (updateDb) { logger.info(`Added ${addedToDb} new records to the database.`); } return { totalFiles, addedToDb }; } /** * Recursively walk a directory and find all image files. */ async walkDirectoryForImages(directory) { const imageFiles = []; const imageExtensions = ['.jpg', '.png', '.jpeg', '.gif']; try { const entries = await readdir(directory, { withFileTypes: true }); for (const entry of entries) { const fullPath = join(directory, entry.name); if (entry.isFile()) { const extension = extname(entry.name).toLowerCase(); if (imageExtensions.includes(extension)) { imageFiles.push(fullPath); } } else if (entry.isDirectory()) { const subFiles = await this.walkDirectoryForImages(fullPath); imageFiles.push(...subFiles); } } } catch (error) { logger.error(`Error reading directory ${directory}:`, error); } return imageFiles; } /** * Clean the database by removing records for files that no longer exist. */ async cleanDatabase(dryRun = true, verbose = false) { await this.init(); const cards = this.db.getAllCards(); const toRemove = []; logger.info(`Checking ${cards.length} database records for missing files...`); for (const card of cards) { const filename = card.filename; if (!existsSync(filename)) { toRemove.push(card.card_name); if (verbose) { logger.info(`Will remove: ${card.card_name} (file: ${filename})`); } } } if (dryRun) { logger.info(`Would remove ${toRemove.length} records from the database (dry run).`); } else { for (const cardName of toRemove) { this.db.removeCard(cardName); } logger.info(`Removed ${toRemove.length} records from the database.`); } return toRemove.length; } /** * Generate a comprehensive report on the database status. */ async generateReport() { await this.init(); const cards = this.db.getAllCards(); const totalRecords = cards.length; // Count by set const sets = {}; for (const card of cards) { const setCode = card.set_code || 'Unknown'; if (!sets[setCode]) { sets[setCode] = 0; } sets[setCode]++; } // Check for missing files let missingFiles = 0; for (const card of cards) { if (!existsSync(card.filename)) { missingFiles++; } } // Check image directories let setDirectories = 0; let totalImages = 0; const imageDir = '.local/scryfall_images'; if (existsSync(imageDir)) { try { const entries = await readdir(imageDir, { withFileTypes: true }); const setDirs = entries.filter(entry => entry.isDirectory()).map(entry => entry.name); setDirectories = setDirs.length; // Count image files for (const setDir of setDirs) { const setPath = join(imageDir, setDir); try { const files = await readdir(setPath); const imageFiles = files.filter(file => { const fullPath = join(setPath, file); return statSync(fullPath).isFile() && ['.jpg', '.png', '.jpeg', '.gif'].includes(extname(file).toLowerCase()); }); totalImages += imageFiles.length; } catch (error) { // Ignore errors for individual directories } } } catch (error) { logger.error(`Error reading image directory ${imageDir}:`, error); } } const databaseCoverage = totalImages > 0 ? (totalRecords / totalImages) * 100 : 0; return { totalRecords, missingFiles, sets, setDirectories, totalImages, databaseCoverage: Math.round(databaseCoverage * 100) / 100 }; } /** * Print a formatted report to the logger. */ async printReport() { const report = await this.generateReport(); logger.info('\n=== Database Report ==='); logger.info(`Total records: ${report.totalRecords}`); logger.info(`Missing files: ${report.missingFiles}`); logger.info('\nRecords by set:'); const sortedSets = Object.entries(report.sets) .sort(([, a], [, b]) => b - a); for (const [setCode, count] of sortedSets) { logger.info(` ${setCode}: ${count}`); } if (report.setDirectories > 0) { logger.info(`\nFound ${report.setDirectories} set directories`); logger.info(`Total image files: ${report.totalImages}`); logger.info(`Database coverage: ${report.databaseCoverage}% of files are in the database`); } else { logger.info('\nNo image directories found.'); } } /** * Close the database connection. */ close() { if (this.db) { this.db.close(); } } } // Export a singleton instance export const bulkOperations = new BulkOperations(); //# sourceMappingURL=bulkOperations.js.map