-
-
- delete
-
+
@@ -53,9 +58,9 @@ export default {
authorCopy: {
name: '',
asin: '',
- description: '',
- imagePath: ''
+ description: ''
},
+ imageUrl: '',
processing: false
}
},
@@ -100,10 +105,10 @@ export default {
},
methods: {
init() {
+ this.imageUrl = ''
this.authorCopy.name = this.author.name
this.authorCopy.asin = this.author.asin
this.authorCopy.description = this.author.description
- this.authorCopy.imagePath = this.author.imagePath
},
removeClick() {
const payload = {
@@ -131,7 +136,7 @@ export default {
this.$store.commit('globals/setConfirmPrompt', payload)
},
async submitForm() {
- var keysToCheck = ['name', 'asin', 'description', 'imagePath']
+ var keysToCheck = ['name', 'asin', 'description']
var updatePayload = {}
keysToCheck.forEach((key) => {
if (this.authorCopy[key] !== this.author[key]) {
@@ -160,21 +165,46 @@ export default {
}
this.processing = false
},
- async removeCover() {
- var updatePayload = {
- imagePath: null
- }
+ removeCover() {
this.processing = true
- var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => {
- console.error('Failed', error)
- this.$toast.error(this.$strings.ToastAuthorImageRemoveFailed)
- return null
- })
- if (result && result.updated) {
- this.$toast.success(this.$strings.ToastAuthorImageRemoveSuccess)
- this.$store.commit('globals/showEditAuthorModal', result.author)
+ this.$axios
+ .$delete(`/api/authors/${this.authorId}/image`)
+ .then((data) => {
+ this.$toast.success(this.$strings.ToastAuthorImageRemoveSuccess)
+ this.$store.commit('globals/showEditAuthorModal', data.author)
+ })
+ .catch((error) => {
+ console.error('Failed', error)
+ this.$toast.error(this.$strings.ToastAuthorImageRemoveFailed)
+ })
+ .finally(() => {
+ this.processing = false
+ })
+ },
+ submitUploadCover() {
+ if (!this.imageUrl?.startsWith('http:') && !this.imageUrl?.startsWith('https:')) {
+ this.$toast.error('Invalid image url')
+ return
}
- this.processing = false
+
+ this.processing = true
+ const updatePayload = {
+ url: this.imageUrl
+ }
+ this.$axios
+ .$post(`/api/authors/${this.authorId}/image`, updatePayload)
+ .then((data) => {
+ this.imageUrl = ''
+ this.$toast.success('Author image updated')
+ this.$store.commit('globals/showEditAuthorModal', data.author)
+ })
+ .catch((error) => {
+ console.error('Failed', error)
+ this.$toast.error(error.response.data || 'Failed to remove author image')
+ })
+ .finally(() => {
+ this.processing = false
+ })
},
async searchAuthor() {
if (!this.authorCopy.name && !this.authorCopy.asin) {
diff --git a/server/controllers/AuthorController.js b/server/controllers/AuthorController.js
index 0cd243fd..62a7ebde 100644
--- a/server/controllers/AuthorController.js
+++ b/server/controllers/AuthorController.js
@@ -67,30 +67,10 @@ class AuthorController {
const payload = req.body
let hasUpdated = false
- // Updating/removing cover image
- if (payload.imagePath !== undefined && payload.imagePath !== req.author.imagePath) {
- if (!payload.imagePath && req.author.imagePath) { // If removing image then remove file
- await CacheManager.purgeImageCache(req.author.id) // Purge cache
- await CoverManager.removeFile(req.author.imagePath)
- } else if (payload.imagePath.startsWith('http')) { // Check if image path is a url
- const imageData = await AuthorFinder.saveAuthorImage(req.author.id, payload.imagePath)
- if (imageData) {
- if (req.author.imagePath) {
- await CacheManager.purgeImageCache(req.author.id) // Purge cache
- }
- payload.imagePath = imageData.path
- hasUpdated = true
- }
- } else if (payload.imagePath && payload.imagePath !== req.author.imagePath) { // Changing image path locally
- if (!await fs.pathExists(payload.imagePath)) { // Make sure image path exists
- Logger.error(`[AuthorController] Image path does not exist: "${payload.imagePath}"`)
- return res.status(400).send('Author image path does not exist')
- }
-
- if (req.author.imagePath) {
- await CacheManager.purgeImageCache(req.author.id) // Purge cache
- }
- }
+ // author imagePath must be set through other endpoints as of v2.4.5
+ if (payload.imagePath !== undefined) {
+ Logger.warn(`[AuthorController] Updating local author imagePath is not supported`)
+ delete payload.imagePath
}
const authorNameUpdate = payload.name !== undefined && payload.name !== req.author.name
@@ -131,7 +111,7 @@ class AuthorController {
Database.removeAuthorFromFilterData(req.author.libraryId, req.author.id)
// Send updated num books for merged author
- const numBooks = await Database.libraryItemModel.getForAuthor(existingAuthor).length
+ const numBooks = (await Database.libraryItemModel.getForAuthor(existingAuthor)).length
SocketAuthority.emitter('author_updated', existingAuthor.toJSONExpanded(numBooks))
res.json({
@@ -191,6 +171,75 @@ class AuthorController {
res.sendStatus(200)
}
+ /**
+ * POST: /api/authors/:id/image
+ * Upload author image from web URL
+ *
+ * @param {import('express').Request} req
+ * @param {import('express').Response} res
+ */
+ async uploadImage(req, res) {
+ if (!req.user.canUpload) {
+ Logger.warn('User attempted to upload an image without permission', req.user)
+ return res.sendStatus(403)
+ }
+ if (!req.body.url) {
+ Logger.error(`[AuthorController] Invalid request payload. 'url' not in request body`)
+ return res.status(400).send(`Invalid request payload. 'url' not in request body`)
+ }
+ if (!req.body.url.startsWith?.('http:') && !req.body.url.startsWith?.('https:')) {
+ Logger.error(`[AuthorController] Invalid request payload. Invalid url "${req.body.url}"`)
+ return res.status(400).send(`Invalid request payload. Invalid url "${req.body.url}"`)
+ }
+
+ Logger.debug(`[AuthorController] Requesting download author image from url "${req.body.url}"`)
+ const result = await AuthorFinder.saveAuthorImage(req.author.id, req.body.url)
+
+ if (result?.error) {
+ return res.status(400).send(result.error)
+ } else if (!result?.path) {
+ return res.status(500).send('Unknown error occurred')
+ }
+
+ if (req.author.imagePath) {
+ await CacheManager.purgeImageCache(req.author.id) // Purge cache
+ }
+
+ req.author.imagePath = result.path
+ await Database.authorModel.updateFromOld(req.author)
+
+ const numBooks = (await Database.libraryItemModel.getForAuthor(req.author)).length
+ SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks))
+ res.json({
+ author: req.author.toJSON()
+ })
+ }
+
+ /**
+ * DELETE: /api/authors/:id/image
+ * Remove author image & delete image file
+ *
+ * @param {import('express').Request} req
+ * @param {import('express').Response} res
+ */
+ async deleteImage(req, res) {
+ if (!req.author.imagePath) {
+ Logger.error(`[AuthorController] Author "${req.author.imagePath}" has no imagePath set`)
+ return res.status(400).send('Author has no image path set')
+ }
+ Logger.info(`[AuthorController] Removing image for author "${req.author.name}" at "${req.author.imagePath}"`)
+ await CacheManager.purgeImageCache(req.author.id) // Purge cache
+ await CoverManager.removeFile(req.author.imagePath)
+ req.author.imagePath = null
+ await Database.authorModel.updateFromOld(req.author)
+
+ const numBooks = (await Database.libraryItemModel.getForAuthor(req.author)).length
+ SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks))
+ res.json({
+ author: req.author.toJSON()
+ })
+ }
+
async match(req, res) {
let authorData = null
const region = req.body.region || 'us'
@@ -215,7 +264,7 @@ class AuthorController {
await CacheManager.purgeImageCache(req.author.id)
const imageData = await AuthorFinder.saveAuthorImage(req.author.id, authorData.image)
- if (imageData) {
+ if (imageData?.path) {
req.author.imagePath = imageData.path
hasUpdates = true
}
@@ -231,7 +280,7 @@ class AuthorController {
await Database.updateAuthor(req.author)
- const numBooks = await Database.libraryItemModel.getForAuthor(req.author).length
+ const numBooks = (await Database.libraryItemModel.getForAuthor(req.author)).length
SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks))
}
diff --git a/server/finders/AuthorFinder.js b/server/finders/AuthorFinder.js
index 9c2a3b4f..59c6ce16 100644
--- a/server/finders/AuthorFinder.js
+++ b/server/finders/AuthorFinder.js
@@ -10,13 +10,6 @@ class AuthorFinder {
this.audnexus = new Audnexus()
}
- async downloadImage(url, outputPath) {
- return downloadFile(url, outputPath).then(() => true).catch((error) => {
- Logger.error('[AuthorFinder] Failed to download author image', error)
- return null
- })
- }
-
findAuthorByASIN(asin, region) {
if (!asin) return null
return this.audnexus.findAuthorByASIN(asin, region)
@@ -33,28 +26,36 @@ class AuthorFinder {
return author
}
+ /**
+ * Download author image from url and save in authors folder
+ *
+ * @param {string} authorId
+ * @param {string} url
+ * @returns {Promise<{path:string, error:string}>}
+ */
async saveAuthorImage(authorId, url) {
- var authorDir = Path.join(global.MetadataPath, 'authors')
- var relAuthorDir = Path.posix.join('/metadata', 'authors')
+ const authorDir = Path.join(global.MetadataPath, 'authors')
if (!await fs.pathExists(authorDir)) {
await fs.ensureDir(authorDir)
}
- var imageExtension = url.toLowerCase().split('.').pop()
- var ext = imageExtension === 'png' ? 'png' : 'jpg'
- var filename = authorId + '.' + ext
- var outputPath = Path.posix.join(authorDir, filename)
- var relPath = Path.posix.join(relAuthorDir, filename)
+ const imageExtension = url.toLowerCase().split('.').pop()
+ const ext = imageExtension === 'png' ? 'png' : 'jpg'
+ const filename = authorId + '.' + ext
+ const outputPath = Path.posix.join(authorDir, filename)
- var success = await this.downloadImage(url, outputPath)
- if (!success) {
- return null
- }
- return {
- path: outputPath,
- relPath
- }
+ return downloadFile(url, outputPath).then(() => {
+ return {
+ path: outputPath
+ }
+ }).catch((err) => {
+ let errorMsg = err.message || 'Unknown error'
+ Logger.error(`[AuthorFinder] Download image file failed for "${url}"`, errorMsg)
+ return {
+ error: errorMsg
+ }
+ })
}
}
module.exports = new AuthorFinder()
\ No newline at end of file
diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js
index dc816b44..a90d1873 100644
--- a/server/routers/ApiRouter.js
+++ b/server/routers/ApiRouter.js
@@ -202,6 +202,8 @@ class ApiRouter {
this.router.delete('/authors/:id', AuthorController.middleware.bind(this), AuthorController.delete.bind(this))
this.router.post('/authors/:id/match', AuthorController.middleware.bind(this), AuthorController.match.bind(this))
this.router.get('/authors/:id/image', AuthorController.middleware.bind(this), AuthorController.getImage.bind(this))
+ this.router.post('/authors/:id/image', AuthorController.middleware.bind(this), AuthorController.uploadImage.bind(this))
+ this.router.delete('/authors/:id/image', AuthorController.middleware.bind(this), AuthorController.deleteImage.bind(this))
//
// Series Routes