diff --git a/package-lock.json b/package-lock.json index 1487b63..a96897c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@prisma/adapter-pg": "^7.2.0", "@prisma/client": "^7.2.0", "epub": "^1.3.0", + "epub2": "^3.0.2", "exif-reader": "^2.0.3", "next": "16.1.1", "next-auth": "^5.0.0-beta.30", @@ -2205,7 +2206,6 @@ "version": "20.19.27", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -2936,6 +2936,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-hyper-unique": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/array-hyper-unique/-/array-hyper-unique-2.1.6.tgz", + "integrity": "sha512-BdlHRqjKSYs88WFaVNVEc6Kv8ln/FdzCKPbcDPuWs4/EXkQFhnjc8TyR7hnPxRjcjo5LKOhUMGUWpAqRgeJvpA==", + "license": "ISC", + "dependencies": { + "deep-eql": "= 4.0.0", + "lodash": "^4.17.21" + } + }, "node_modules/array-includes": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", @@ -3163,6 +3173,12 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -3498,6 +3514,15 @@ "node": ">=10" } }, + "node_modules/crlf-normalize": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/crlf-normalize/-/crlf-normalize-1.0.20.tgz", + "integrity": "sha512-h/rBerTd3YHQGfv7tNT25mfhWvRq2BBLCZZ80GFarFxf6HQGbpW6iqDL3N+HBLpjLfAdcBXfWAzVlLfHkRUQBQ==", + "license": "ISC", + "dependencies": { + "ts-type": ">=2" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3597,6 +3622,18 @@ } } }, + "node_modules/deep-eql": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.0.0.tgz", + "integrity": "sha512-GxJC5MOg2KyQlv6WiUF/VAnMj4MWnYiXo4oLgeptOELVoknyErb4Z8+5F/IM/K4g9/80YzzatxmWcyRwUseH0A==", + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -3833,6 +3870,42 @@ "zipfile": "^0.5.11" } }, + "node_modules/epub2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/epub2/-/epub2-3.0.2.tgz", + "integrity": "sha512-rhvpt27CV5MZfRetfNtdNwi3XcNg1Am0TwfveJkK8YWeHItHepQ8Js9J06v8XRIjuTrCW/NSGYMTy55Of7BfNQ==", + "license": "ISC", + "dependencies": { + "adm-zip": "^0.5.10", + "array-hyper-unique": "^2.1.4", + "bluebird": "^3.7.2", + "crlf-normalize": "^1.0.19", + "tslib": "^2.6.2", + "xml2js": "^0.6.2" + } + }, + "node_modules/epub2/node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/epub2/node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -5772,6 +5845,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -7889,6 +7968,27 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-toolbelt": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz", + "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/ts-type": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ts-type/-/ts-type-3.0.1.tgz", + "integrity": "sha512-cleRydCkBGBFQ4KAvLH0ARIkciduS745prkGVVxPGvcRGhMMoSJUB7gNR1ByKhFTEYrYRg2CsMRGYnqp+6op+g==", + "license": "ISC", + "dependencies": { + "@types/node": "*", + "tslib": ">=2", + "typedarray-dts": "^1.0.0" + }, + "peerDependencies": { + "ts-toolbelt": "^9.6.0" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -7934,6 +8034,15 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -8012,6 +8121,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typedarray-dts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typedarray-dts/-/typedarray-dts-1.0.0.tgz", + "integrity": "sha512-Ka0DBegjuV9IPYFT1h0Qqk5U4pccebNIJCGl8C5uU7xtOs+jpJvKGAY4fHGK25hTmXZOEUl9Cnsg5cS6K/b5DA==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -8073,7 +8188,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unrs-resolver": { diff --git a/package.json b/package.json index 7b11f05..93cc779 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@prisma/adapter-pg": "^7.2.0", "@prisma/client": "^7.2.0", "epub": "^1.3.0", + "epub2": "^3.0.2", "exif-reader": "^2.0.3", "next": "16.1.1", "next-auth": "^5.0.0-beta.30", diff --git a/src/lib/metadata-extractor.ts b/src/lib/metadata-extractor.ts index a9fee5b..29ed8f9 100644 --- a/src/lib/metadata-extractor.ts +++ b/src/lib/metadata-extractor.ts @@ -1,30 +1,23 @@ // src/lib/metadata-extractor.ts -// 1. Update your import to use the new named export -//import * as PDFLib from 'pdf-parse'; -//import { PDFParse } from 'pdf-parse'; -//import {pdf} from 'pdf-parse'; +// src/lib/metadata-extractor.ts import * as PdfParse from 'pdf-parse-new'; import sharp from 'sharp'; import exifReader from 'exif-reader'; +import EPub from 'epub2'; // New Import /** - * Converts EXIF DMS (Degrees, Minutes, Seconds) array to Decimal Degrees. + * Converts EXIF DMS array to Decimal Degrees. */ function convertDMSToDD(dms: any, ref: string): string { if (!Array.isArray(dms) || dms.length < 3) return String(dms); - const [degrees, minutes, seconds] = dms; let dd = degrees + (minutes / 60) + (seconds / 3600); - - if (ref === 'S' || ref === 'W') { - dd = dd * -1; - } + if (ref === 'S' || ref === 'W') dd = dd * -1; return dd.toFixed(6); } /** - * RECURSIVE SANITIZER: - * Converts Buffers to strings, standardizes keys, and handles GPS conversion. + * RECURSIVE SANITIZER */ function sanitizeMetadata(obj: any): any { if (obj === null || typeof obj !== 'object') return obj; @@ -36,7 +29,6 @@ function sanitizeMetadata(obj: any): any { for (const [key, value] of Object.entries(obj)) { const cleanKey = key.charAt(0).toLowerCase() + key.slice(1); - // Specifically handle GPS Latitude/Longitude Arrays if (cleanKey === 'gPSLatitude' && obj['gPSLatitudeRef']) { sanitized[cleanKey] = convertDMSToDD(value, obj['gPSLatitudeRef']); continue; @@ -55,24 +47,15 @@ export async function extractMetadata(buffer: Buffer, filename: string): Promise const extension = filename.split('.').pop()?.toLowerCase(); try { - // --- 1. PDF EXTRACTION (pdf-parse-new) --- + // --- 1. PDF EXTRACTION --- if (extension === 'pdf') { - const parser = new PdfParse.SmartPDFParser({ - oversaturationFactor: 2.0, - enableFastPath: true - }); - - console.log(`--- PDF Extraction Start: ${filename} ---`); + const parser = new PdfParse.SmartPDFParser({ oversaturationFactor: 2.0, enableFastPath: true }); const result = await parser.parse(buffer); - return { type: 'PDF', title: filename, - // pageCount is specifically named 'numpages' in this library pageCount: result.numpages || 0, - // We sanitize the result.info to handle those nested Custom objects details: sanitizeMetadata(result.info || {}), - // Use result.text for a preview if available textPreview: result.text ? result.text.substring(0, 200).replace(/\s+/g, ' ') : "" }; } @@ -81,17 +64,13 @@ export async function extractMetadata(buffer: Buffer, filename: string): Promise if (['jpg', 'jpeg', 'png', 'webp'].includes(extension || '')) { const image = sharp(buffer); const metadata = await image.metadata(); - let details = {}; if (metadata.exif) { try { const rawExif = exifReader(metadata.exif); details = sanitizeMetadata(rawExif); - } catch (e) { - console.warn("EXIF Parse failed"); - } + } catch (e) { console.warn("EXIF Parse failed"); } } - return { type: `IMAGE (${metadata.format?.toUpperCase()})`, dimensions: `${metadata.width}x${metadata.height}`, @@ -100,6 +79,36 @@ export async function extractMetadata(buffer: Buffer, filename: string): Promise }; } + // --- 3. EPUB EXTRACTION (New Section) --- + if (extension === 'epub') { + return new Promise((resolve, reject) => { + // We initialize the EPub instance with a null image path and use the buffer + const epub = new EPub(buffer); + + epub.on('error', (err) => { + console.error("EPUB Parser Error:", err); + resolve({ type: 'EPUB', title: filename, error: "Failed to parse EPUB" }); + }); + + epub.on('end', () => { + // Standardizing the metadata for your UI + resolve({ + type: 'EPUB', + title: epub.metadata.title || filename, + author: epub.metadata.creator || 'Unknown', + details: sanitizeMetadata(epub.metadata), + // Use the number of chapters/manifest items as a rough "page" guide + pageCount: epub.spine.contents.length || 0, + textPreview: epub.metadata.description + ? epub.metadata.description.substring(0, 200).replace(/<[^>]*>?/gm, '') + : "" + }); + }); + + epub.parse(); + }); + } + return { type: 'FILE', title: filename }; } catch (error: any) { console.error(`❌ Extraction failed for ${filename}:`, error.message);