const fs = require('fs-extra') const Path = require('path') const axios = require('axios') const Logger = require('./Logger') const readChunk = require('read-chunk') const imageType = require('image-type') const globals = require('./utils/globals') const { CoverDestination } = require('./utils/constants') const { downloadFile } = require('./utils/fileUtils') class CoverController { constructor(db, MetadataPath, AudiobookPath) { this.db = db this.MetadataPath = MetadataPath.replace(/\\/g, '/') this.BookMetadataPath = Path.posix.join(this.MetadataPath, 'books') this.AudiobookPath = AudiobookPath } getCoverDirectory(audiobook) { if (this.db.serverSettings.coverDestination === CoverDestination.AUDIOBOOK) { return { fullPath: audiobook.fullPath, relPath: '/s/book/' + audiobook.id } } else { return { fullPath: Path.posix.join(this.BookMetadataPath, audiobook.id), relPath: Path.posix.join('/metadata', 'books', audiobook.id) } } } getFilesInDirectory(dir) { try { return fs.readdir(dir) } catch (error) { Logger.error(`[CoverController] Failed to get files in dir ${dir}`, error) return [] } } removeFile(filepath) { try { return fs.pathExists(filepath).then((exists) => { if (!exists) Logger.warn(`[CoverController] Attempting to remove file that does not exist ${filepath}`) return exists ? fs.unlink(filepath) : false }) } catch (error) { Logger.error(`[CoverController] Failed to remove file "${filepath}"`, error) return false } } // Remove covers that dont have the same filename as the new cover async removeOldCovers(dirpath, newCoverExt) { var filesInDir = await this.getFilesInDirectory(dirpath) for (let i = 0; i < filesInDir.length; i++) { var file = filesInDir[i] var _extname = Path.extname(file) var _filename = Path.basename(file, _extname) if (_filename === 'cover' && _extname !== newCoverExt) { var filepath = Path.join(dirpath, file) Logger.debug(`[CoverController] Removing old cover from metadata "${filepath}"`) await this.removeFile(filepath) } } } async checkFileIsValidImage(imagepath) { const buffer = await readChunk(imagepath, 0, 12) const imgType = imageType(buffer) if (!imgType) { await this.removeFile(imagepath) return { error: 'Invalid image' } } if (!globals.SupportedImageTypes.includes(imgType.ext)) { await this.removeFile(imagepath) return { error: `Invalid image type ${imgType.ext} (Supported: ${globals.SupportedImageTypes.join(',')})` } } return imgType } async uploadCover(audiobook, coverFile) { var extname = Path.extname(coverFile.name.toLowerCase()) if (!extname || !globals.SupportedImageTypes.includes(extname.slice(1))) { return { error: `Invalid image type ${extname} (Supported: ${globals.SupportedImageTypes.join(',')})` } } var { fullPath, relPath } = this.getCoverDirectory(audiobook) await fs.ensureDir(fullPath) var coverFilename = `cover${extname}` var coverFullPath = Path.posix.join(fullPath, coverFilename) var coverPath = Path.posix.join(relPath, coverFilename) // Move cover from temp upload dir to destination var success = await coverFile.mv(coverFullPath).then(() => true).catch((error) => { Logger.error('[CoverController] Failed to move cover file', path, error) return false }) if (!success) { return { error: 'Failed to move cover into destination' } } await this.removeOldCovers(fullPath, extname) Logger.info(`[CoverController] Uploaded audiobook cover "${coverPath}" for "${audiobook.title}"`) audiobook.updateBookCover(coverPath, coverFullPath) return { cover: coverPath } } async downloadCoverFromUrl(audiobook, url) { try { var { fullPath, relPath } = this.getCoverDirectory(audiobook) await fs.ensureDir(fullPath) var temppath = Path.posix.join(fullPath, 'cover') var success = await downloadFile(url, temppath).then(() => true).catch((err) => { Logger.error(`[CoverController] Download image file failed for "${url}"`, err) return false }) if (!success) { return { error: 'Failed to download image from url' } } var imgtype = await this.checkFileIsValidImage(temppath) if (imgtype.error) { return imgtype } var coverFilename = `cover.${imgtype.ext}` var coverPath = Path.posix.join(relPath, coverFilename) var coverFullPath = Path.posix.join(fullPath, coverFilename) await fs.rename(temppath, coverFullPath) await this.removeOldCovers(fullPath, '.' + imgtype.ext) Logger.info(`[CoverController] Downloaded audiobook cover "${coverPath}" from url "${url}" for "${audiobook.title}"`) audiobook.updateBookCover(coverPath, coverFullPath) return { cover: coverPath } } catch (error) { Logger.error(`[CoverController] Fetch cover image from url "${url}" failed`, error) return { error: 'Failed to fetch image from url' } } } } module.exports = CoverController