mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-11-07 16:44:16 +01:00
364 lines
12 KiB
JavaScript
364 lines
12 KiB
JavaScript
const fs = require('../libs/fsExtra')
|
|
const Path = require('path')
|
|
const Logger = require('../Logger')
|
|
const readChunk = require('../libs/readChunk')
|
|
const imageType = require('../libs/imageType')
|
|
|
|
const globals = require('../utils/globals')
|
|
const { downloadImageFile, filePathToPOSIX, checkPathIsFile } = require('../utils/fileUtils')
|
|
const { extractCoverArt } = require('../utils/ffmpegHelpers')
|
|
const parseEbookMetadata = require('../utils/parsers/parseEbookMetadata')
|
|
|
|
const CacheManager = require('../managers/CacheManager')
|
|
|
|
class CoverManager {
|
|
constructor() { }
|
|
|
|
getCoverDirectory(libraryItem) {
|
|
if (global.ServerSettings.storeCoverWithItem && !libraryItem.isFile) {
|
|
return libraryItem.path
|
|
} else {
|
|
return Path.posix.join(Path.posix.join(global.MetadataPath, 'items'), libraryItem.id)
|
|
}
|
|
}
|
|
|
|
getFilesInDirectory(dir) {
|
|
try {
|
|
return fs.readdir(dir)
|
|
} catch (error) {
|
|
Logger.error(`[CoverManager] Failed to get files in dir ${dir}`, error)
|
|
return []
|
|
}
|
|
}
|
|
|
|
removeFile(filepath) {
|
|
try {
|
|
return fs.pathExists(filepath).then((exists) => {
|
|
if (!exists) Logger.warn(`[CoverManager] Attempting to remove file that does not exist ${filepath}`)
|
|
return exists ? fs.unlink(filepath) : false
|
|
})
|
|
} catch (error) {
|
|
Logger.error(`[CoverManager] 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)
|
|
|
|
const imageExtensions = ['.jpeg', '.jpg', '.png', '.webp', '.jiff']
|
|
for (let i = 0; i < filesInDir.length; i++) {
|
|
var file = filesInDir[i]
|
|
var _extname = Path.extname(file).toLowerCase()
|
|
var _filename = Path.basename(file, _extname).toLowerCase()
|
|
if (_filename === 'cover' && _extname !== newCoverExt && imageExtensions.includes(_extname)) {
|
|
var filepath = Path.join(dirpath, file)
|
|
Logger.debug(`[CoverManager] Removing old cover from metadata "${filepath}"`)
|
|
await this.removeFile(filepath)
|
|
}
|
|
}
|
|
}
|
|
|
|
async checkFileIsValidImage(imagepath, removeOnInvalid = false) {
|
|
const buffer = await readChunk(imagepath, 0, 12)
|
|
const imgType = imageType(buffer)
|
|
if (!imgType) {
|
|
if (removeOnInvalid) await this.removeFile(imagepath)
|
|
return {
|
|
error: 'Invalid image'
|
|
}
|
|
}
|
|
|
|
if (!globals.SupportedImageTypes.includes(imgType.ext)) {
|
|
if (removeOnInvalid) await this.removeFile(imagepath)
|
|
return {
|
|
error: `Invalid image type ${imgType.ext} (Supported: ${globals.SupportedImageTypes.join(',')})`
|
|
}
|
|
}
|
|
return imgType
|
|
}
|
|
|
|
async uploadCover(libraryItem, coverFile) {
|
|
const extname = Path.extname(coverFile.name.toLowerCase())
|
|
if (!extname || !globals.SupportedImageTypes.includes(extname.slice(1))) {
|
|
return {
|
|
error: `Invalid image type ${extname} (Supported: ${globals.SupportedImageTypes.join(',')})`
|
|
}
|
|
}
|
|
|
|
const coverDirPath = this.getCoverDirectory(libraryItem)
|
|
await fs.ensureDir(coverDirPath)
|
|
|
|
const coverFullPath = Path.posix.join(coverDirPath, `cover${extname}`)
|
|
|
|
// Move cover from temp upload dir to destination
|
|
const success = await coverFile.mv(coverFullPath).then(() => true).catch((error) => {
|
|
Logger.error('[CoverManager] Failed to move cover file', path, error)
|
|
return false
|
|
})
|
|
|
|
if (!success) {
|
|
return {
|
|
error: 'Failed to move cover into destination'
|
|
}
|
|
}
|
|
|
|
await this.removeOldCovers(coverDirPath, extname)
|
|
await CacheManager.purgeCoverCache(libraryItem.id)
|
|
|
|
Logger.info(`[CoverManager] Uploaded libraryItem cover "${coverFullPath}" for "${libraryItem.media.metadata.title}"`)
|
|
|
|
libraryItem.updateMediaCover(coverFullPath)
|
|
return {
|
|
cover: coverFullPath
|
|
}
|
|
}
|
|
|
|
async downloadCoverFromUrl(libraryItem, url, forceLibraryItemFolder = false) {
|
|
try {
|
|
// Force save cover with library item is used for adding new podcasts
|
|
var coverDirPath = forceLibraryItemFolder ? libraryItem.path : this.getCoverDirectory(libraryItem)
|
|
await fs.ensureDir(coverDirPath)
|
|
|
|
var temppath = Path.posix.join(coverDirPath, 'cover')
|
|
|
|
let errorMsg = ''
|
|
let success = await downloadImageFile(url, temppath).then(() => true).catch((err) => {
|
|
errorMsg = err.message || 'Unknown error'
|
|
Logger.error(`[CoverManager] Download image file failed for "${url}"`, errorMsg)
|
|
return false
|
|
})
|
|
if (!success) {
|
|
return {
|
|
error: 'Failed to download image from url: ' + errorMsg
|
|
}
|
|
}
|
|
|
|
var imgtype = await this.checkFileIsValidImage(temppath, true)
|
|
|
|
if (imgtype.error) {
|
|
return imgtype
|
|
}
|
|
|
|
var coverFilename = `cover.${imgtype.ext}`
|
|
var coverFullPath = Path.posix.join(coverDirPath, coverFilename)
|
|
await fs.rename(temppath, coverFullPath)
|
|
|
|
await this.removeOldCovers(coverDirPath, '.' + imgtype.ext)
|
|
await CacheManager.purgeCoverCache(libraryItem.id)
|
|
|
|
Logger.info(`[CoverManager] Downloaded libraryItem cover "${coverFullPath}" from url "${url}" for "${libraryItem.media.metadata.title}"`)
|
|
libraryItem.updateMediaCover(coverFullPath)
|
|
return {
|
|
cover: coverFullPath
|
|
}
|
|
} catch (error) {
|
|
Logger.error(`[CoverManager] Fetch cover image from url "${url}" failed`, error)
|
|
return {
|
|
error: 'Failed to fetch image from url'
|
|
}
|
|
}
|
|
}
|
|
|
|
async validateCoverPath(coverPath, libraryItem) {
|
|
// Invalid cover path
|
|
if (!coverPath || coverPath.startsWith('http:') || coverPath.startsWith('https:')) {
|
|
Logger.error(`[CoverManager] validate cover path invalid http url "${coverPath}"`)
|
|
return {
|
|
error: 'Invalid cover path'
|
|
}
|
|
}
|
|
coverPath = filePathToPOSIX(coverPath)
|
|
// Cover path already set on media
|
|
if (libraryItem.media.coverPath == coverPath) {
|
|
Logger.debug(`[CoverManager] validate cover path already set "${coverPath}"`)
|
|
return {
|
|
cover: coverPath,
|
|
updated: false
|
|
}
|
|
}
|
|
|
|
// Cover path does not exist
|
|
if (!await fs.pathExists(coverPath)) {
|
|
Logger.error(`[CoverManager] validate cover path does not exist "${coverPath}"`)
|
|
return {
|
|
error: 'Cover path does not exist'
|
|
}
|
|
}
|
|
|
|
// Cover path is not a file
|
|
if (!await checkPathIsFile(coverPath)) {
|
|
Logger.error(`[CoverManager] validate cover path is not a file "${coverPath}"`)
|
|
return {
|
|
error: 'Cover path is not a file'
|
|
}
|
|
}
|
|
|
|
// Check valid image at path
|
|
var imgtype = await this.checkFileIsValidImage(coverPath, false)
|
|
if (imgtype.error) {
|
|
return imgtype
|
|
}
|
|
|
|
var coverDirPath = this.getCoverDirectory(libraryItem)
|
|
|
|
// Cover path is not in correct directory - make a copy
|
|
if (!coverPath.startsWith(coverDirPath)) {
|
|
await fs.ensureDir(coverDirPath)
|
|
|
|
var coverFilename = `cover.${imgtype.ext}`
|
|
var newCoverPath = Path.posix.join(coverDirPath, coverFilename)
|
|
Logger.debug(`[CoverManager] validate cover path copy cover from "${coverPath}" to "${newCoverPath}"`)
|
|
|
|
var copySuccess = await fs.copy(coverPath, newCoverPath, { overwrite: true }).then(() => true).catch((error) => {
|
|
Logger.error(`[CoverManager] validate cover path failed to copy cover`, error)
|
|
return false
|
|
})
|
|
if (!copySuccess) {
|
|
return {
|
|
error: 'Failed to copy cover to dir'
|
|
}
|
|
}
|
|
await this.removeOldCovers(coverDirPath, '.' + imgtype.ext)
|
|
Logger.debug(`[CoverManager] cover copy success`)
|
|
coverPath = newCoverPath
|
|
}
|
|
|
|
await CacheManager.purgeCoverCache(libraryItem.id)
|
|
|
|
libraryItem.updateMediaCover(coverPath)
|
|
return {
|
|
cover: coverPath,
|
|
updated: true
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract cover art from audio file and save for library item
|
|
*
|
|
* @param {import('../models/Book').AudioFileObject[]} audioFiles
|
|
* @param {string} libraryItemId
|
|
* @param {string} [libraryItemPath] null for isFile library items
|
|
* @returns {Promise<string>} returns cover path
|
|
*/
|
|
async saveEmbeddedCoverArt(audioFiles, libraryItemId, libraryItemPath) {
|
|
let audioFileWithCover = audioFiles.find(af => af.embeddedCoverArt)
|
|
if (!audioFileWithCover) return null
|
|
|
|
let coverDirPath = null
|
|
if (global.ServerSettings.storeCoverWithItem && libraryItemPath) {
|
|
coverDirPath = libraryItemPath
|
|
} else {
|
|
coverDirPath = Path.posix.join(global.MetadataPath, 'items', libraryItemId)
|
|
}
|
|
await fs.ensureDir(coverDirPath)
|
|
|
|
const coverFilename = audioFileWithCover.embeddedCoverArt === 'png' ? 'cover.png' : 'cover.jpg'
|
|
const coverFilePath = Path.join(coverDirPath, coverFilename)
|
|
|
|
const coverAlreadyExists = await fs.pathExists(coverFilePath)
|
|
if (coverAlreadyExists) {
|
|
Logger.warn(`[CoverManager] Extract embedded cover art but cover already exists for "${coverFilePath}" - bail`)
|
|
return null
|
|
}
|
|
|
|
const success = await extractCoverArt(audioFileWithCover.metadata.path, coverFilePath)
|
|
if (success) {
|
|
await CacheManager.purgeCoverCache(libraryItemId)
|
|
return coverFilePath
|
|
}
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Extract cover art from ebook and save for library item
|
|
*
|
|
* @param {import('../utils/parsers/parseEbookMetadata').EBookFileScanData} ebookFileScanData
|
|
* @param {string} libraryItemId
|
|
* @param {string} [libraryItemPath] null for isFile library items
|
|
* @returns {Promise<string>} returns cover path
|
|
*/
|
|
async saveEbookCoverArt(ebookFileScanData, libraryItemId, libraryItemPath) {
|
|
if (!ebookFileScanData?.ebookCoverPath) return null
|
|
|
|
let coverDirPath = null
|
|
if (global.ServerSettings.storeCoverWithItem && libraryItemPath) {
|
|
coverDirPath = libraryItemPath
|
|
} else {
|
|
coverDirPath = Path.posix.join(global.MetadataPath, 'items', libraryItemId)
|
|
}
|
|
await fs.ensureDir(coverDirPath)
|
|
|
|
let extname = Path.extname(ebookFileScanData.ebookCoverPath) || '.jpg'
|
|
if (extname === '.jpeg') extname = '.jpg'
|
|
const coverFilename = `cover${extname}`
|
|
const coverFilePath = Path.join(coverDirPath, coverFilename)
|
|
|
|
// TODO: Overwrite if exists?
|
|
const coverAlreadyExists = await fs.pathExists(coverFilePath)
|
|
if (coverAlreadyExists) {
|
|
Logger.warn(`[CoverManager] Extract embedded cover art but cover already exists for "${coverFilePath}" - overwriting`)
|
|
}
|
|
|
|
const success = await parseEbookMetadata.extractCoverImage(ebookFileScanData, coverFilePath)
|
|
if (success) {
|
|
await CacheManager.purgeCoverCache(libraryItemId)
|
|
return coverFilePath
|
|
}
|
|
return null
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string} url
|
|
* @param {string} libraryItemId
|
|
* @param {string} [libraryItemPath] null if library item isFile or is from adding new podcast
|
|
* @returns {Promise<{error:string}|{cover:string}>}
|
|
*/
|
|
async downloadCoverFromUrlNew(url, libraryItemId, libraryItemPath) {
|
|
try {
|
|
let coverDirPath = null
|
|
if (global.ServerSettings.storeCoverWithItem && libraryItemPath) {
|
|
coverDirPath = libraryItemPath
|
|
} else {
|
|
coverDirPath = Path.posix.join(global.MetadataPath, 'items', libraryItemId)
|
|
}
|
|
|
|
await fs.ensureDir(coverDirPath)
|
|
|
|
const temppath = Path.posix.join(coverDirPath, 'cover')
|
|
const success = await downloadImageFile(url, temppath).then(() => true).catch((err) => {
|
|
Logger.error(`[CoverManager] Download image file failed for "${url}"`, err)
|
|
return false
|
|
})
|
|
if (!success) {
|
|
return {
|
|
error: 'Failed to download image from url'
|
|
}
|
|
}
|
|
|
|
const imgtype = await this.checkFileIsValidImage(temppath, true)
|
|
if (imgtype.error) {
|
|
return imgtype
|
|
}
|
|
|
|
const coverFullPath = Path.posix.join(coverDirPath, `cover.${imgtype.ext}`)
|
|
await fs.rename(temppath, coverFullPath)
|
|
|
|
await this.removeOldCovers(coverDirPath, '.' + imgtype.ext)
|
|
await CacheManager.purgeCoverCache(libraryItemId)
|
|
|
|
Logger.info(`[CoverManager] Downloaded libraryItem cover "${coverFullPath}" from url "${url}"`)
|
|
return {
|
|
cover: coverFullPath
|
|
}
|
|
} catch (error) {
|
|
Logger.error(`[CoverManager] Fetch cover image from url "${url}" failed`, error)
|
|
return {
|
|
error: 'Failed to fetch image from url'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
module.exports = new CoverManager() |