mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Add:Scanner extracts cover from comic files #1837 and ComicInfo.xml parser
This commit is contained in:
		
							parent
							
								
									e76af3bfc2
								
							
						
					
					
						commit
						f5545cd3f4
					
				
							
								
								
									
										21
									
								
								server/libs/libarchive/LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								server/libs/libarchive/LICENSE
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					MIT License
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Copyright (c) 2018 ნიკა
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
				
			||||||
 | 
					of this software and associated documentation files (the "Software"), to deal
 | 
				
			||||||
 | 
					in the Software without restriction, including without limitation the rights
 | 
				
			||||||
 | 
					to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | 
				
			||||||
 | 
					copies of the Software, and to permit persons to whom the Software is
 | 
				
			||||||
 | 
					furnished to do so, subject to the following conditions:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The above copyright notice and this permission notice shall be included in all
 | 
				
			||||||
 | 
					copies or substantial portions of the Software.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
				
			||||||
 | 
					IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | 
				
			||||||
 | 
					FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | 
				
			||||||
 | 
					AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | 
				
			||||||
 | 
					LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
				
			||||||
 | 
					OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 | 
				
			||||||
 | 
					SOFTWARE.
 | 
				
			||||||
							
								
								
									
										262
									
								
								server/libs/libarchive/archive.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										262
									
								
								server/libs/libarchive/archive.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,262 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Modified from https://github.com/nika-begiashvili/libarchivejs
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Path = require('path')
 | 
				
			||||||
 | 
					const { Worker } = require('worker_threads')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Represents compressed file before extraction
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					class CompressedFile {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructor(name, size, path, archiveRef) {
 | 
				
			||||||
 | 
					        this._name = name
 | 
				
			||||||
 | 
					        this._size = size
 | 
				
			||||||
 | 
					        this._path = path
 | 
				
			||||||
 | 
					        this._archiveRef = archiveRef
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * file name
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    get name() {
 | 
				
			||||||
 | 
					        return this._name
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * file size
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    get size() {
 | 
				
			||||||
 | 
					        return this._size
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Extract file from archive
 | 
				
			||||||
 | 
					     * @returns {Promise<File>} extracted file
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    extract() {
 | 
				
			||||||
 | 
					        return this._archiveRef.extractSingleFile(this._path)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Archive {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Creates new archive instance from browser native File object
 | 
				
			||||||
 | 
					     * @param {Buffer} fileBuffer
 | 
				
			||||||
 | 
					     * @param {object} options
 | 
				
			||||||
 | 
					     * @returns {Archive}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    static open(fileBuffer) {
 | 
				
			||||||
 | 
					        const arch = new Archive(fileBuffer, { workerUrl: Path.join(__dirname, 'libarchiveWorker.js') })
 | 
				
			||||||
 | 
					        return arch.open()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Create new archive
 | 
				
			||||||
 | 
					     * @param {File} file 
 | 
				
			||||||
 | 
					     * @param {Object} options 
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    constructor(file, options) {
 | 
				
			||||||
 | 
					        this._worker = new Worker(options.workerUrl)
 | 
				
			||||||
 | 
					        this._worker.on('message', this._workerMsg.bind(this))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this._callbacks = []
 | 
				
			||||||
 | 
					        this._content = {}
 | 
				
			||||||
 | 
					        this._processed = 0
 | 
				
			||||||
 | 
					        this._file = file
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Prepares file for reading
 | 
				
			||||||
 | 
					     * @returns {Promise<Archive>} archive instance
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async open() {
 | 
				
			||||||
 | 
					        await this._postMessage({ type: 'HELLO' }, (resolve, reject, msg) => {
 | 
				
			||||||
 | 
					            if (msg.type === 'READY') {
 | 
				
			||||||
 | 
					                resolve()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        return await this._postMessage({ type: 'OPEN', file: this._file }, (resolve, reject, msg) => {
 | 
				
			||||||
 | 
					            if (msg.type === 'OPENED') {
 | 
				
			||||||
 | 
					                resolve(this)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Terminate worker to free up memory
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    close() {
 | 
				
			||||||
 | 
					        this._worker.terminate()
 | 
				
			||||||
 | 
					        this._worker = null
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * detect if archive has encrypted data
 | 
				
			||||||
 | 
					     * @returns {boolean|null} null if could not be determined
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    hasEncryptedData() {
 | 
				
			||||||
 | 
					        return this._postMessage({ type: 'CHECK_ENCRYPTION' },
 | 
				
			||||||
 | 
					            (resolve, reject, msg) => {
 | 
				
			||||||
 | 
					                if (msg.type === 'ENCRYPTION_STATUS') {
 | 
				
			||||||
 | 
					                    resolve(msg.status)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * set password to be used when reading archive
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    usePassword(archivePassword) {
 | 
				
			||||||
 | 
					        return this._postMessage({ type: 'SET_PASSPHRASE', passphrase: archivePassword },
 | 
				
			||||||
 | 
					            (resolve, reject, msg) => {
 | 
				
			||||||
 | 
					                if (msg.type === 'PASSPHRASE_STATUS') {
 | 
				
			||||||
 | 
					                    resolve(msg.status)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Returns object containing directory structure and file information 
 | 
				
			||||||
 | 
					     * @returns {Promise<object>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    getFilesObject() {
 | 
				
			||||||
 | 
					        if (this._processed > 0) {
 | 
				
			||||||
 | 
					            return Promise.resolve().then(() => this._content)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return this._postMessage({ type: 'LIST_FILES' }, (resolve, reject, msg) => {
 | 
				
			||||||
 | 
					            if (msg.type === 'ENTRY') {
 | 
				
			||||||
 | 
					                const entry = msg.entry
 | 
				
			||||||
 | 
					                const [target, prop] = this._getProp(this._content, entry.path)
 | 
				
			||||||
 | 
					                if (entry.type === 'FILE') {
 | 
				
			||||||
 | 
					                    target[prop] = new CompressedFile(entry.fileName, entry.size, entry.path, this)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                return true
 | 
				
			||||||
 | 
					            } else if (msg.type === 'END') {
 | 
				
			||||||
 | 
					                this._processed = 1
 | 
				
			||||||
 | 
					                resolve(this._cloneContent(this._content))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getFilesArray() {
 | 
				
			||||||
 | 
					        return this.getFilesObject().then((obj) => {
 | 
				
			||||||
 | 
					            return this._objectToArray(obj)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    extractSingleFile(target) {
 | 
				
			||||||
 | 
					        // Prevent extraction if worker already terminated
 | 
				
			||||||
 | 
					        if (this._worker === null) {
 | 
				
			||||||
 | 
					            throw new Error("Archive already closed")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return this._postMessage({ type: 'EXTRACT_SINGLE_FILE', target: target },
 | 
				
			||||||
 | 
					            (resolve, reject, msg) => {
 | 
				
			||||||
 | 
					                if (msg.type === 'FILE') {
 | 
				
			||||||
 | 
					                    resolve(msg.entry)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Returns object containing directory structure and extracted File objects 
 | 
				
			||||||
 | 
					     * @param {Function} extractCallback
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    extractFiles(extractCallback) {
 | 
				
			||||||
 | 
					        if (this._processed > 1) {
 | 
				
			||||||
 | 
					            return Promise.resolve().then(() => this._content)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return this._postMessage({ type: 'EXTRACT_FILES' }, (resolve, reject, msg) => {
 | 
				
			||||||
 | 
					            if (msg.type === 'ENTRY') {
 | 
				
			||||||
 | 
					                const [target, prop] = this._getProp(this._content, msg.entry.path)
 | 
				
			||||||
 | 
					                if (msg.entry.type === 'FILE') {
 | 
				
			||||||
 | 
					                    target[prop] = msg.entry
 | 
				
			||||||
 | 
					                    if (extractCallback !== undefined) {
 | 
				
			||||||
 | 
					                        setTimeout(extractCallback.bind(null, {
 | 
				
			||||||
 | 
					                            file: target[prop],
 | 
				
			||||||
 | 
					                            path: msg.entry.path,
 | 
				
			||||||
 | 
					                        }))
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                return true
 | 
				
			||||||
 | 
					            } else if (msg.type === 'END') {
 | 
				
			||||||
 | 
					                this._processed = 2
 | 
				
			||||||
 | 
					                this._worker.terminate()
 | 
				
			||||||
 | 
					                resolve(this._cloneContent(this._content))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _cloneContent(obj) {
 | 
				
			||||||
 | 
					        if (obj instanceof CompressedFile || obj === null) return obj
 | 
				
			||||||
 | 
					        const o = {}
 | 
				
			||||||
 | 
					        for (const prop of Object.keys(obj)) {
 | 
				
			||||||
 | 
					            o[prop] = this._cloneContent(obj[prop])
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return o
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _objectToArray(obj, path = '') {
 | 
				
			||||||
 | 
					        const files = []
 | 
				
			||||||
 | 
					        for (const key of Object.keys(obj)) {
 | 
				
			||||||
 | 
					            if (obj[key] instanceof CompressedFile || obj[key] === null) {
 | 
				
			||||||
 | 
					                files.push({
 | 
				
			||||||
 | 
					                    file: obj[key] || key,
 | 
				
			||||||
 | 
					                    path: path
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                files.push(...this._objectToArray(obj[key], `${path}${key}/`))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return files
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _getProp(obj, path) {
 | 
				
			||||||
 | 
					        const parts = path.split('/')
 | 
				
			||||||
 | 
					        if (parts[parts.length - 1] === '') parts.pop()
 | 
				
			||||||
 | 
					        let cur = obj, prev = null
 | 
				
			||||||
 | 
					        for (const part of parts) {
 | 
				
			||||||
 | 
					            cur[part] = cur[part] || {}
 | 
				
			||||||
 | 
					            prev = cur
 | 
				
			||||||
 | 
					            cur = cur[part]
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return [prev, parts[parts.length - 1]]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _postMessage(msg, callback) {
 | 
				
			||||||
 | 
					        this._worker.postMessage(msg)
 | 
				
			||||||
 | 
					        return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					            this._callbacks.push(this._msgHandler.bind(this, callback, resolve, reject))
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _msgHandler(callback, resolve, reject, msg) {
 | 
				
			||||||
 | 
					        if (!msg) {
 | 
				
			||||||
 | 
					            reject('invalid msg')
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (msg.type === 'BUSY') {
 | 
				
			||||||
 | 
					            reject('worker is busy')
 | 
				
			||||||
 | 
					        } else if (msg.type === 'ERROR') {
 | 
				
			||||||
 | 
					            reject(msg.error)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            return callback(resolve, reject, msg)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _workerMsg(msg) {
 | 
				
			||||||
 | 
					        const callback = this._callbacks[this._callbacks.length - 1]
 | 
				
			||||||
 | 
					        const next = callback(msg)
 | 
				
			||||||
 | 
					        if (!next) {
 | 
				
			||||||
 | 
					            this._callbacks.pop()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					module.exports = Archive
 | 
				
			||||||
							
								
								
									
										72
									
								
								server/libs/libarchive/libarchiveWorker.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								server/libs/libarchive/libarchiveWorker.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,72 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Modified from https://github.com/nika-begiashvili/libarchivejs
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { parentPort } = require('worker_threads')
 | 
				
			||||||
 | 
					const { getArchiveReader } = require('./wasm-module')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let reader = null
 | 
				
			||||||
 | 
					let busy = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					getArchiveReader((_reader) => {
 | 
				
			||||||
 | 
					  reader = _reader
 | 
				
			||||||
 | 
					  busy = false
 | 
				
			||||||
 | 
					  parentPort.postMessage({ type: 'READY' })
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					parentPort.on('message', async msg => {
 | 
				
			||||||
 | 
					  if (busy) {
 | 
				
			||||||
 | 
					    parentPort.postMessage({ type: 'BUSY' })
 | 
				
			||||||
 | 
					    return
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let skipExtraction = false
 | 
				
			||||||
 | 
					  busy = true
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    switch (msg.type) {
 | 
				
			||||||
 | 
					      case 'HELLO': // module will respond READY when it's ready
 | 
				
			||||||
 | 
					        break
 | 
				
			||||||
 | 
					      case 'OPEN':
 | 
				
			||||||
 | 
					        await reader.open(msg.file)
 | 
				
			||||||
 | 
					        parentPort.postMessage({ type: 'OPENED' })
 | 
				
			||||||
 | 
					        break
 | 
				
			||||||
 | 
					      case 'LIST_FILES':
 | 
				
			||||||
 | 
					        skipExtraction = true
 | 
				
			||||||
 | 
					      // eslint-disable-next-line no-fallthrough
 | 
				
			||||||
 | 
					      case 'EXTRACT_FILES':
 | 
				
			||||||
 | 
					        for (const entry of reader.entries(skipExtraction)) {
 | 
				
			||||||
 | 
					          parentPort.postMessage({ type: 'ENTRY', entry })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        parentPort.postMessage({ type: 'END' })
 | 
				
			||||||
 | 
					        break
 | 
				
			||||||
 | 
					      case 'EXTRACT_SINGLE_FILE':
 | 
				
			||||||
 | 
					        for (const entry of reader.entries(true, msg.target)) {
 | 
				
			||||||
 | 
					          if (entry.fileData) {
 | 
				
			||||||
 | 
					            parentPort.postMessage({ type: 'FILE', entry })
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        break
 | 
				
			||||||
 | 
					      case 'CHECK_ENCRYPTION':
 | 
				
			||||||
 | 
					        parentPort.postMessage({ type: 'ENCRYPTION_STATUS', status: reader.hasEncryptedData() })
 | 
				
			||||||
 | 
					        break
 | 
				
			||||||
 | 
					      case 'SET_PASSPHRASE':
 | 
				
			||||||
 | 
					        reader.setPassphrase(msg.passphrase)
 | 
				
			||||||
 | 
					        parentPort.postMessage({ type: 'PASSPHRASE_STATUS', status: true })
 | 
				
			||||||
 | 
					        break
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        throw new Error('Invalid Command')
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    parentPort.postMessage({
 | 
				
			||||||
 | 
					      type: 'ERROR',
 | 
				
			||||||
 | 
					      error: {
 | 
				
			||||||
 | 
					        message: err.message,
 | 
				
			||||||
 | 
					        name: err.name,
 | 
				
			||||||
 | 
					        stack: err.stack
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    // eslint-disable-next-line require-atomic-updates
 | 
				
			||||||
 | 
					    busy = false
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
							
								
								
									
										18
									
								
								server/libs/libarchive/wasm-libarchive.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								server/libs/libarchive/wasm-libarchive.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										235
									
								
								server/libs/libarchive/wasm-module.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										235
									
								
								server/libs/libarchive/wasm-module.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,235 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Modified from https://github.com/nika-begiashvili/libarchivejs
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Path = require('path')
 | 
				
			||||||
 | 
					const libarchive = require('./wasm-libarchive')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const TYPE_MAP = {
 | 
				
			||||||
 | 
					  32768: 'FILE',
 | 
				
			||||||
 | 
					  16384: 'DIR',
 | 
				
			||||||
 | 
					  40960: 'SYMBOLIC_LINK',
 | 
				
			||||||
 | 
					  49152: 'SOCKET',
 | 
				
			||||||
 | 
					  8192: 'CHARACTER_DEVICE',
 | 
				
			||||||
 | 
					  24576: 'BLOCK_DEVICE',
 | 
				
			||||||
 | 
					  4096: 'NAMED_PIPE',
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ArchiveReader {
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * archive reader
 | 
				
			||||||
 | 
					   * @param {WasmModule} wasmModule emscripten module 
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  constructor(wasmModule) {
 | 
				
			||||||
 | 
					    this._wasmModule = wasmModule
 | 
				
			||||||
 | 
					    this._runCode = wasmModule.runCode
 | 
				
			||||||
 | 
					    this._file = null
 | 
				
			||||||
 | 
					    this._passphrase = null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * open archive, needs to closed manually
 | 
				
			||||||
 | 
					   * @param {File} file 
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  open(file) {
 | 
				
			||||||
 | 
					    if (this._file !== null) {
 | 
				
			||||||
 | 
					      console.warn('Closing previous file')
 | 
				
			||||||
 | 
					      this.close()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const { promise, resolve, reject } = this._promiseHandles()
 | 
				
			||||||
 | 
					    this._file = file
 | 
				
			||||||
 | 
					    this._loadFile(file, resolve, reject)
 | 
				
			||||||
 | 
					    return promise
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * close archive
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  close() {
 | 
				
			||||||
 | 
					    this._runCode.closeArchive(this._archive)
 | 
				
			||||||
 | 
					    this._wasmModule._free(this._filePtr)
 | 
				
			||||||
 | 
					    this._file = null
 | 
				
			||||||
 | 
					    this._filePtr = null
 | 
				
			||||||
 | 
					    this._archive = null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * detect if archive has encrypted data
 | 
				
			||||||
 | 
					   * @returns {boolean|null} null if could not be determined
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  hasEncryptedData() {
 | 
				
			||||||
 | 
					    this._archive = this._runCode.openArchive(this._filePtr, this._fileLength, this._passphrase)
 | 
				
			||||||
 | 
					    this._runCode.getNextEntry(this._archive)
 | 
				
			||||||
 | 
					    const status = this._runCode.hasEncryptedEntries(this._archive)
 | 
				
			||||||
 | 
					    if (status === 0) {
 | 
				
			||||||
 | 
					      return false
 | 
				
			||||||
 | 
					    } else if (status > 0) {
 | 
				
			||||||
 | 
					      return true
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return null
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * set passphrase to be used with archive
 | 
				
			||||||
 | 
					   * @param {*} passphrase 
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  setPassphrase(passphrase) {
 | 
				
			||||||
 | 
					    this._passphrase = passphrase
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * get archive entries
 | 
				
			||||||
 | 
					   * @param {boolean} skipExtraction
 | 
				
			||||||
 | 
					   * @param {string} except don't skip this entry
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  *entries(skipExtraction = false, except = null) {
 | 
				
			||||||
 | 
					    this._archive = this._runCode.openArchive(this._filePtr, this._fileLength, this._passphrase)
 | 
				
			||||||
 | 
					    let entry
 | 
				
			||||||
 | 
					    while (true) {
 | 
				
			||||||
 | 
					      entry = this._runCode.getNextEntry(this._archive)
 | 
				
			||||||
 | 
					      if (entry === 0) break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const entryData = {
 | 
				
			||||||
 | 
					        size: this._runCode.getEntrySize(entry),
 | 
				
			||||||
 | 
					        path: this._runCode.getEntryName(entry),
 | 
				
			||||||
 | 
					        type: TYPE_MAP[this._runCode.getEntryType(entry)],
 | 
				
			||||||
 | 
					        ref: entry,
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (entryData.type === 'FILE') {
 | 
				
			||||||
 | 
					        let fileName = entryData.path.split('/')
 | 
				
			||||||
 | 
					        entryData.fileName = fileName[fileName.length - 1]
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (skipExtraction && except !== entryData.path) {
 | 
				
			||||||
 | 
					        this._runCode.skipEntry(this._archive)
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const ptr = this._runCode.getFileData(this._archive, entryData.size)
 | 
				
			||||||
 | 
					        if (ptr < 0) {
 | 
				
			||||||
 | 
					          throw new Error(this._runCode.getError(this._archive))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        entryData.fileData = this._wasmModule.HEAP8.slice(ptr, ptr + entryData.size)
 | 
				
			||||||
 | 
					        this._wasmModule._free(ptr)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      yield entryData
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  _loadFile(fileBuffer, resolve, reject) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const array = new Uint8Array(fileBuffer)
 | 
				
			||||||
 | 
					      this._fileLength = array.length
 | 
				
			||||||
 | 
					      this._filePtr = this._runCode.malloc(this._fileLength)
 | 
				
			||||||
 | 
					      this._wasmModule.HEAP8.set(array, this._filePtr)
 | 
				
			||||||
 | 
					      resolve()
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      reject(error)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  _promiseHandles() {
 | 
				
			||||||
 | 
					    let resolve = null, reject = null
 | 
				
			||||||
 | 
					    const promise = new Promise((_resolve, _reject) => {
 | 
				
			||||||
 | 
					      resolve = _resolve
 | 
				
			||||||
 | 
					      reject = _reject
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    return { promise, resolve, reject }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class WasmModule {
 | 
				
			||||||
 | 
					  constructor() {
 | 
				
			||||||
 | 
					    this.preRun = []
 | 
				
			||||||
 | 
					    this.postRun = []
 | 
				
			||||||
 | 
					    this.totalDependencies = 0
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  print(...text) {
 | 
				
			||||||
 | 
					    console.log(text)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  printErr(...text) {
 | 
				
			||||||
 | 
					    console.error(text)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  initFunctions() {
 | 
				
			||||||
 | 
					    this.runCode = {
 | 
				
			||||||
 | 
					      // const char * get_version()
 | 
				
			||||||
 | 
					      getVersion: this.cwrap('get_version', 'string', []),
 | 
				
			||||||
 | 
					      // void * archive_open( const void * buffer, size_t buffer_size)
 | 
				
			||||||
 | 
					      // retuns archive pointer
 | 
				
			||||||
 | 
					      openArchive: this.cwrap('archive_open', 'number', ['number', 'number', 'string']),
 | 
				
			||||||
 | 
					      // void * get_entry(void * archive)
 | 
				
			||||||
 | 
					      // return archive entry pointer
 | 
				
			||||||
 | 
					      getNextEntry: this.cwrap('get_next_entry', 'number', ['number']),
 | 
				
			||||||
 | 
					      // void * get_filedata( void * archive, size_t bufferSize )
 | 
				
			||||||
 | 
					      getFileData: this.cwrap('get_filedata', 'number', ['number', 'number']),
 | 
				
			||||||
 | 
					      // int archive_read_data_skip(struct archive *_a)
 | 
				
			||||||
 | 
					      skipEntry: this.cwrap('archive_read_data_skip', 'number', ['number']),
 | 
				
			||||||
 | 
					      // void archive_close( void * archive )
 | 
				
			||||||
 | 
					      closeArchive: this.cwrap('archive_close', null, ['number']),
 | 
				
			||||||
 | 
					      // la_int64_t archive_entry_size( struct archive_entry * )
 | 
				
			||||||
 | 
					      getEntrySize: this.cwrap('archive_entry_size', 'number', ['number']),
 | 
				
			||||||
 | 
					      // const char * archive_entry_pathname( struct archive_entry * )
 | 
				
			||||||
 | 
					      getEntryName: this.cwrap('archive_entry_pathname', 'string', ['number']),
 | 
				
			||||||
 | 
					      // __LA_MODE_T archive_entry_filetype( struct archive_entry * )
 | 
				
			||||||
 | 
					      /*
 | 
				
			||||||
 | 
					      #define AE_IFMT		((__LA_MODE_T)0170000)
 | 
				
			||||||
 | 
					      #define AE_IFREG	((__LA_MODE_T)0100000) // Regular file
 | 
				
			||||||
 | 
					      #define AE_IFLNK	((__LA_MODE_T)0120000) // Sybolic link
 | 
				
			||||||
 | 
					      #define AE_IFSOCK	((__LA_MODE_T)0140000) // Socket
 | 
				
			||||||
 | 
					      #define AE_IFCHR	((__LA_MODE_T)0020000) // Character device
 | 
				
			||||||
 | 
					      #define AE_IFBLK	((__LA_MODE_T)0060000) // Block device
 | 
				
			||||||
 | 
					      #define AE_IFDIR	((__LA_MODE_T)0040000) // Directory
 | 
				
			||||||
 | 
					      #define AE_IFIFO	((__LA_MODE_T)0010000) // Named pipe
 | 
				
			||||||
 | 
					      */
 | 
				
			||||||
 | 
					      getEntryType: this.cwrap('archive_entry_filetype', 'number', ['number']),
 | 
				
			||||||
 | 
					      // const char * archive_error_string(struct archive *); 
 | 
				
			||||||
 | 
					      getError: this.cwrap('archive_error_string', 'string', ['number']),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      /*
 | 
				
			||||||
 | 
					      * Returns 1 if the archive contains at least one encrypted entry.
 | 
				
			||||||
 | 
					      * If the archive format not support encryption at all
 | 
				
			||||||
 | 
					      * ARCHIVE_READ_FORMAT_ENCRYPTION_UNSUPPORTED is returned.
 | 
				
			||||||
 | 
					      * If for any other reason (e.g. not enough data read so far)
 | 
				
			||||||
 | 
					      * we cannot say whether there are encrypted entries, then
 | 
				
			||||||
 | 
					      * ARCHIVE_READ_FORMAT_ENCRYPTION_DONT_KNOW is returned.
 | 
				
			||||||
 | 
					      * In general, this function will return values below zero when the
 | 
				
			||||||
 | 
					      * reader is uncertain or totally incapable of encryption support.
 | 
				
			||||||
 | 
					      * When this function returns 0 you can be sure that the reader
 | 
				
			||||||
 | 
					      * supports encryption detection but no encrypted entries have
 | 
				
			||||||
 | 
					      * been found yet.
 | 
				
			||||||
 | 
					      *
 | 
				
			||||||
 | 
					      * NOTE: If the metadata/header of an archive is also encrypted, you
 | 
				
			||||||
 | 
					      * cannot rely on the number of encrypted entries. That is why this
 | 
				
			||||||
 | 
					      * function does not return the number of encrypted entries but#
 | 
				
			||||||
 | 
					      * just shows that there are some.
 | 
				
			||||||
 | 
					      */
 | 
				
			||||||
 | 
					      // __LA_DECL int	archive_read_has_encrypted_entries(struct archive *);
 | 
				
			||||||
 | 
					      entryIsEncrypted: this.cwrap('archive_entry_is_encrypted', 'number', ['number']),
 | 
				
			||||||
 | 
					      hasEncryptedEntries: this.cwrap('archive_read_has_encrypted_entries', 'number', ['number']),
 | 
				
			||||||
 | 
					      // __LA_DECL int archive_read_add_passphrase(struct archive *, const char *);
 | 
				
			||||||
 | 
					      addPassphrase: this.cwrap('archive_read_add_passphrase', 'number', ['number', 'string']),
 | 
				
			||||||
 | 
					      //this.stringToUTF(str), //
 | 
				
			||||||
 | 
					      string: (str) => this.allocate(this.intArrayFromString(str), 'i8', 0),
 | 
				
			||||||
 | 
					      malloc: this.cwrap('malloc', 'number', ['number']),
 | 
				
			||||||
 | 
					      free: this.cwrap('free', null, ['number']),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  monitorRunDependencies() { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  locateFile(path /* ,prefix */) {
 | 
				
			||||||
 | 
					    const wasmFilepath = Path.join(__dirname, `../../../client/dist/libarchive/wasm-gen/${path}`)
 | 
				
			||||||
 | 
					    return wasmFilepath
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports.getArchiveReader = (cb) => {
 | 
				
			||||||
 | 
					  libarchive(new WasmModule()).then((module) => {
 | 
				
			||||||
 | 
					    module.initFunctions()
 | 
				
			||||||
 | 
					    cb(new ArchiveReader(module))
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -681,7 +681,7 @@ class BookScanner {
 | 
				
			|||||||
        const bookTitle = this.bookMetadata.title || this.libraryItemData.mediaMetadata.title
 | 
					        const bookTitle = this.bookMetadata.title || this.libraryItemData.mediaMetadata.title
 | 
				
			||||||
        AudioFileScanner.setBookMetadataFromAudioMetaTags(bookTitle, this.audioFiles, this.bookMetadata, this.libraryScan)
 | 
					        AudioFileScanner.setBookMetadataFromAudioMetaTags(bookTitle, this.audioFiles, this.bookMetadata, this.libraryScan)
 | 
				
			||||||
      } else if (this.ebookFileScanData) {
 | 
					      } else if (this.ebookFileScanData) {
 | 
				
			||||||
        const ebookMetdataObject = this.ebookFileScanData.metadata
 | 
					        const ebookMetdataObject = this.ebookFileScanData.metadata || {}
 | 
				
			||||||
        for (const key in ebookMetdataObject) {
 | 
					        for (const key in ebookMetdataObject) {
 | 
				
			||||||
          if (key === 'tags') {
 | 
					          if (key === 'tags') {
 | 
				
			||||||
            if (ebookMetdataObject.tags.length) {
 | 
					            if (ebookMetdataObject.tags.length) {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										35
									
								
								server/utils/parsers/parseComicInfoMetadata.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								server/utils/parsers/parseComicInfoMetadata.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * TODO: Add more fields
 | 
				
			||||||
 | 
					 * @see https://anansi-project.github.io/docs/comicinfo/intro
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * @param {Object} comicInfoJson 
 | 
				
			||||||
 | 
					 * @returns {import('../../scanner/BookScanner').BookMetadataObject}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					module.exports.parse = (comicInfoJson) => {
 | 
				
			||||||
 | 
					  if (!comicInfoJson?.ComicInfo) return null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const ComicSeries = comicInfoJson.ComicInfo.Series?.[0]?.trim() || null
 | 
				
			||||||
 | 
					  const ComicNumber = comicInfoJson.ComicInfo.Number?.[0]?.trim() || null
 | 
				
			||||||
 | 
					  const ComicSummary = comicInfoJson.ComicInfo.Summary?.[0]?.trim() || null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let title = null
 | 
				
			||||||
 | 
					  const series = []
 | 
				
			||||||
 | 
					  if (ComicSeries) {
 | 
				
			||||||
 | 
					    series.push({
 | 
				
			||||||
 | 
					      name: ComicSeries,
 | 
				
			||||||
 | 
					      sequence: ComicNumber
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    title = ComicSeries
 | 
				
			||||||
 | 
					    if (ComicNumber) {
 | 
				
			||||||
 | 
					      title += ` ${ComicNumber}`
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    title,
 | 
				
			||||||
 | 
					    series,
 | 
				
			||||||
 | 
					    description: ComicSummary
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										109
									
								
								server/utils/parsers/parseComicMetadata.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								server/utils/parsers/parseComicMetadata.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,109 @@
 | 
				
			|||||||
 | 
					const Path = require('path')
 | 
				
			||||||
 | 
					const globals = require('../globals')
 | 
				
			||||||
 | 
					const fs = require('../../libs/fsExtra')
 | 
				
			||||||
 | 
					const Logger = require('../../Logger')
 | 
				
			||||||
 | 
					const Archive = require('../../libs/libarchive/archive')
 | 
				
			||||||
 | 
					const { xmlToJSON } = require('../index')
 | 
				
			||||||
 | 
					const parseComicInfoMetadata = require('./parseComicInfoMetadata')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * @param {string} filepath 
 | 
				
			||||||
 | 
					 * @returns {Promise<Buffer>}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					async function getComicFileBuffer(filepath) {
 | 
				
			||||||
 | 
					  if (!await fs.pathExists(filepath)) {
 | 
				
			||||||
 | 
					    Logger.error(`Comic path does not exist "${filepath}"`)
 | 
				
			||||||
 | 
					    return null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    return fs.readFile(filepath)
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    Logger.error(`Failed to read comic at "${filepath}"`, error)
 | 
				
			||||||
 | 
					    return null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Extract cover image from comic return true if success
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * @param {string} comicPath 
 | 
				
			||||||
 | 
					 * @param {string} comicImageFilepath 
 | 
				
			||||||
 | 
					 * @param {string} outputCoverPath 
 | 
				
			||||||
 | 
					 * @returns {Promise<boolean>}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					async function extractCoverImage(comicPath, comicImageFilepath, outputCoverPath) {
 | 
				
			||||||
 | 
					  const comicFileBuffer = await getComicFileBuffer(comicPath)
 | 
				
			||||||
 | 
					  if (!comicFileBuffer) return null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const archive = await Archive.open(comicFileBuffer)
 | 
				
			||||||
 | 
					  const fileEntry = await archive.extractSingleFile(comicImageFilepath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!fileEntry?.fileData) {
 | 
				
			||||||
 | 
					    Logger.error(`[parseComicMetadata] Invalid file entry data for comicPath "${comicPath}"/${comicImageFilepath}`)
 | 
				
			||||||
 | 
					    return false
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    await fs.writeFile(outputCoverPath, fileEntry.fileData)
 | 
				
			||||||
 | 
					    return true
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    Logger.error(`[parseComicMetadata] Failed to extract image from comicPath "${comicPath}"`, error)
 | 
				
			||||||
 | 
					    return false
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					module.exports.extractCoverImage = extractCoverImage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Parse metadata from comic
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * @param {import('../../models/Book').EBookFileObject} ebookFile 
 | 
				
			||||||
 | 
					 * @returns {Promise<import('./parseEbookMetadata').EBookFileScanData>}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					async function parse(ebookFile) {
 | 
				
			||||||
 | 
					  const comicPath = ebookFile.metadata.path
 | 
				
			||||||
 | 
					  Logger.debug(`Parsing metadata from comic at "${comicPath}"`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const comicFileBuffer = await getComicFileBuffer(comicPath)
 | 
				
			||||||
 | 
					  if (!comicFileBuffer) return null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const archive = await Archive.open(comicFileBuffer)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fileObjects = await archive.getFilesArray()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  fileObjects.sort((a, b) => {
 | 
				
			||||||
 | 
					    return a.file.name.localeCompare(b.file.name, undefined, {
 | 
				
			||||||
 | 
					      numeric: true,
 | 
				
			||||||
 | 
					      sensitivity: 'base'
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let metadata = null
 | 
				
			||||||
 | 
					  const comicInfo = fileObjects.find(fo => fo.file.name === 'ComicInfo.xml')
 | 
				
			||||||
 | 
					  if (comicInfo) {
 | 
				
			||||||
 | 
					    const comicInfoEntry = await comicInfo.file.extract()
 | 
				
			||||||
 | 
					    if (comicInfoEntry?.fileData) {
 | 
				
			||||||
 | 
					      const comicInfoStr = new TextDecoder().decode(comicInfoEntry.fileData)
 | 
				
			||||||
 | 
					      const comicInfoJson = await xmlToJSON(comicInfoStr)
 | 
				
			||||||
 | 
					      if (comicInfoJson) {
 | 
				
			||||||
 | 
					        metadata = parseComicInfoMetadata.parse(comicInfoJson)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const payload = {
 | 
				
			||||||
 | 
					    path: comicPath,
 | 
				
			||||||
 | 
					    ebookFormat: ebookFile.ebookFormat,
 | 
				
			||||||
 | 
					    metadata
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const firstImage = fileObjects.find(fo => globals.SupportedImageTypes.includes(Path.extname(fo.file.name).toLowerCase().slice(1)))
 | 
				
			||||||
 | 
					  if (firstImage?.file?._path) {
 | 
				
			||||||
 | 
					    payload.ebookCoverPath = firstImage.file._path
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    Logger.warn(`Cover image not found in comic at "${comicPath}"`)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return payload
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					module.exports.parse = parse
 | 
				
			||||||
@ -1,4 +1,5 @@
 | 
				
			|||||||
const parseEpubMetadata = require('./parseEpubMetadata')
 | 
					const parseEpubMetadata = require('./parseEpubMetadata')
 | 
				
			||||||
 | 
					const parseComicMetadata = require('./parseComicMetadata')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @typedef EBookFileScanData
 | 
					 * @typedef EBookFileScanData
 | 
				
			||||||
@ -18,7 +19,9 @@ async function parse(ebookFile) {
 | 
				
			|||||||
  if (!ebookFile) return null
 | 
					  if (!ebookFile) return null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (ebookFile.ebookFormat === 'epub') {
 | 
					  if (ebookFile.ebookFormat === 'epub') {
 | 
				
			||||||
    return parseEpubMetadata.parse(ebookFile.metadata.path)
 | 
					    return parseEpubMetadata.parse(ebookFile)
 | 
				
			||||||
 | 
					  } else if (['cbz', 'cbr'].includes(ebookFile.ebookFormat)) {
 | 
				
			||||||
 | 
					    return parseComicMetadata.parse(ebookFile)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return null
 | 
					  return null
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -36,6 +39,8 @@ async function extractCoverImage(ebookFileScanData, outputCoverPath) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  if (ebookFileScanData.ebookFormat === 'epub') {
 | 
					  if (ebookFileScanData.ebookFormat === 'epub') {
 | 
				
			||||||
    return parseEpubMetadata.extractCoverImage(ebookFileScanData.path, ebookFileScanData.ebookCoverPath, outputCoverPath)
 | 
					    return parseEpubMetadata.extractCoverImage(ebookFileScanData.path, ebookFileScanData.ebookCoverPath, outputCoverPath)
 | 
				
			||||||
 | 
					  } else if (['cbz', 'cbr'].includes(ebookFileScanData.ebookFormat)) {
 | 
				
			||||||
 | 
					    return parseComicMetadata.extractCoverImage(ebookFileScanData.path, ebookFileScanData.ebookCoverPath, outputCoverPath)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return false
 | 
					  return false
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -60,10 +60,11 @@ module.exports.extractCoverImage = extractCoverImage
 | 
				
			|||||||
/**
 | 
					/**
 | 
				
			||||||
 * Parse metadata from epub
 | 
					 * Parse metadata from epub
 | 
				
			||||||
 * 
 | 
					 * 
 | 
				
			||||||
 * @param {string} epubPath 
 | 
					 * @param {import('../../models/Book').EBookFileObject} ebookFile 
 | 
				
			||||||
 * @returns {Promise<import('./parseEbookMetadata').EBookFileScanData>}
 | 
					 * @returns {Promise<import('./parseEbookMetadata').EBookFileScanData>}
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
async function parse(epubPath) {
 | 
					async function parse(ebookFile) {
 | 
				
			||||||
 | 
					  const epubPath = ebookFile.metadata.path
 | 
				
			||||||
  Logger.debug(`Parsing metadata from epub at "${epubPath}"`)
 | 
					  Logger.debug(`Parsing metadata from epub at "${epubPath}"`)
 | 
				
			||||||
  // Entrypoint of the epub that contains the filepath to the package document (opf file)
 | 
					  // Entrypoint of the epub that contains the filepath to the package document (opf file)
 | 
				
			||||||
  const containerJson = await extractXmlToJson(epubPath, 'META-INF/container.xml')
 | 
					  const containerJson = await extractXmlToJson(epubPath, 'META-INF/container.xml')
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user