mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-07 09:51:37 +00:00
Fix merge conflicts
This commit is contained in:
parent
f37d6fa3cd
commit
2f93d96b12
3 changed files with 202 additions and 96 deletions
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<div ref="wrapper" :class="`rounded-${rounded}`" class="w-full h-full bg-primary overflow-hidden">
|
||||
<svg v-if="!imagePath" width="140%" height="140%" style="margin-left: -20%; margin-top: -20%; opacity: 0.6" viewBox="0 0 177 266" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<div ref="wrapper" :class="`rounded-${rounded}`" class="w-full h-full bg-primary overflow-hidden flex items-center justify-center">
|
||||
<img v-if="imgSrc" :src="imgSrc" :alt="altText" class="w-full h-full object-cover" @load="imageLoaded" />
|
||||
<svg v-else width="140%" height="140%" style="margin-left: -20%; margin-top: -20%; opacity: 0.6" viewBox="0 0 177 266" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="white" d="M40.7156 165.47C10.2694 150.865 -31.5407 148.629 -38.0532 155.529L63.3191 204.159L76.9443 190.899C66.828 181.394 54.006 171.846 40.7156 165.47Z" stroke="white" stroke-width="4" transform="translate(-2 -1)" />
|
||||
<path d="M-38.0532 155.529C-31.5407 148.629 10.2694 150.865 40.7156 165.47C54.006 171.846 66.828 181.394 76.9443 190.899L95.0391 173.37C80.6681 159.403 64.7526 149.155 51.5747 142.834C21.3549 128.337 -46.2471 114.563 -60.6897 144.67L-71.5489 167.307L44.5864 223.019L63.3191 204.159L-38.0532 155.529Z" fill="white" />
|
||||
<path
|
||||
|
|
@ -13,10 +14,6 @@
|
|||
/>
|
||||
<path d="M151.336 159.01L159.048 166.762L82.7048 242.703L74.973 242.683L74.9934 234.951L151.336 159.01Z" fill="white" stroke="white" stroke-width="10px" />
|
||||
</svg>
|
||||
<div v-else class="w-full h-full relative">
|
||||
<div v-if="showCoverBg" class="cover-bg absolute" :style="{ backgroundImage: `url(${imgSrc})` }" />
|
||||
<img ref="img" :src="imgSrc" @load="imageLoaded" class="absolute top-0 left-0 h-full w-full" :class="coverContain ? 'object-contain' : 'object-cover'" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -25,55 +22,68 @@ export default {
|
|||
props: {
|
||||
author: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
default: () => ({})
|
||||
},
|
||||
rounded: {
|
||||
type: String,
|
||||
default: 'lg'
|
||||
},
|
||||
uploadedFile: {
|
||||
type: [File, null],
|
||||
default: null
|
||||
},
|
||||
uploadedUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
altText: {
|
||||
type: String,
|
||||
default: 'author image'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showCoverBg: false,
|
||||
coverContain: true
|
||||
objectUrl: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
_author() {
|
||||
return this.author || {}
|
||||
},
|
||||
authorId() {
|
||||
return this._author.id
|
||||
},
|
||||
imagePath() {
|
||||
return this._author.imagePath
|
||||
return this.author && this.author.imagePath
|
||||
},
|
||||
updatedAt() {
|
||||
return this._author.updatedAt
|
||||
return this.author && this.author.updatedAt
|
||||
},
|
||||
imgSrc() {
|
||||
if (!this.imagePath) return null
|
||||
return `${this.$config.routerBasePath}/api/authors/${this.authorId}/image?ts=${this.updatedAt}`
|
||||
if (this.objectUrl) return this.objectUrl
|
||||
if (this.uploadedUrl) return this.uploadedUrl
|
||||
if (this.imagePath) {
|
||||
return `${this.$config.routerBasePath}/api/authors/${this.author.id}/image?ts=${this.updatedAt}`
|
||||
}
|
||||
return null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
<<<<<<< Updated upstream
|
||||
imageLoaded() {
|
||||
if (this.$refs.img) {
|
||||
var { naturalWidth, naturalHeight } = this.$refs.img
|
||||
var imgAr = naturalHeight / naturalWidth
|
||||
if (imgAr < 0.5 || imgAr > 2) {
|
||||
this.showCoverBg = true
|
||||
} else {
|
||||
this.showCoverBg = false
|
||||
this.coverContain = false
|
||||
watch: {
|
||||
uploadedFile: {
|
||||
immediate: true,
|
||||
handler(file) {
|
||||
if (this.objectUrl) {
|
||||
URL.revokeObjectURL(this.objectUrl)
|
||||
this.objectUrl = null
|
||||
}
|
||||
if (file && file instanceof File) {
|
||||
this.objectUrl = URL.createObjectURL(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
=======
|
||||
imageLoaded() {}
|
||||
>>>>>>> Stashed changes
|
||||
},
|
||||
mounted() {}
|
||||
methods: {
|
||||
imageLoaded() {}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.objectUrl) {
|
||||
URL.revokeObjectURL(this.objectUrl)
|
||||
this.objectUrl = null
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -9,27 +9,21 @@
|
|||
<div class="flex">
|
||||
<div class="w-40 p-2">
|
||||
<div class="w-full h-45 relative">
|
||||
<covers-author-image :author="authorCopy" />
|
||||
<covers-author-image :author="authorCopy" :uploaded-file="selectedFile" :uploaded-url="previewUrl" />
|
||||
<div v-if="userCanDelete && !processing && author.imagePath" class="absolute top-0 left-0 w-full h-full opacity-0 hover:opacity-100">
|
||||
<span class="absolute top-2 right-2 material-symbols text-error transform hover:scale-125 transition-transform cursor-pointer text-lg" @click="removeCover">delete</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grow">
|
||||
<<<<<<< Updated upstream
|
||||
<form @submit.prevent="submitUploadCover" class="flex grow mb-2 p-2">
|
||||
<ui-text-input v-model="imageUrl" :placeholder="$strings.LabelImageURLFromTheWeb" class="h-9 w-full" />
|
||||
<ui-btn color="bg-success" type="submit" :padding-x="4" :disabled="!imageUrl" class="ml-2 sm:ml-3 w-24 h-9">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
=======
|
||||
<form @submit.prevent="submitUploadCover" class="flex grow mb-2 p-2 items-center">
|
||||
<ui-text-input v-model="imageUrl" :placeholder="$strings.LabelImageURLFromTheWeb" class="h-9 w-full" @input="onUrlInput" />
|
||||
<label class="ml-2 sm:ml-3 w-24 h-9 flex items-center justify-center bg-gray-700 text-white rounded cursor-pointer hover:bg-gray-600">
|
||||
<input type="file" accept="image/*" class="hidden" @change="onFileChange" />
|
||||
{{ $strings.ButtonUpload }}
|
||||
{{ $strings.ButtonUpload || 'Upload' }}
|
||||
</label>
|
||||
<ui-btn color="bg-success" type="submit" :padding-x="4" :disabled="!imageUrl && !selectedFile" class="ml-2 sm:ml-3 w-24 h-9">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
<ui-btn v-if="selectedFile || previewUrl" type="button" color="bg-error" class="ml-2 sm:ml-3 w-24 h-9" small @click="clearSelection">{{ $strings.ButtonCancel }}</ui-btn>
|
||||
>>>>>>> Stashed changes
|
||||
<ui-btn v-if="selectedFile || previewUrl" type="button" color="bg-error" class="ml-2" @click="clearSelection">Cancel</ui-btn>
|
||||
</form>
|
||||
|
||||
<form v-if="author" @submit.prevent="submitForm">
|
||||
|
|
@ -49,7 +43,6 @@
|
|||
<ui-btn v-if="userCanDelete" small color="bg-error" type="button" @click.stop="removeClick">{{ $strings.ButtonRemove }}</ui-btn>
|
||||
<div class="grow" />
|
||||
<ui-btn type="button" class="mx-2" @click="searchAuthor">{{ $strings.ButtonQuickMatch }}</ui-btn>
|
||||
|
||||
<ui-btn type="submit">{{ $strings.ButtonSave }}</ui-btn>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -69,6 +62,8 @@ export default {
|
|||
description: ''
|
||||
},
|
||||
imageUrl: '',
|
||||
selectedFile: null,
|
||||
previewUrl: '',
|
||||
processing: false
|
||||
}
|
||||
},
|
||||
|
|
@ -114,10 +109,36 @@ export default {
|
|||
methods: {
|
||||
init() {
|
||||
this.imageUrl = ''
|
||||
this.selectedFile = null
|
||||
this.previewUrl = ''
|
||||
this.authorCopy = {
|
||||
...this.author
|
||||
}
|
||||
},
|
||||
onFileChange(e) {
|
||||
const file = e.target.files[0]
|
||||
if (file) {
|
||||
this.selectedFile = file
|
||||
if (this.previewUrl) {
|
||||
URL.revokeObjectURL(this.previewUrl)
|
||||
}
|
||||
this.previewUrl = URL.createObjectURL(file)
|
||||
this.imageUrl = ''
|
||||
}
|
||||
},
|
||||
onUrlInput() {
|
||||
if (this.selectedFile) {
|
||||
this.clearSelection()
|
||||
}
|
||||
},
|
||||
clearSelection() {
|
||||
this.imageUrl = ''
|
||||
if (this.previewUrl) {
|
||||
URL.revokeObjectURL(this.previewUrl)
|
||||
this.previewUrl = ''
|
||||
}
|
||||
this.selectedFile = null
|
||||
},
|
||||
removeClick() {
|
||||
const payload = {
|
||||
message: this.$getString('MessageConfirmRemoveAuthor', [this.author.name]),
|
||||
|
|
@ -179,7 +200,6 @@ export default {
|
|||
.$delete(`/api/authors/${this.authorId}/image`)
|
||||
.then((data) => {
|
||||
this.$toast.success(this.$strings.ToastAuthorImageRemoveSuccess)
|
||||
|
||||
this.authorCopy.updatedAt = data.author.updatedAt
|
||||
this.authorCopy.imagePath = data.author.imagePath
|
||||
})
|
||||
|
|
@ -192,6 +212,28 @@ export default {
|
|||
})
|
||||
},
|
||||
submitUploadCover() {
|
||||
if (this.selectedFile) {
|
||||
this.processing = true
|
||||
const formData = new FormData()
|
||||
formData.append('image', this.selectedFile)
|
||||
this.$axios
|
||||
.$post(`/api/authors/${this.authorId}/image`, formData)
|
||||
.then((data) => {
|
||||
this.clearSelection()
|
||||
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
||||
this.authorCopy.updatedAt = data.author.updatedAt
|
||||
this.authorCopy.imagePath = data.author.imagePath
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
this.$toast.error(error.response?.data || this.$strings.ToastRemoveFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.imageUrl?.startsWith('http:') && !this.imageUrl?.startsWith('https:')) {
|
||||
this.$toast.error(this.$strings.ToastInvalidImageUrl)
|
||||
return
|
||||
|
|
@ -204,15 +246,14 @@ export default {
|
|||
this.$axios
|
||||
.$post(`/api/authors/${this.authorId}/image`, updatePayload)
|
||||
.then((data) => {
|
||||
this.imageUrl = ''
|
||||
this.clearSelection()
|
||||
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
||||
|
||||
this.authorCopy.updatedAt = data.author.updatedAt
|
||||
this.authorCopy.imagePath = data.author.imagePath
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
this.$toast.error(error.response.data || this.$strings.ToastRemoveFailed)
|
||||
this.$toast.error(error.response?.data || this.$strings.ToastRemoveFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
|
|
@ -256,7 +297,10 @@ export default {
|
|||
this.processing = false
|
||||
}
|
||||
},
|
||||
mounted() {},
|
||||
beforeDestroy() {}
|
||||
beforeDestroy() {
|
||||
if (this.previewUrl) {
|
||||
URL.revokeObjectURL(this.previewUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ const { Request, Response, NextFunction } = require('express')
|
|||
const sequelize = require('sequelize')
|
||||
const fs = require('../libs/fsExtra')
|
||||
const { createNewSortInstance } = require('../libs/fastSort')
|
||||
const axios = require('axios')
|
||||
const Path = require('path')
|
||||
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
|
|
@ -258,7 +260,7 @@ class AuthorController {
|
|||
|
||||
/**
|
||||
* POST: /api/authors/:id/image
|
||||
* Upload author image from web URL
|
||||
* Upload author image from file (express-fileupload) or URL
|
||||
*
|
||||
* @param {AuthorControllerRequest} req
|
||||
* @param {Response} res
|
||||
|
|
@ -268,30 +270,45 @@ class AuthorController {
|
|||
Logger.warn(`User "${req.user.username}" attempted to upload an image without permission`)
|
||||
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)
|
||||
let imagePath = null
|
||||
|
||||
if (result?.error) {
|
||||
return res.status(400).send(result.error)
|
||||
} else if (!result?.path) {
|
||||
return res.status(500).send('Unknown error occurred')
|
||||
if (req.files && req.files.image) {
|
||||
const uploadedFile = req.files.image
|
||||
const uploadsDir = Path.join(global.MetadataPath, 'authors')
|
||||
|
||||
if (!fs.existsSync(uploadsDir)) {
|
||||
fs.mkdirSync(uploadsDir, { recursive: true })
|
||||
}
|
||||
|
||||
const fileName = `${req.author.id}${Path.extname(uploadedFile.name)}`
|
||||
const filePath = Path.join(uploadsDir, fileName)
|
||||
|
||||
// Remove existing file if it exists
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath)
|
||||
}
|
||||
|
||||
await uploadedFile.mv(filePath)
|
||||
imagePath = filePath
|
||||
} else if (req.body.url && (req.body.url.startsWith('http://') || req.body.url.startsWith('https://'))) {
|
||||
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')
|
||||
}
|
||||
imagePath = result.path
|
||||
} else {
|
||||
Logger.error(`[AuthorController] Invalid request payload. No file or valid url`)
|
||||
return res.status(400).send(`Invalid request payload. No file or valid url`)
|
||||
}
|
||||
|
||||
if (req.author.imagePath) {
|
||||
await CacheManager.purgeImageCache(req.author.id) // Purge cache
|
||||
await CacheManager.purgeImageCache(req.author.id)
|
||||
}
|
||||
|
||||
req.author.imagePath = result.path
|
||||
// imagePath may not have changed, but we still want to update the updatedAt field to bust image cache
|
||||
req.author.imagePath = imagePath
|
||||
req.author.changed('imagePath', true)
|
||||
await req.author.save()
|
||||
|
||||
|
|
@ -311,20 +328,40 @@ class AuthorController {
|
|||
*/
|
||||
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)
|
||||
|
||||
// Clear cache
|
||||
try {
|
||||
await CacheManager.purgeImageCache(req.author.id)
|
||||
} catch (e) {
|
||||
console.log('Cache purge error (ignored):', e.message)
|
||||
}
|
||||
|
||||
// Only local files are deleted
|
||||
if (!req.author.imagePath.startsWith('http')) {
|
||||
await CoverManager.removeFile(req.author.imagePath)
|
||||
}
|
||||
|
||||
req.author.imagePath = null
|
||||
// Mark changes and explicitly update updatedAt to ensure the client receives the new timestamp
|
||||
req.author.updatedAt = new Date()
|
||||
req.author.changed('imagePath', true)
|
||||
await req.author.save()
|
||||
|
||||
// Clear cache again
|
||||
try {
|
||||
await CacheManager.purgeImageCache(req.author.id)
|
||||
} catch (e) {
|
||||
console.log('Cache purge error (ignored):', e.message)
|
||||
}
|
||||
|
||||
const numBooks = await Database.bookAuthorModel.getCountForAuthor(req.author.id)
|
||||
SocketAuthority.emitter('author_updated', req.author.toOldJSONExpanded(numBooks))
|
||||
res.json({
|
||||
author: req.author.toOldJSON()
|
||||
})
|
||||
|
||||
res.json({ author: req.author.toOldJSON() })
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -388,37 +425,52 @@ class AuthorController {
|
|||
* @param {Response} res
|
||||
*/
|
||||
async getImage(req, res) {
|
||||
const {
|
||||
query: { width, height, format, raw }
|
||||
} = req
|
||||
const { raw } = req.query
|
||||
const author = await Database.authorModel.findByPk(req.params.id)
|
||||
if (!author || !author.imagePath) return res.sendStatus(404)
|
||||
|
||||
const authorId = req.params.id
|
||||
// Set no-cache headers to always get the latest image
|
||||
res.set({
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
Pragma: 'no-cache',
|
||||
Expires: '0'
|
||||
})
|
||||
|
||||
// Proxy external image URLs
|
||||
if (author.imagePath.startsWith('http')) {
|
||||
try {
|
||||
const response = await axios.get(author.imagePath, { responseType: 'stream' })
|
||||
res.setHeader('Content-Type', response.headers['content-type'] || 'image/jpeg')
|
||||
response.data.pipe(res)
|
||||
} catch (err) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Local file
|
||||
let imagePath = author.imagePath
|
||||
if (!Path.isAbsolute(imagePath)) {
|
||||
imagePath = Path.resolve(process.cwd(), imagePath)
|
||||
}
|
||||
if (!(await fs.pathExists(imagePath))) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
if (raw) {
|
||||
const author = await Database.authorModel.findByPk(authorId)
|
||||
if (!author) {
|
||||
Logger.warn(`[AuthorController] Author "${authorId}" not found`)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
if (!author.imagePath || !(await fs.pathExists(author.imagePath))) {
|
||||
Logger.warn(`[AuthorController] Author "${author.name}" has invalid imagePath: ${author.imagePath}`)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
return res.sendFile(author.imagePath)
|
||||
return res.sendFile(imagePath)
|
||||
}
|
||||
|
||||
const options = {
|
||||
format: format || (reqSupportsWebp(req) ? 'webp' : 'jpeg'),
|
||||
height: height ? parseInt(height) : null,
|
||||
width: width ? parseInt(width) : null
|
||||
format: 'jpeg',
|
||||
width: req.query.width ? parseInt(req.query.width) : null,
|
||||
height: req.query.height ? parseInt(req.query.height) : null
|
||||
}
|
||||
return CacheManager.handleAuthorCache(res, authorId, options)
|
||||
return CacheManager.handleAuthorCache(res, author.id, options)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Middleware for author routes
|
||||
* @param {RequestWithUser} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
|
|
@ -426,6 +478,7 @@ class AuthorController {
|
|||
async middleware(req, res, next) {
|
||||
const author = await Database.authorModel.findByPk(req.params.id)
|
||||
if (!author) return res.sendStatus(404)
|
||||
req.author = author
|
||||
|
||||
if (req.method == 'DELETE' && !req.user.canDelete) {
|
||||
Logger.warn(`[AuthorController] User "${req.user.username}" attempted to delete without permission`)
|
||||
|
|
@ -435,7 +488,6 @@ class AuthorController {
|
|||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
req.author = author
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue