diff --git a/server/Server.js b/server/Server.js index 1f77e54a..f90e68ea 100644 --- a/server/Server.js +++ b/server/Server.js @@ -35,7 +35,6 @@ const AudioMetadataMangaer = require('./managers/AudioMetadataManager') const RssFeedManager = require('./managers/RssFeedManager') const CronManager = require('./managers/CronManager') const TaskManager = require('./managers/TaskManager') -const EBookManager = require('./managers/EBookManager') class Server { constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) { @@ -75,7 +74,6 @@ class Server { this.podcastManager = new PodcastManager(this.db, this.watcher, this.notificationManager, this.taskManager) this.audioMetadataManager = new AudioMetadataMangaer(this.db, this.taskManager) this.rssFeedManager = new RssFeedManager(this.db) - this.eBookManager = new EBookManager(this.db) this.scanner = new Scanner(this.db, this.coverManager) this.cronManager = new CronManager(this.db, this.scanner, this.podcastManager) diff --git a/server/controllers/EBookController.js b/server/controllers/EBookController.js deleted file mode 100644 index 743bea16..00000000 --- a/server/controllers/EBookController.js +++ /dev/null @@ -1,52 +0,0 @@ -const Logger = require('../Logger') -const { isNullOrNaN } = require('../utils/index') - -class EBookController { - constructor() { } - - async getEbookInfo(req, res) { - const isDev = req.query.dev == 1 - const json = await this.eBookManager.getBookInfo(req.libraryItem, req.user, isDev) - res.json(json) - } - - async getEbookPage(req, res) { - if (isNullOrNaN(req.params.page)) { - return res.status(400).send('Invalid page params') - } - const isDev = req.query.dev == 1 - const pageIndex = Number(req.params.page) - const page = await this.eBookManager.getBookPage(req.libraryItem, req.user, pageIndex, isDev) - if (!page) { - return res.status(500).send('Failed to get page') - } - - res.send(page) - } - - async getEbookResource(req, res) { - if (!req.query.path) { - return res.status(400).send('Invalid query path') - } - const isDev = req.query.dev == 1 - this.eBookManager.getBookResource(req.libraryItem, req.user, req.query.path, isDev, res) - } - - middleware(req, res, next) { - const item = this.db.libraryItems.find(li => li.id === req.params.id) - if (!item || !item.media) return res.sendStatus(404) - - // Check user can access this library item - if (!req.user.checkCanAccessLibraryItem(item)) { - return res.sendStatus(403) - } - - if (!item.isBook || !item.media.ebookFile) { - return res.status(400).send('Invalid ebook library item') - } - - req.libraryItem = item - next() - } -} -module.exports = new EBookController() \ No newline at end of file diff --git a/server/managers/EBookManager.js b/server/managers/EBookManager.js deleted file mode 100644 index 3dab617d..00000000 --- a/server/managers/EBookManager.js +++ /dev/null @@ -1,80 +0,0 @@ -const Logger = require('../Logger') -const StreamZip = require('../libs/nodeStreamZip') - -const parseEpub = require('../utils/parsers/parseEpub') - -class EBookManager { - constructor() { - this.extractedEpubs = {} - } - - async extractBookData(libraryItem, user, isDev = false) { - if (!libraryItem || !libraryItem.isBook || !libraryItem.media.ebookFile) return null - - if (this.extractedEpubs[libraryItem.id]) return this.extractedEpubs[libraryItem.id] - - const ebookFile = libraryItem.media.ebookFile - if (!ebookFile.isEpub) { - Logger.error(`[EBookManager] get book data is not supported for format ${ebookFile.ebookFormat}`) - return null - } - - this.extractedEpubs[libraryItem.id] = await parseEpub.parse(ebookFile, libraryItem.id, user.token, isDev) - - return this.extractedEpubs[libraryItem.id] - } - - async getBookInfo(libraryItem, user, isDev = false) { - if (!libraryItem || !libraryItem.isBook || !libraryItem.media.ebookFile) return null - - const bookData = await this.extractBookData(libraryItem, user, isDev) - - return { - title: libraryItem.media.metadata.title, - pages: bookData.pages.length - } - } - - async getBookPage(libraryItem, user, pageIndex, isDev = false) { - if (!libraryItem || !libraryItem.isBook || !libraryItem.media.ebookFile) return null - - const bookData = await this.extractBookData(libraryItem, user, isDev) - - const pageObj = bookData.pages[pageIndex] - - if (!pageObj) { - return null - } - - const parsed = await parseEpub.parsePage(pageObj.path, bookData, libraryItem.id, user.token, isDev) - - if (parsed.error) { - Logger.error(`[EBookManager] Failed to parse epub page at "${pageObj.path}"`, parsed.error) - return null - } - - return parsed.html - } - - async getBookResource(libraryItem, user, resourcePath, isDev = false, res) { - if (!libraryItem || !libraryItem.isBook || !libraryItem.media.ebookFile) return res.sendStatus(500) - const bookData = await this.extractBookData(libraryItem, user, isDev) - const resourceItem = bookData.resources.find(r => r.path === resourcePath) - - if (!resourceItem) { - return res.status(404).send('Resource not found') - } - - const zip = new StreamZip.async({ file: bookData.filepath }) - const stm = await zip.stream(resourceItem.path) - - res.set('content-type', resourceItem['media-type']) - - stm.pipe(res) - stm.on('end', () => { - zip.close() - }) - } - -} -module.exports = EBookManager \ No newline at end of file diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 3918bc2c..3693ec52 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -24,7 +24,6 @@ const SearchController = require('../controllers/SearchController') const CacheController = require('../controllers/CacheController') const ToolsController = require('../controllers/ToolsController') const RSSFeedController = require('../controllers/RSSFeedController') -const EBookController = require('../controllers/EBookController') const MiscController = require('../controllers/MiscController') const BookFinder = require('../finders/BookFinder') @@ -52,7 +51,6 @@ class ApiRouter { this.cronManager = Server.cronManager this.notificationManager = Server.notificationManager this.taskManager = Server.taskManager - this.eBookManager = Server.eBookManager this.bookFinder = new BookFinder() this.authorFinder = new AuthorFinder() @@ -284,13 +282,6 @@ class ApiRouter { this.router.post('/feeds/series/:seriesId/open', RSSFeedController.middleware.bind(this), RSSFeedController.openRSSFeedForSeries.bind(this)) this.router.post('/feeds/:id/close', RSSFeedController.middleware.bind(this), RSSFeedController.closeRSSFeed.bind(this)) - // - // EBook Routes - // - this.router.get('/ebooks/:id/info', EBookController.middleware.bind(this), EBookController.getEbookInfo.bind(this)) - this.router.get('/ebooks/:id/page/:page', EBookController.middleware.bind(this), EBookController.getEbookPage.bind(this)) - this.router.get('/ebooks/:id/resource', EBookController.middleware.bind(this), EBookController.getEbookResource.bind(this)) - // // Misc Routes // diff --git a/server/utils/parsers/parseEpub.js b/server/utils/parsers/parseEpub.js deleted file mode 100644 index fcdc7aa7..00000000 --- a/server/utils/parsers/parseEpub.js +++ /dev/null @@ -1,226 +0,0 @@ - -const Path = require('path') -const h = require('htmlparser2') -const ds = require('dom-serializer') - -const Logger = require('../../Logger') -const StreamZip = require('../../libs/nodeStreamZip') -const css = require('../../libs/css') - -const { xmlToJSON } = require('../index.js') - -module.exports.parse = async (ebookFile, libraryItemId, token, isDev) => { - const zip = new StreamZip.async({ file: ebookFile.metadata.path }) - const containerXml = await zip.entryData('META-INF/container.xml') - const containerJson = await xmlToJSON(containerXml.toString('utf8')) - - const packageOpfPath = containerJson.container.rootfiles[0].rootfile[0].$['full-path'] - const packageOpfDir = Path.dirname(packageOpfPath) - - const packageDoc = await zip.entryData(packageOpfPath) - const packageJson = await xmlToJSON(packageDoc.toString('utf8')) - - const pages = [] - - let manifestItems = packageJson.package.manifest[0].item.map(item => item.$) - const spineItems = packageJson.package.spine[0].itemref.map(ref => ref.$.idref) - for (const spineItem of spineItems) { - const mi = manifestItems.find(i => i.id === spineItem) - if (mi) { - manifestItems = manifestItems.filter(_mi => _mi.id !== mi.id) // Remove from manifest items - - mi.path = Path.posix.join(packageOpfDir, mi.href) - pages.push(mi) - } else { - Logger.error('[parseEpub] Invalid spine item', spineItem) - } - } - - const stylesheets = [] - const resources = [] - - for (const manifestItem of manifestItems) { - manifestItem.path = Path.posix.join(packageOpfDir, manifestItem.href) - - if (manifestItem['media-type'] === 'text/css') { - const stylesheetData = await zip.entryData(manifestItem.path) - const modifiedCss = this.parseStylesheet(stylesheetData.toString('utf8'), manifestItem.path, libraryItemId, token, isDev) - if (modifiedCss) { - manifestItem.style = modifiedCss - stylesheets.push(manifestItem) - } else { - Logger.error(`[parseEpub] Invalid stylesheet "${manifestItem.path}"`) - } - } else { - resources.push(manifestItem) - } - } - - await zip.close() - - return { - filepath: ebookFile.metadata.path, - epubVersion: packageJson.package.$.version, - packageDir: packageOpfDir, - resources, - stylesheets, - pages - } -} - -module.exports.parsePage = async (pagePath, bookData, libraryItemId, token, isDev) => { - const pageDir = Path.dirname(pagePath) - - const zip = new StreamZip.async({ file: bookData.filepath }) - const pageData = await zip.entryData(pagePath) - await zip.close() - const rawHtml = pageData.toString('utf8') - - const results = {} - - const dh = new h.DomHandler((err, dom) => { - if (err) return results.error = err - - // Get stylesheets - const isStylesheetLink = (elem) => elem.type == 'tag' && elem.name.toLowerCase() === 'link' && elem.attribs.rel === 'stylesheet' && elem.attribs.type === 'text/css' - const stylesheets = h.DomUtils.findAll(isStylesheetLink, dom) - - // Get body tag - const isBodyTag = (elem) => elem.type == 'tag' && elem.name.toLowerCase() == 'body' - const body = h.DomUtils.findOne(isBodyTag, dom) - - // Get all svg elements - const isSvgTag = (name) => ['svg'].includes((name || '').toLowerCase()) - const svgElements = h.DomUtils.getElementsByTagName(isSvgTag, body.children) - svgElements.forEach((el) => { - if (el.attribs.class) el.attribs.class += ' abs-svg-scale' - else el.attribs.class = 'abs-svg-scale' - }) - - // Get all img elements - const isImageTag = (name) => ['img', 'image'].includes((name || '').toLowerCase()) - const imgElements = h.DomUtils.getElementsByTagName(isImageTag, body.children) - - imgElements.forEach(el => { - if (!el.attribs.src && !el.attribs['xlink:href']) { - Logger.warn('[parseEpub] parsePage: Invalid img element attribs', el.attribs) - return - } - - if (el.attribs.class) el.attribs.class += ' abs-image-scale' - else el.attribs.class = 'abs-image-scale' - - const srcKey = el.attribs.src ? 'src' : 'xlink:href' - const src = encodeURIComponent(Path.posix.join(pageDir, el.attribs[srcKey])) - - const basePath = isDev ? 'http://localhost:3333' : '' - el.attribs[srcKey] = `${basePath}/api/ebooks/${libraryItemId}/resource?path=${src}&token=${token}` - }) - - let finalHtml = '
' - - stylesheets.forEach((el) => { - const href = Path.posix.join(pageDir, el.attribs.href) - const ssObj = bookData.stylesheets.find(sso => sso.path === href) - - // find @import css and add it - const importSheets = getStylesheetImports(ssObj.style, bookData.stylesheets) - if (importSheets) { - importSheets.forEach((sheet) => { - finalHtml += `\n` - }) - } - - if (!ssObj) { - Logger.warn('[parseEpub] parsePage: Stylesheet object not found for href', href) - } else { - finalHtml += `\n` - } - }) - - finalHtml += `\n` - - finalHtml += ds.render(body.children) - - finalHtml += '\n
' - - results.html = finalHtml - }) - - const parser = new h.Parser(dh) - parser.write(rawHtml) - parser.end() - - return results -} - -module.exports.parseStylesheet = (rawCss, stylesheetPath, libraryItemId, token, isDev) => { - try { - const stylesheetDir = Path.dirname(stylesheetPath) - - const res = css.parse(rawCss) - - res.stylesheet.rules.forEach((rule) => { - if (rule.type === 'rule') { - rule.selectors = rule.selectors.map(s => s === 'body' ? '.abs-page-content' : `.abs-page-content ${s}`) - } else if (rule.type === 'font-face' && rule.declarations) { - rule.declarations = rule.declarations.map(dec => { - if (dec.property === 'src') { - const match = dec.value.trim().split(' ').shift().match(/url\((.+)\)/) - if (match && match[1]) { - const fontPath = Path.posix.join(stylesheetDir, match[1]) - const newSrc = encodeURIComponent(fontPath) - - const basePath = isDev ? 'http://localhost:3333' : '' - dec.value = dec.value.replace(match[1], `"${basePath}/api/ebooks/${libraryItemId}/resource?path=${newSrc}&token=${token}"`) - } - } - return dec - }) - } else if (rule.type === 'import') { - const importUrl = rule.import - const match = importUrl.match(/\"(.*)\"/) - const path = match ? match[1] || '' : '' - if (path) { - // const newSrc = encodeURIComponent(Path.posix.join(stylesheetDir, path)) - // const basePath = isDev ? 'http://localhost:3333' : '' - // const newPath = `"${basePath}/api/ebooks/${libraryItemId}/resource?path=${newSrc}&token=${token}"` - // rule.import = rule.import.replace(path, newPath) - - rule.import = Path.posix.join(stylesheetDir, path) - } - } - }) - - return css.stringify(res) - } catch (error) { - Logger.error('[parseEpub] parseStylesheet: Failed', error) - return null - } -} - -function getStylesheetImports(rawCss, stylesheets) { - try { - const res = css.parse(rawCss) - - const imports = [] - res.stylesheet.rules.forEach((rule) => { - if (rule.type === 'import') { - const importUrl = rule.import.replaceAll('"', '') - const sheet = stylesheets.find(s => s.path === importUrl) - if (sheet) imports.push(sheet) - else { - Logger.error('[parseEpub] getStylesheetImports: Sheet not found', stylesheets) - } - } - }) - - return imports - } catch (error) { - Logger.error('[parseEpub] getStylesheetImports: Failed', error) - return null - } -} \ No newline at end of file