New data model save covers, scanner, new api routes

This commit is contained in:
advplyr 2022-03-12 17:45:32 -06:00
parent 5f4e5cd3d8
commit 73257188f6
37 changed files with 1649 additions and 672 deletions

View file

@ -4,9 +4,11 @@ const axios = require('axios')
const Logger = require('./Logger')
const readChunk = require('read-chunk')
const imageType = require('image-type')
const filePerms = require('./utils/filePerms')
const globals = require('./utils/globals')
const { downloadFile } = require('./utils/fileUtils')
const { extractCoverArt } = require('./utils/ffmpegHelpers')
class CoverController {
constructor(db, cacheManager) {
@ -16,17 +18,11 @@ class CoverController {
this.BookMetadataPath = Path.posix.join(global.MetadataPath, 'books')
}
getCoverDirectory(audiobook) {
getCoverDirectory(libraryItem) {
if (this.db.serverSettings.storeCoverWithBook) {
return {
fullPath: audiobook.fullPath,
relPath: '/s/book/' + audiobook.id
}
return libraryItem.path
} else {
return {
fullPath: Path.posix.join(this.BookMetadataPath, audiobook.id),
relPath: Path.posix.join('/metadata', 'books', audiobook.id)
}
return Path.posix.join(this.BookMetadataPath, libraryItem.id)
}
}
@ -67,18 +63,18 @@ class CoverController {
}
}
async checkFileIsValidImage(imagepath) {
async checkFileIsValidImage(imagepath, removeOnInvalid = false) {
const buffer = await readChunk(imagepath, 0, 12)
const imgType = imageType(buffer)
if (!imgType) {
await this.removeFile(imagepath)
if (removeOnInvalid) await this.removeFile(imagepath)
return {
error: 'Invalid image'
}
}
if (!globals.SupportedImageTypes.includes(imgType.ext)) {
await this.removeFile(imagepath)
if (removeOnInvalid) await this.removeFile(imagepath)
return {
error: `Invalid image type ${imgType.ext} (Supported: ${globals.SupportedImageTypes.join(',')})`
}
@ -86,7 +82,7 @@ class CoverController {
return imgType
}
async uploadCover(audiobook, coverFile) {
async uploadCover(libraryItem, coverFile) {
var extname = Path.extname(coverFile.name.toLowerCase())
if (!extname || !globals.SupportedImageTypes.includes(extname.slice(1))) {
return {
@ -94,12 +90,10 @@ class CoverController {
}
}
var { fullPath, relPath } = this.getCoverDirectory(audiobook)
await fs.ensureDir(fullPath)
var coverDirPath = this.getCoverDirectory(libraryItem)
await fs.ensureDir(coverDirPath)
var coverFilename = `cover${extname}`
var coverFullPath = Path.posix.join(fullPath, coverFilename)
var coverPath = Path.posix.join(relPath, coverFilename)
var coverFullPath = Path.posix.join(coverDirPath, `cover${extname}`)
// Move cover from temp upload dir to destination
var success = await coverFile.mv(coverFullPath).then(() => true).catch((error) => {
@ -113,23 +107,23 @@ class CoverController {
}
}
await this.removeOldCovers(fullPath, extname)
await this.cacheManager.purgeCoverCache(audiobook.id)
await this.removeOldCovers(coverDirPath, extname)
await this.cacheManager.purgeCoverCache(libraryItem.id)
Logger.info(`[CoverController] Uploaded audiobook cover "${coverPath}" for "${audiobook.title}"`)
Logger.info(`[CoverController] Uploaded libraryItem cover "${coverFullPath}" for "${libraryItem.media.metadata.title}"`)
audiobook.updateBookCover(coverPath, coverFullPath)
libraryItem.updateMediaCover(coverFullPath)
return {
cover: coverPath
cover: coverFullPath
}
}
async downloadCoverFromUrl(audiobook, url) {
async downloadCoverFromUrl(libraryItem, url) {
try {
var { fullPath, relPath } = this.getCoverDirectory(audiobook)
await fs.ensureDir(fullPath)
var coverDirPath = this.getCoverDirectory(libraryItem)
await fs.ensureDir(coverDirPath)
var temppath = Path.posix.join(fullPath, 'cover')
var temppath = Path.posix.join(coverDirPath, 'cover')
var success = await downloadFile(url, temppath).then(() => true).catch((err) => {
Logger.error(`[CoverController] Download image file failed for "${url}"`, err)
return false
@ -140,25 +134,24 @@ class CoverController {
}
}
var imgtype = await this.checkFileIsValidImage(temppath)
var imgtype = await this.checkFileIsValidImage(temppath, true)
if (imgtype.error) {
return imgtype
}
var coverFilename = `cover.${imgtype.ext}`
var coverPath = Path.posix.join(relPath, coverFilename)
var coverFullPath = Path.posix.join(fullPath, coverFilename)
var coverFullPath = Path.posix.join(coverDirPath, coverFilename)
await fs.rename(temppath, coverFullPath)
await this.removeOldCovers(fullPath, '.' + imgtype.ext)
await this.cacheManager.purgeCoverCache(audiobook.id)
await this.removeOldCovers(coverDirPath, '.' + imgtype.ext)
await this.cacheManager.purgeCoverCache(libraryItem.id)
Logger.info(`[CoverController] Downloaded audiobook cover "${coverPath}" from url "${url}" for "${audiobook.title}"`)
Logger.info(`[CoverController] Downloaded libraryItem cover "${coverFullPath}" from url "${url}" for "${libraryItem.media.metadata.title}"`)
audiobook.updateBookCover(coverPath, coverFullPath)
libraryItem.updateMediaCover(coverFullPath)
return {
cover: coverPath
cover: coverFullPath
}
} catch (error) {
Logger.error(`[CoverController] Fetch cover image from url "${url}" failed`, error)
@ -167,5 +160,94 @@ class CoverController {
}
}
}
async validateCoverPath(coverPath, libraryItem) {
// Invalid cover path
if (!coverPath || coverPath.startsWith('http:') || coverPath.startsWith('https:')) {
Logger.error(`[CoverController] validate cover path invalid http url "${coverPath}"`)
return {
error: 'Invalid cover path'
}
}
coverPath = coverPath.replace(/\\/g, '/')
// Cover path already set on media
if (libraryItem.media.coverPath == coverPath) {
Logger.debug(`[CoverController] validate cover path already set "${coverPath}"`)
return {
cover: coverPath,
updated: false
}
}
// Cover path does not exist
if (!await fs.pathExists(coverPath)) {
Logger.error(`[CoverController] validate cover path does not exist "${coverPath}"`)
return {
error: 'Cover path does not exist'
}
}
// Check valid image at path
var imgtype = await this.checkFileIsValidImage(coverPath, true)
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(`[CoverController] validate cover path copy cover from "${coverPath}" to "${newCoverPath}"`)
var copySuccess = await fs.copy(coverPath, newCoverPath, { overwrite: true }).then(() => true).catch((error) => {
Logger.error(`[CoverController] validate cover path failed to copy cover`, error)
return false
})
if (!copySuccess) {
return {
error: 'Failed to copy cover to dir'
}
}
await filePerms.setDefault(newCoverPath)
await this.removeOldCovers(coverDirPath, '.' + imgtype.ext)
Logger.debug(`[CoverController] cover copy success`)
coverPath = newCoverPath
}
await this.cacheManager.purgeCoverCache(libraryItem.id)
libraryItem.updateMediaCover(coverPath)
return {
cover: coverPath,
updated: true
}
}
async saveEmbeddedCoverArt(libraryItem) {
var audioFileWithCover = libraryItem.media.audioFiles.find(af => af.embeddedCoverArt)
if (!audioFileWithCover) return false
var coverDirPath = this.getCoverDirectory(libraryItem)
await fs.ensureDir(coverDirPath)
var coverFilename = audioFileWithCover.embeddedCoverArt === 'png' ? 'cover.png' : 'cover.jpg'
var coverFilePath = Path.join(coverDirPath, coverFilename)
var coverAlreadyExists = await fs.pathExists(coverFilePath)
if (coverAlreadyExists) {
Logger.warn(`[Audiobook] Extract embedded cover art but cover already exists for "${libraryItem.media.metadata.title}" - bail`)
return false
}
var success = await extractCoverArt(audioFileWithCover.metadata.path, coverFilePath)
if (success) {
libraryItem.updateMediaCover(coverFilePath)
return coverFilePath
}
return false
}
}
module.exports = CoverController