Added getting epub metadata

This commit is contained in:
stephen 2026-02-13 17:13:05 +11:00
parent 96eda53f31
commit 6a3470229e
3 changed files with 155 additions and 31 deletions

118
package-lock.json generated
View file

@ -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": {

View file

@ -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",

View file

@ -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);