mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-31 02:17:01 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			208 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			208 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| const Path = require('path')
 | |
| const os = require('os')
 | |
| const unrar = require('node-unrar-js')
 | |
| const Logger = require('../Logger')
 | |
| const fs = require('../libs/fsExtra')
 | |
| const StreamZip = require('../libs/nodeStreamZip')
 | |
| const Archive = require('../libs/libarchive/archive')
 | |
| const { isWritable } = require('./fileUtils')
 | |
| 
 | |
| class AbstractComicBookExtractor {
 | |
|   constructor(comicPath) {
 | |
|     this.comicPath = comicPath
 | |
|   }
 | |
| 
 | |
|   async getBuffer() {
 | |
|     if (!(await fs.pathExists(this.comicPath))) {
 | |
|       Logger.error(`[parseComicMetadata] Comic path does not exist "${this.comicPath}"`)
 | |
|       return null
 | |
|     }
 | |
|     try {
 | |
|       return fs.readFile(this.comicPath)
 | |
|     } catch (error) {
 | |
|       Logger.error(`[parseComicMetadata] Failed to read comic at "${this.comicPath}"`, error)
 | |
|       return null
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   async open() {
 | |
|     throw new Error('Not implemented')
 | |
|   }
 | |
| 
 | |
|   async getFilePaths() {
 | |
|     throw new Error('Not implemented')
 | |
|   }
 | |
| 
 | |
|   async extractToFile(filePath, outputFilePath) {
 | |
|     throw new Error('Not implemented')
 | |
|   }
 | |
| 
 | |
|   async extractToBuffer(filePath) {
 | |
|     throw new Error('Not implemented')
 | |
|   }
 | |
| 
 | |
|   close() {
 | |
|     throw new Error('Not implemented')
 | |
|   }
 | |
| }
 | |
| 
 | |
| class CbrComicBookExtractor extends AbstractComicBookExtractor {
 | |
|   constructor(comicPath) {
 | |
|     super(comicPath)
 | |
|     this.archive = null
 | |
|     this.tmpDir = null
 | |
|   }
 | |
| 
 | |
|   async open() {
 | |
|     this.tmpDir = global.MetadataPath ? Path.join(global.MetadataPath, 'tmp') : os.tmpdir()
 | |
|     await fs.ensureDir(this.tmpDir)
 | |
|     if (!(await isWritable(this.tmpDir))) throw new Error(`[CbrComicBookExtractor] Temp directory "${this.tmpDir}" is not writable`)
 | |
|     this.archive = await unrar.createExtractorFromFile({ filepath: this.comicPath, targetPath: this.tmpDir })
 | |
|     Logger.debug(`[CbrComicBookExtractor] Opened comic book "${this.comicPath}". Using temp directory "${this.tmpDir}" for extraction.`)
 | |
|   }
 | |
| 
 | |
|   async getFilePaths() {
 | |
|     if (!this.archive) return null
 | |
|     const list = this.archive.getFileList()
 | |
|     const fileHeaders = [...list.fileHeaders]
 | |
|     const filePaths = fileHeaders.filter((fh) => !fh.flags.directory).map((fh) => fh.name)
 | |
|     Logger.debug(`[CbrComicBookExtractor] Found ${filePaths.length} files in comic book "${this.comicPath}"`)
 | |
|     return filePaths
 | |
|   }
 | |
| 
 | |
|   async removeEmptyParentDirs(file) {
 | |
|     let dir = Path.dirname(file)
 | |
|     while (dir !== '.') {
 | |
|       const fullDirPath = Path.join(this.tmpDir, dir)
 | |
|       const files = await fs.readdir(fullDirPath)
 | |
|       if (files.length > 0) break
 | |
|       await fs.remove(fullDirPath)
 | |
|       dir = Path.dirname(dir)
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   async extractToBuffer(file) {
 | |
|     if (!this.archive) return null
 | |
|     const extracted = this.archive.extract({ files: [file] })
 | |
|     const files = [...extracted.files]
 | |
|     const filePath = Path.join(this.tmpDir, files[0].fileHeader.name)
 | |
|     const fileData = await fs.readFile(filePath)
 | |
|     await fs.remove(filePath)
 | |
|     await this.removeEmptyParentDirs(files[0].fileHeader.name)
 | |
|     Logger.debug(`[CbrComicBookExtractor] Extracted file "${file}" from comic book "${this.comicPath}" to buffer, size: ${fileData.length}`)
 | |
|     return fileData
 | |
|   }
 | |
| 
 | |
|   async extractToFile(file, outputFilePath) {
 | |
|     if (!this.archive) return false
 | |
|     const extracted = this.archive.extract({ files: [file] })
 | |
|     const files = [...extracted.files]
 | |
|     const extractedFilePath = Path.join(this.tmpDir, files[0].fileHeader.name)
 | |
|     await fs.move(extractedFilePath, outputFilePath, { overwrite: true })
 | |
|     await this.removeEmptyParentDirs(files[0].fileHeader.name)
 | |
|     Logger.debug(`[CbrComicBookExtractor] Extracted file "${file}" from comic book "${this.comicPath}" to "${outputFilePath}"`)
 | |
|     return true
 | |
|   }
 | |
| 
 | |
|   close() {
 | |
|     Logger.debug(`[CbrComicBookExtractor] Closed comic book "${this.comicPath}"`)
 | |
|   }
 | |
| }
 | |
| 
 | |
| class CbzComicBookExtractor extends AbstractComicBookExtractor {
 | |
|   constructor(comicPath) {
 | |
|     super(comicPath)
 | |
|     this.archive = null
 | |
|   }
 | |
| 
 | |
|   async open() {
 | |
|     const buffer = await this.getBuffer()
 | |
|     this.archive = await Archive.open(buffer)
 | |
|     Logger.debug(`[CbzComicBookExtractor] Opened comic book "${this.comicPath}"`)
 | |
|   }
 | |
| 
 | |
|   async getFilePaths() {
 | |
|     if (!this.archive) return null
 | |
|     const list = await this.archive.getFilesArray()
 | |
|     const fileNames = list.map((fo) => fo.file._path)
 | |
|     Logger.debug(`[CbzComicBookExtractor] Found ${fileNames.length} files in comic book "${this.comicPath}"`)
 | |
|     return fileNames
 | |
|   }
 | |
| 
 | |
|   async extractToBuffer(file) {
 | |
|     if (!this.archive) return null
 | |
|     const extracted = await this.archive.extractSingleFile(file)
 | |
|     Logger.debug(`[CbzComicBookExtractor] Extracted file "${file}" from comic book "${this.comicPath}" to buffer, size: ${extracted?.fileData.length}`)
 | |
|     return extracted?.fileData
 | |
|   }
 | |
| 
 | |
|   async extractToFile(file, outputFilePath) {
 | |
|     const data = await this.extractToBuffer(file)
 | |
|     if (!data) return false
 | |
|     await fs.writeFile(outputFilePath, data)
 | |
|     Logger.debug(`[CbzComicBookExtractor] Extracted file "${file}" from comic book "${this.comicPath}" to "${outputFilePath}"`)
 | |
|     return true
 | |
|   }
 | |
| 
 | |
|   close() {
 | |
|     this.archive?.close()
 | |
|     Logger.debug(`[CbzComicBookExtractor] Closed comic book "${this.comicPath}"`)
 | |
|   }
 | |
| }
 | |
| 
 | |
| class CbzStreamZipComicBookExtractor extends AbstractComicBookExtractor {
 | |
|   constructor(comicPath) {
 | |
|     super(comicPath)
 | |
|     this.archive = null
 | |
|   }
 | |
| 
 | |
|   async open() {
 | |
|     this.archive = new StreamZip.async({ file: this.comicPath })
 | |
|     Logger.debug(`[CbzStreamZipComicBookExtractor] Opened comic book "${this.comicPath}"`)
 | |
|   }
 | |
| 
 | |
|   async getFilePaths() {
 | |
|     if (!this.archive) return null
 | |
|     const entries = await this.archive.entries()
 | |
|     const fileNames = Object.keys(entries).filter((entry) => !entries[entry].isDirectory)
 | |
|     Logger.debug(`[CbzStreamZipComicBookExtractor] Found ${fileNames.length} files in comic book "${this.comicPath}"`)
 | |
|     return fileNames
 | |
|   }
 | |
| 
 | |
|   async extractToBuffer(file) {
 | |
|     if (!this.archive) return null
 | |
|     const extracted = await this.archive?.entryData(file)
 | |
|     Logger.debug(`[CbzStreamZipComicBookExtractor] Extracted file "${file}" from comic book "${this.comicPath}" to buffer, size: ${extracted.length}`)
 | |
|     return extracted
 | |
|   }
 | |
| 
 | |
|   async extractToFile(file, outputFilePath) {
 | |
|     if (!this.archive) return false
 | |
|     try {
 | |
|       await this.archive.extract(file, outputFilePath)
 | |
|       Logger.debug(`[CbzStreamZipComicBookExtractor] Extracted file "${file}" from comic book "${this.comicPath}" to "${outputFilePath}"`)
 | |
|       return true
 | |
|     } catch (error) {
 | |
|       Logger.error(`[CbzStreamZipComicBookExtractor] Failed to extract file "${file}" to "${outputFilePath}"`, error)
 | |
|       return false
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   close() {
 | |
|     this.archive?.close()
 | |
|     Logger.debug(`[CbzStreamZipComicBookExtractor] Closed comic book "${this.comicPath}"`)
 | |
|   }
 | |
| }
 | |
| 
 | |
| function createComicBookExtractor(comicPath) {
 | |
|   const ext = Path.extname(comicPath).toLowerCase()
 | |
|   if (ext === '.cbr') {
 | |
|     return new CbrComicBookExtractor(comicPath)
 | |
|   } else if (ext === '.cbz') {
 | |
|     return new CbzStreamZipComicBookExtractor(comicPath)
 | |
|   } else {
 | |
|     throw new Error(`Unsupported comic book format "${ext}"`)
 | |
|   }
 | |
| }
 | |
| module.exports = { createComicBookExtractor }
 |