mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-04 16:31:37 +00:00
Allow removing authors with no books
This commit is contained in:
parent
771f8c586f
commit
fc97b10f58
4 changed files with 127 additions and 16 deletions
|
|
@ -25,6 +25,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="isBookLibrary" class="w-full border border-black-200 p-4 my-8">
|
||||||
|
<div class="flex flex-wrap items-center">
|
||||||
|
<div>
|
||||||
|
<p class="text-lg">{{ $strings.LabelCleanupAuthors }}</p>
|
||||||
|
<p class="max-w-sm text-sm pt-2 text-gray-300">{{ $strings.LabelCleanupAuthorsHelp }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="grow" />
|
||||||
|
<div>
|
||||||
|
<ui-btn @click.stop="cleanupAuthorsClick">{{ $strings.ButtonRemove }}</ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -114,6 +126,38 @@ export default {
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.$emit('update:processing', false)
|
this.$emit('update:processing', false)
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
cleanupAuthorsClick() {
|
||||||
|
const payload = {
|
||||||
|
message: this.$strings.MessageConfirmCleanupAuthors,
|
||||||
|
persistent: true,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.cleanupAuthors()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
|
cleanupAuthors() {
|
||||||
|
this.$emit('update:processing', true)
|
||||||
|
this.$axios
|
||||||
|
.$delete(`/api/libraries/${this.libraryId}/authors/cleanup?force=1`)
|
||||||
|
.then((data) => {
|
||||||
|
if (data.removed) {
|
||||||
|
this.$toast.success(this.$getString('ToastCleanupAuthorsSuccess', [data.removed]))
|
||||||
|
} else {
|
||||||
|
this.$toast.info(this.$strings.ToastCleanupAuthorsNoAuthors)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to cleanup authors', error)
|
||||||
|
this.$toast.error(this.$strings.ToastCleanupAuthorsFailed)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.$emit('update:processing', false)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
|
|
|
||||||
|
|
@ -284,6 +284,8 @@
|
||||||
"LabelChapterTitle": "Chapter Title",
|
"LabelChapterTitle": "Chapter Title",
|
||||||
"LabelChapters": "Chapters",
|
"LabelChapters": "Chapters",
|
||||||
"LabelChaptersFound": "chapters found",
|
"LabelChaptersFound": "chapters found",
|
||||||
|
"LabelCleanupAuthors": "Cleanup authors",
|
||||||
|
"LabelCleanupAuthorsHelp": "Remove authors that have no books in this library.",
|
||||||
"LabelClickForMoreInfo": "Click for more info",
|
"LabelClickForMoreInfo": "Click for more info",
|
||||||
"LabelClickToUseCurrentValue": "Click to use current value",
|
"LabelClickToUseCurrentValue": "Click to use current value",
|
||||||
"LabelClosePlayer": "Close player",
|
"LabelClosePlayer": "Close player",
|
||||||
|
|
@ -800,6 +802,7 @@
|
||||||
"MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?",
|
"MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?",
|
||||||
"MessageConfirmRemoveItemsWithIssues": "Are you sure you want to remove all items with issues?",
|
"MessageConfirmRemoveItemsWithIssues": "Are you sure you want to remove all items with issues?",
|
||||||
|
"MessageConfirmCleanupAuthors": "Are you sure you want to remove all authors with no books in this library?",
|
||||||
"MessageConfirmRemoveEpisodeNote": "Note: This does not delete the audio file unless toggling \"Hard delete file\"",
|
"MessageConfirmRemoveEpisodeNote": "Note: This does not delete the audio file unless toggling \"Hard delete file\"",
|
||||||
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
|
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
|
||||||
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
|
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
|
||||||
|
|
@ -1085,6 +1088,9 @@
|
||||||
"ToastLibraryItemsWithIssuesNoneFound": "No items with issues found",
|
"ToastLibraryItemsWithIssuesNoneFound": "No items with issues found",
|
||||||
"ToastLibraryItemsWithIssuesRemoved": "Removed items with issues",
|
"ToastLibraryItemsWithIssuesRemoved": "Removed items with issues",
|
||||||
"ToastLibraryItemsWithIssuesRemoveFailed": "Failed to remove items with issues",
|
"ToastLibraryItemsWithIssuesRemoveFailed": "Failed to remove items with issues",
|
||||||
|
"ToastCleanupAuthorsSuccess": "Cleaned up {0} authors",
|
||||||
|
"ToastCleanupAuthorsNoAuthors": "No authors found to cleanup",
|
||||||
|
"ToastCleanupAuthorsFailed": "Failed to cleanup authors",
|
||||||
"ToastLibraryScanStarted": "Library scan started",
|
"ToastLibraryScanStarted": "Library scan started",
|
||||||
"ToastLibraryUpdateSuccess": "Library \"{0}\" updated",
|
"ToastLibraryUpdateSuccess": "Library \"{0}\" updated",
|
||||||
"ToastMatchAllAuthorsFailed": "Failed to match all authors",
|
"ToastMatchAllAuthorsFailed": "Failed to match all authors",
|
||||||
|
|
|
||||||
|
|
@ -1412,6 +1412,57 @@ class LibraryController {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE: /api/libraries/:id/authors/cleanup
|
||||||
|
* Remove authors with no books, no description and no image
|
||||||
|
*
|
||||||
|
* @this {import('../routers/ApiRouter')}
|
||||||
|
*
|
||||||
|
* @param {LibraryControllerRequest} req
|
||||||
|
* @param {Response} res
|
||||||
|
*/
|
||||||
|
async cleanupAuthorsWithNoBooks(req, res) {
|
||||||
|
if (!req.user.isAdminOrUp) {
|
||||||
|
Logger.error(`[LibraryController] Non-admin user "${req.user.username}" attempted to cleanup authors`)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
const force = req.query.force === '1'
|
||||||
|
|
||||||
|
const authors = await Database.authorModel.findAll({
|
||||||
|
where: {
|
||||||
|
libraryId: req.library.id
|
||||||
|
},
|
||||||
|
attributes: ['id']
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!authors.length) {
|
||||||
|
return res.json({
|
||||||
|
removed: 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const authorIds = authors.map((a) => a.id)
|
||||||
|
const initialCount = authorIds.length
|
||||||
|
|
||||||
|
// This method is defined on ApiRouter
|
||||||
|
await this.checkRemoveAuthorsWithNoBooks(authorIds, force)
|
||||||
|
|
||||||
|
// Check how many are left
|
||||||
|
const remainingCount = await Database.authorModel.count({
|
||||||
|
where: {
|
||||||
|
id: authorIds
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const removed = initialCount - remainingCount
|
||||||
|
Logger.info(`[LibraryController] Cleaned up ${removed} authors (force=${force}) with no books for library "${req.library.name}"`)
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
removed
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET: /api/library/:id/download
|
* GET: /api/library/:id/download
|
||||||
* Downloads multiple library items
|
* Downloads multiple library items
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ class ApiRouter {
|
||||||
this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this))
|
this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this))
|
||||||
this.router.get('/libraries/:id/stats', LibraryController.middleware.bind(this), LibraryController.stats.bind(this))
|
this.router.get('/libraries/:id/stats', LibraryController.middleware.bind(this), LibraryController.stats.bind(this))
|
||||||
this.router.get('/libraries/:id/authors', LibraryController.middleware.bind(this), LibraryController.getAuthors.bind(this))
|
this.router.get('/libraries/:id/authors', LibraryController.middleware.bind(this), LibraryController.getAuthors.bind(this))
|
||||||
|
this.router.delete('/libraries/:id/authors/cleanup', LibraryController.middleware.bind(this), LibraryController.cleanupAuthorsWithNoBooks.bind(this))
|
||||||
this.router.get('/libraries/:id/narrators', LibraryController.middleware.bind(this), LibraryController.getNarrators.bind(this))
|
this.router.get('/libraries/:id/narrators', LibraryController.middleware.bind(this), LibraryController.getNarrators.bind(this))
|
||||||
this.router.patch('/libraries/:id/narrators/:narratorId', LibraryController.middleware.bind(this), LibraryController.updateNarrator.bind(this))
|
this.router.patch('/libraries/:id/narrators/:narratorId', LibraryController.middleware.bind(this), LibraryController.updateNarrator.bind(this))
|
||||||
this.router.delete('/libraries/:id/narrators/:narratorId', LibraryController.middleware.bind(this), LibraryController.removeNarrator.bind(this))
|
this.router.delete('/libraries/:id/narrators/:narratorId', LibraryController.middleware.bind(this), LibraryController.removeNarrator.bind(this))
|
||||||
|
|
@ -464,29 +465,38 @@ class ApiRouter {
|
||||||
* @param {string[]} authorIds
|
* @param {string[]} authorIds
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async checkRemoveAuthorsWithNoBooks(authorIds) {
|
async checkRemoveAuthorsWithNoBooks(authorIds, force = false) {
|
||||||
if (!authorIds?.length) return
|
if (!authorIds?.length) return
|
||||||
|
|
||||||
const transaction = await Database.sequelize.transaction()
|
const transaction = await Database.sequelize.transaction()
|
||||||
try {
|
try {
|
||||||
|
const where = [
|
||||||
|
sequelize.where(sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 0)
|
||||||
|
]
|
||||||
|
|
||||||
|
if (!force) {
|
||||||
|
where.push({
|
||||||
|
id: authorIds,
|
||||||
|
asin: {
|
||||||
|
[sequelize.Op.or]: [null, '']
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
[sequelize.Op.or]: [null, '']
|
||||||
|
},
|
||||||
|
imagePath: {
|
||||||
|
[sequelize.Op.or]: [null, '']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
where.push({
|
||||||
|
id: authorIds
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Select authors with locking to prevent concurrent updates
|
// Select authors with locking to prevent concurrent updates
|
||||||
const bookAuthorsToRemove = (
|
const bookAuthorsToRemove = (
|
||||||
await Database.authorModel.findAll({
|
await Database.authorModel.findAll({
|
||||||
where: [
|
where,
|
||||||
{
|
|
||||||
id: authorIds,
|
|
||||||
asin: {
|
|
||||||
[sequelize.Op.or]: [null, '']
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
[sequelize.Op.or]: [null, '']
|
|
||||||
},
|
|
||||||
imagePath: {
|
|
||||||
[sequelize.Op.or]: [null, '']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sequelize.where(sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 0)
|
|
||||||
],
|
|
||||||
attributes: ['id', 'name', 'libraryId'],
|
attributes: ['id', 'name', 'libraryId'],
|
||||||
raw: true,
|
raw: true,
|
||||||
transaction
|
transaction
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue