-
Notifications
You must be signed in to change notification settings - Fork 520
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3435 from mikiher/comic-book-extractors
Move to node-unrar-js for cbr and node-stream-zip for cbz
- Loading branch information
Showing
4 changed files
with
238 additions
and
53 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,207 @@ | ||
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 } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters