From 179f11f55dffaf47a1f215461084360b591e358e Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 14 Apr 2023 16:44:41 -0500 Subject: [PATCH 1/5] Add:Delete library items from file system #1439 --- client/components/app/Appbar.vue | 49 ++++++++++++------- client/components/cards/LazyBookCard.vue | 37 ++++++++++++++ .../components/modals/item/tabs/Details.vue | 27 +--------- client/components/prompt/Confirm.vue | 25 ++++++++-- client/pages/item/_id/index.vue | 24 +++++---- server/controllers/LibraryItemController.js | 24 +++++++-- 6 files changed, 125 insertions(+), 61 deletions(-) diff --git a/client/components/app/Appbar.vue b/client/components/app/Appbar.vue index 382f5cf3..d625dac7 100644 --- a/client/components/app/Appbar.vue +++ b/client/components/app/Appbar.vue @@ -287,26 +287,37 @@ export default { }) }, batchDeleteClick() { - const audiobookText = this.numMediaItemsSelected > 1 ? `these ${this.numMediaItemsSelected} items` : 'this item' - const confirmMsg = `Are you sure you want to remove ${audiobookText}?\n\n*Does not delete your files, only removes the items from Audiobookshelf` - if (confirm(confirmMsg)) { - this.$store.commit('setProcessingBatch', true) - this.$axios - .$post(`/api/items/batch/delete`, { - libraryItemIds: this.selectedMediaItems.map((i) => i.id) - }) - .then(() => { - this.$toast.success('Batch delete success!') - this.$store.commit('setProcessingBatch', false) - this.$store.commit('globals/resetSelectedMediaItems', []) - this.$eventBus.$emit('bookshelf_clear_selection') - }) - .catch((error) => { - this.$toast.error('Batch delete failed') - console.error('Failed to batch delete', error) - this.$store.commit('setProcessingBatch', false) - }) + const payload = { + message: `This will delete ${this.numMediaItemsSelected} library items from the database and your file system. Are you sure?`, + checkboxLabel: 'Delete from file system. Uncheck to only remove from database.', + yesButtonText: this.$strings.ButtonDelete, + yesButtonColor: 'error', + checkboxDefaultValue: true, + callback: (confirmed, hardDelete) => { + if (confirmed) { + this.$store.commit('setProcessingBatch', true) + + this.$axios + .$post(`/api/items/batch/delete?hard=${hardDelete ? 1 : 0}`, { + libraryItemIds: this.selectedMediaItems.map((i) => i.id) + }) + .then(() => { + this.$toast.success('Batch delete success') + this.$store.commit('globals/resetSelectedMediaItems', []) + this.$eventBus.$emit('bookshelf_clear_selection') + }) + .catch((error) => { + console.error('Batch delete failed', error) + this.$toast.error('Batch delete failed') + }) + .finally(() => { + this.$store.commit('setProcessingBatch', false) + }) + } + }, + type: 'yesNo' } + this.$store.commit('globals/setConfirmPrompt', payload) }, batchEditClick() { this.$router.push('/batch') diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index c4d3e967..42a3be4e 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -526,6 +526,14 @@ export default { } } } + + if (this.userCanDelete) { + items.push({ + func: 'deleteLibraryItem', + text: this.$strings.ButtonDelete + }) + } + return items }, _socket() { @@ -777,6 +785,35 @@ export default { this.store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem, episode: this.recentEpisode }]) this.store.commit('globals/setShowPlaylistsModal', true) }, + deleteLibraryItem() { + const payload = { + message: 'This will delete the library item from the database and your file system. Are you sure?', + checkboxLabel: 'Delete from file system. Uncheck to only remove from database.', + yesButtonText: this.$strings.ButtonDelete, + yesButtonColor: 'error', + checkboxDefaultValue: true, + callback: (confirmed, hardDelete) => { + if (confirmed) { + this.processing = true + const axios = this.$axios || this.$nuxt.$axios + axios + .$delete(`/api/items/${this.libraryItemId}?hard=${hardDelete ? 1 : 0}`) + .then(() => { + this.$toast.success('Item deleted') + }) + .catch((error) => { + console.error('Failed to delete item', error) + this.$toast.error('Failed to delete item') + }) + .finally(() => { + this.processing = false + }) + } + }, + type: 'yesNo' + } + this.store.commit('globals/setConfirmPrompt', payload) + }, createMoreMenu() { if (!this.$refs.moreIcon) return diff --git a/client/components/modals/item/tabs/Details.vue b/client/components/modals/item/tabs/Details.vue index e95b8d57..14fe68a7 100644 --- a/client/components/modals/item/tabs/Details.vue +++ b/client/components/modals/item/tabs/Details.vue @@ -7,11 +7,6 @@
- - - -
- {{ $strings.ButtonQuickMatch }} @@ -20,6 +15,8 @@ {{ $strings.ButtonReScan }} +
+ @@ -77,9 +74,6 @@ export default { mediaMetadata() { return this.media.metadata || {} }, - userCanDelete() { - return this.$store.getters['user/getUserCanDelete'] - }, libraryId() { return this.libraryItem ? this.libraryItem.libraryId : null }, @@ -184,23 +178,6 @@ export default { } return false }, - removeItem() { - if (confirm(`Are you sure you want to remove this item?\n\n*Does not delete your files, only removes the item from audiobookshelf`)) { - this.isProcessing = true - this.$axios - .$delete(`/api/items/${this.libraryItemId}`) - .then(() => { - console.log('Item removed') - this.$toast.success('Item Removed') - this.$emit('close') - this.isProcessing = false - }) - .catch((error) => { - console.error('Remove item failed', error) - this.isProcessing = false - }) - } - }, checkIsScrollable() { this.$nextTick(() => { var formWrapper = document.getElementById('formWrapper') diff --git a/client/components/prompt/Confirm.vue b/client/components/prompt/Confirm.vue index 00e38e08..b09bf55d 100644 --- a/client/components/prompt/Confirm.vue +++ b/client/components/prompt/Confirm.vue @@ -3,11 +3,14 @@
-

+

+ + +

{{ $strings.ButtonCancel }}
- {{ $strings.ButtonYes }} + {{ yesButtonText }} {{ $strings.ButtonOk }}
@@ -21,7 +24,8 @@ export default { data() { return { el: null, - content: null + content: null, + checkboxValue: false } }, watch: { @@ -57,6 +61,18 @@ export default { persistent() { return !!this.confirmPromptOptions.persistent }, + checkboxLabel() { + return this.confirmPromptOptions.checkboxLabel + }, + yesButtonText() { + return this.confirmPromptOptions.yesButtonText || this.$strings.ButtonYes + }, + yesButtonColor() { + return this.confirmPromptOptions.yesButtonColor || 'success' + }, + checkboxDefaultValue() { + return !!this.confirmPromptOptions.checkboxDefaultValue + }, isYesNo() { return this.type === 'yesNo' }, @@ -84,10 +100,11 @@ export default { this.show = false }, confirm() { - if (this.callback) this.callback(true) + if (this.callback) this.callback(true, this.checkboxValue) this.show = false }, setShow() { + this.checkboxValue = this.checkboxDefaultValue this.$eventBus.$emit('showing-prompt', true) document.body.appendChild(this.el) setTimeout(() => { diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index 2d376646..88b15fc1 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -562,12 +562,12 @@ export default { }) } - // if (this.userCanDelete) { - // items.push({ - // text: this.$strings.ButtonDelete, - // action: 'delete' - // }) - // } + if (this.userCanDelete) { + items.push({ + text: this.$strings.ButtonDelete, + action: 'delete' + }) + } return items } @@ -818,14 +818,18 @@ export default { }, deleteLibraryItem() { const payload = { - message: 'This will delete the library item files from your file system. Are you sure?', - callback: (confirmed) => { + message: 'This will delete the library item from the database and your file system. Are you sure?', + checkboxLabel: 'Delete from file system. Uncheck to only remove from database.', + yesButtonText: this.$strings.ButtonDelete, + yesButtonColor: 'error', + checkboxDefaultValue: true, + callback: (confirmed, hardDelete) => { if (confirmed) { this.$axios - .$delete(`/api/items/${this.libraryItemId}?hard=1`) + .$delete(`/api/items/${this.libraryItemId}?hard=${hardDelete ? 1 : 0}`) .then(() => { this.$toast.success('Item deleted') - this.$router.replace(`/library/${this.libraryId}/bookshelf`) + this.$router.replace(`/library/${this.libraryId}`) }) .catch((error) => { console.error('Failed to delete item', error) diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index 842db177..19f287f0 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -66,7 +66,15 @@ class LibraryItemController { } async delete(req, res) { + const hardDelete = req.query.hard == 1 // Delete from file system + const libraryItemPath = req.libraryItem.path await this.handleDeleteLibraryItem(req.libraryItem) + if (hardDelete) { + Logger.info(`[LibraryItemController] Deleting library item from file system at "${libraryItemPath}"`) + await fs.remove(libraryItemPath).catch((error) => { + Logger.error(`[LibraryItemController] Failed to delete library item from file system at "${libraryItemPath}"`, error) + }) + } res.sendStatus(200) } @@ -292,19 +300,27 @@ class LibraryItemController { Logger.warn(`[LibraryItemController] User attempted to delete without permission`, req.user) return res.sendStatus(403) } + const hardDelete = req.query.hard == 1 // Delete files from filesystem - var { libraryItemIds } = req.body + const { libraryItemIds } = req.body if (!libraryItemIds || !libraryItemIds.length) { return res.sendStatus(500) } - var itemsToDelete = this.db.libraryItems.filter(li => libraryItemIds.includes(li.id)) + const itemsToDelete = this.db.libraryItems.filter(li => libraryItemIds.includes(li.id)) if (!itemsToDelete.length) { return res.sendStatus(404) } for (let i = 0; i < itemsToDelete.length; i++) { + const libraryItemPath = itemsToDelete[i].path Logger.info(`[LibraryItemController] Deleting Library Item "${itemsToDelete[i].media.metadata.title}"`) await this.handleDeleteLibraryItem(itemsToDelete[i]) + if (hardDelete) { + Logger.info(`[LibraryItemController] Deleting library item from file system at "${libraryItemPath}"`) + await fs.remove(libraryItemPath).catch((error) => { + Logger.error(`[LibraryItemController] Failed to delete library item from file system at "${libraryItemPath}"`, error) + }) + } } res.sendStatus(200) } @@ -489,7 +505,9 @@ class LibraryItemController { return res.sendStatus(404) } - await fs.remove(libraryFile.metadata.path) + await fs.remove(libraryFile.metadata.path).catch((error) => { + Logger.error(`[LibraryItemController] Failed to delete library file at "${libraryFile.metadata.path}"`, error) + }) req.libraryItem.removeLibraryFile(req.params.ino) if (req.libraryItem.media.removeFileWithInode(req.params.ino)) { From eab019c5770d06784a45997ed438123e5b2558d2 Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 14 Apr 2023 16:48:24 -0500 Subject: [PATCH 2/5] Use horizontal kebab icon --- client/pages/item/_id/index.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index 88b15fc1..d03e4643 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -209,7 +209,7 @@ From 03984f96d4450f5c163f984e80a86844a0fca19b Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 15 Apr 2023 16:21:16 -0500 Subject: [PATCH 3/5] Remove experimental tone probe --- client/components/tables/TracksTable.vue | 35 ------------------------ 1 file changed, 35 deletions(-) diff --git a/client/components/tables/TracksTable.vue b/client/components/tables/TracksTable.vue index 66d11f72..a4abaf94 100644 --- a/client/components/tables/TracksTable.vue +++ b/client/components/tables/TracksTable.vue @@ -24,14 +24,6 @@ {{ $strings.LabelSize }} {{ $strings.LabelDuration }} {{ $strings.LabelDownload }} - -
-

Tone

- - information - -
- @@ -92,35 +81,11 @@ export default { }, userIsAdmin() { return this.$store.getters['user/getIsAdminOrUp'] - }, - showExperimentalFeatures() { - return this.$store.state.showExperimentalFeatures } }, methods: { clickBar() { this.showTracks = !this.showTracks - }, - toneProbe(index) { - this.toneProbing = true - - this.$axios - .$post(`/api/items/${this.libraryItemId}/tone-scan/${index}`) - .then((data) => { - console.log('Tone probe data', data) - if (data.error) { - this.$toast.error('Tone probe error: ' + data.error) - } else { - this.$toast.success('Tone probe successful! Check browser console') - } - }) - .catch((error) => { - console.error('Failed to tone probe', error) - this.$toast.error('Tone probe failed') - }) - .finally(() => { - this.toneProbing = false - }) } }, mounted() {} From 8542d433a27dd7b0d905f9fd3b1ceeec727bbff1 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 15 Apr 2023 18:09:49 -0500 Subject: [PATCH 4/5] Add:Audio file info modal #1667 --- .../components/modals/AudioFileDataModal.vue | 118 +++++++++++++++++ client/components/modals/item/tabs/Files.vue | 5 +- .../components/tables/AudioTracksTableRow.vue | 123 ++++++++++++++++++ .../components/tables/LibraryFilesTable.vue | 43 ++++-- .../tables/LibraryFilesTableRow.vue | 17 ++- client/components/tables/TracksTable.vue | 40 +++--- client/components/widgets/AudiobookData.vue | 8 +- client/pages/item/_id/index.vue | 10 +- client/strings/de.json | 9 ++ client/strings/en-us.json | 9 ++ client/strings/es.json | 9 ++ client/strings/fr.json | 9 ++ client/strings/gu.json | 9 ++ client/strings/hi.json | 9 ++ client/strings/hr.json | 9 ++ client/strings/it.json | 9 ++ client/strings/pl.json | 9 ++ client/strings/ru.json | 9 ++ client/strings/zh-cn.json | 9 ++ 19 files changed, 419 insertions(+), 44 deletions(-) create mode 100644 client/components/modals/AudioFileDataModal.vue create mode 100644 client/components/tables/AudioTracksTableRow.vue diff --git a/client/components/modals/AudioFileDataModal.vue b/client/components/modals/AudioFileDataModal.vue new file mode 100644 index 00000000..fae88056 --- /dev/null +++ b/client/components/modals/AudioFileDataModal.vue @@ -0,0 +1,118 @@ + + + diff --git a/client/components/modals/item/tabs/Files.vue b/client/components/modals/item/tabs/Files.vue index 386aa643..4081f98c 100644 --- a/client/components/modals/item/tabs/Files.vue +++ b/client/components/modals/item/tabs/Files.vue @@ -1,6 +1,6 @@ @@ -30,9 +30,6 @@ export default { media() { return this.libraryItem.media || {} }, - libraryFiles() { - return this.libraryItem.libraryFiles || [] - }, userToken() { return this.$store.getters['user/getToken'] }, diff --git a/client/components/tables/AudioTracksTableRow.vue b/client/components/tables/AudioTracksTableRow.vue new file mode 100644 index 00000000..837aaa1a --- /dev/null +++ b/client/components/tables/AudioTracksTableRow.vue @@ -0,0 +1,123 @@ + + + \ No newline at end of file diff --git a/client/components/tables/LibraryFilesTable.vue b/client/components/tables/LibraryFilesTable.vue index b73c96b0..12ad86e5 100644 --- a/client/components/tables/LibraryFilesTable.vue +++ b/client/components/tables/LibraryFilesTable.vue @@ -18,35 +18,42 @@ {{ $strings.LabelPath }} {{ $strings.LabelSize }} {{ $strings.LabelType }} - + - diff --git a/client/components/widgets/ExplicitIndicator.vue b/client/components/widgets/ExplicitIndicator.vue index 3ff68603..6a03d925 100644 --- a/client/components/widgets/ExplicitIndicator.vue +++ b/client/components/widgets/ExplicitIndicator.vue @@ -1,6 +1,40 @@ diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index 38173340..f03d2516 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -28,6 +28,7 @@
{{ title }} +
@@ -124,14 +125,6 @@ {{ sizePretty }}
-
-
- {{ $strings.LabelAbridged }} -
-
- {{ isAbridged ? 'Yes' : 'No' }} -
-