mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-07 09:51:37 +00:00
102 lines
3 KiB
JavaScript
102 lines
3 KiB
JavaScript
const StreamZip = require('node-stream-zip')
|
|
const { xmlToJSON } = require('./index')
|
|
const Path = require('path')
|
|
const Logger = require('../Logger')
|
|
|
|
class TextExtractor {
|
|
/**
|
|
* Extract text content from EPUB file, split by chapters
|
|
* @param {string} epubPath - Path to EPUB file
|
|
* @returns {Promise<Array<{title: string, text: string}>>} Array of chapters
|
|
*/
|
|
async extractFromEpub(epubPath) {
|
|
const zip = new StreamZip.async({ file: epubPath })
|
|
|
|
try {
|
|
// Read container.xml to find content.opf
|
|
const containerXml = await zip.entryData('META-INF/container.xml')
|
|
const container = await xmlToJSON(containerXml.toString())
|
|
const rootfilePath = container?.container?.rootfiles?.[0]?.rootfile?.[0]?.$?.['full-path']
|
|
|
|
if (!rootfilePath) {
|
|
throw new Error('Could not find rootfile in container.xml')
|
|
}
|
|
|
|
// Read content.opf
|
|
const opfData = await zip.entryData(rootfilePath)
|
|
const opf = await xmlToJSON(opfData.toString())
|
|
const opfDir = Path.dirname(rootfilePath)
|
|
|
|
// Get spine items (reading order)
|
|
const spine = opf?.package?.spine?.[0]?.itemref || []
|
|
const manifest = opf?.package?.manifest?.[0]?.item || []
|
|
|
|
// Build manifest map
|
|
const manifestMap = {}
|
|
for (const item of manifest) {
|
|
const attrs = item.$ || item
|
|
if (attrs.id) {
|
|
manifestMap[attrs.id] = attrs
|
|
}
|
|
}
|
|
|
|
const chapters = []
|
|
|
|
for (const itemref of spine) {
|
|
const idref = itemref.$?.idref || itemref.idref
|
|
const manifestItem = manifestMap[idref]
|
|
if (!manifestItem) continue
|
|
|
|
const href = manifestItem.href
|
|
const mediaType = manifestItem['media-type']
|
|
|
|
// Only process HTML/XHTML content
|
|
if (!mediaType || (!mediaType.includes('html') && !mediaType.includes('xml'))) {
|
|
continue
|
|
}
|
|
|
|
const filePath = opfDir ? Path.join(opfDir, href) : href
|
|
|
|
try {
|
|
const content = await zip.entryData(filePath.replace(/\\/g, '/'))
|
|
const text = this.stripHtml(content.toString())
|
|
|
|
if (text.trim().length > 0) {
|
|
chapters.push({
|
|
title: `Chapter ${chapters.length + 1}`,
|
|
text: text.trim()
|
|
})
|
|
}
|
|
} catch (err) {
|
|
Logger.warn(`[TextExtractor] Could not read ${filePath}: ${err.message}`)
|
|
}
|
|
}
|
|
|
|
return chapters
|
|
} finally {
|
|
await zip.close()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Strip HTML tags and decode entities
|
|
* @param {string} html
|
|
* @returns {string}
|
|
*/
|
|
stripHtml(html) {
|
|
return html
|
|
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
.replace(/<[^>]+>/g, ' ')
|
|
.replace(/ /g, ' ')
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, "'")
|
|
.replace(/\s+/g, ' ')
|
|
.trim()
|
|
}
|
|
}
|
|
|
|
module.exports = new TextExtractor()
|