mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Add:Option to hard delete podcast episode from file system #488
This commit is contained in:
		
							parent
							
								
									3e98b6f749
								
							
						
					
					
						commit
						5187d0e55f
					
				
							
								
								
									
										90
									
								
								client/components/modals/podcast/RemoveEpisode.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								client/components/modals/podcast/RemoveEpisode.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,90 @@ | ||||
| <template> | ||||
|   <modals-modal v-model="show" name="podcast-episode-remove-modal" :width="500" :height="'unset'" :processing="processing"> | ||||
|     <template #outer> | ||||
|       <div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden"> | ||||
|         <p class="font-book text-3xl text-white truncate">{{ title }}</p> | ||||
|       </div> | ||||
|     </template> | ||||
|     <div ref="wrapper" class="px-8 py-6 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden"> | ||||
|       <div class="mb-4"> | ||||
|         <p class="text-lg text-gray-200 mb-4"> | ||||
|           Are you sure you want to remove episode<br /><span class="text-base">{{ episodeTitle }}</span | ||||
|           >? | ||||
|         </p> | ||||
|         <p class="text-xs font-semibold text-warning text-opacity-90">Note: This does not delete the audio file unless toggling "Hard delete file"</p> | ||||
|       </div> | ||||
|       <div class="flex justify-between items-center pt-4"> | ||||
|         <ui-checkbox v-model="hardDeleteFile" label="Hard delete file" check-color="error" checkbox-bg="bg" small label-class="text-base text-gray-200 pl-3" /> | ||||
| 
 | ||||
|         <ui-btn @click="submit">{{ hardDeleteFile ? 'Delete episode' : 'Remove episode' }}</ui-btn> | ||||
|       </div> | ||||
|     </div> | ||||
|   </modals-modal> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   props: { | ||||
|     value: Boolean, | ||||
|     libraryItem: { | ||||
|       type: Object, | ||||
|       default: () => {} | ||||
|     }, | ||||
|     episode: { | ||||
|       type: Object, | ||||
|       default: () => {} | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       hardDeleteFile: false, | ||||
|       processing: false | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     value(newVal) { | ||||
|       if (newVal) this.hardDeleteFile = false | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     show: { | ||||
|       get() { | ||||
|         return this.value | ||||
|       }, | ||||
|       set(val) { | ||||
|         this.$emit('input', val) | ||||
|       } | ||||
|     }, | ||||
|     title() { | ||||
|       return 'Remove Episode' | ||||
|     }, | ||||
|     episodeId() { | ||||
|       return this.episode ? this.episode.id : null | ||||
|     }, | ||||
|     episodeTitle() { | ||||
|       return this.episode ? this.episode.title : null | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     submit() { | ||||
|       this.processing = true | ||||
| 
 | ||||
|       var queryString = this.hardDeleteFile ? '?hard=1' : '' | ||||
|       this.$axios | ||||
|         .$delete(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}${queryString}`) | ||||
|         .then(() => { | ||||
|           this.processing = false | ||||
|           this.$toast.success('Podcast episode removed') | ||||
|           this.show = false | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           var errorMsg = error.response && error.response.data ? error.response.data : 'Failed remove episode' | ||||
|           console.error('Failed update episode', error) | ||||
|           this.processing = false | ||||
|           this.$toast.error(errorMsg) | ||||
|         }) | ||||
|     } | ||||
|   }, | ||||
|   mounted() {} | ||||
| } | ||||
| </script> | ||||
| @ -45,7 +45,6 @@ export default { | ||||
|       type: Object, | ||||
|       default: () => {} | ||||
|     } | ||||
|     // isDragging: Boolean | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
| @ -54,15 +53,6 @@ export default { | ||||
|       isHovering: false | ||||
|     } | ||||
|   }, | ||||
|   // watch: { | ||||
|   //   isDragging: { | ||||
|   //     handler(newVal) { | ||||
|   //       if (newVal) { | ||||
|   //         this.isHovering = false | ||||
|   //       } | ||||
|   //     } | ||||
|   //   } | ||||
|   // }, | ||||
|   computed: { | ||||
|     userCanUpdate() { | ||||
|       return this.$store.getters['user/getUserCanUpdate'] | ||||
| @ -149,22 +139,7 @@ export default { | ||||
|         }) | ||||
|     }, | ||||
|     removeClick() { | ||||
|       if (confirm(`Are you sure you want to remove episode ${this.title}?\nNote: Does not delete from file system`)) { | ||||
|         this.processingRemove = true | ||||
| 
 | ||||
|         this.$axios | ||||
|           .$delete(`/api/items/${this.libraryItemId}/episode/${this.episode.id}`) | ||||
|           .then((updatedPodcast) => { | ||||
|             console.log(`Episode removed from podcast`, updatedPodcast) | ||||
|             this.$toast.success('Episode removed from podcast') | ||||
|             this.processingRemove = false | ||||
|           }) | ||||
|           .catch((error) => { | ||||
|             console.error('Failed to remove episode from podcast', error) | ||||
|             this.$toast.error('Failed to remove episode from podcast') | ||||
|             this.processingRemove = false | ||||
|           }) | ||||
|       } | ||||
|       this.$emit('remove', this.episode) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -3,15 +3,14 @@ | ||||
|     <div class="flex items-center mb-4"> | ||||
|       <p class="text-lg mb-0 font-semibold">Episodes</p> | ||||
|       <div class="flex-grow" /> | ||||
|       <controls-episode-sort-select v-model="sortKey" :descending.sync="sortDesc" class="w-36 sm:w-44 md:w-48 h-9 ml-1 sm:ml-4" @change="changeSort" /> | ||||
|       <div v-if="userCanUpdate" class="w-12"> | ||||
|         <ui-icon-btn v-if="orderChanged" :loading="savingOrder" icon="save" bg-color="primary" class="ml-auto" @click="saveOrder" /> | ||||
|       </div> | ||||
|       <controls-episode-sort-select v-model="sortKey" :descending.sync="sortDesc" class="w-36 sm:w-44 md:w-48 h-9 ml-1 sm:ml-4" /> | ||||
|     </div> | ||||
|     <p v-if="!episodes.length" class="py-4 text-center text-lg">No Episodes</p> | ||||
|     <template v-for="episode in episodes"> | ||||
|       <tables-podcast-episode-table-row :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" class="item" @edit="editEpisode" /> | ||||
|     <template v-for="episode in episodesSorted"> | ||||
|       <tables-podcast-episode-table-row :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" class="item" @remove="removeEpisode" @edit="editEpisode" /> | ||||
|     </template> | ||||
| 
 | ||||
|     <modals-podcast-remove-episode v-model="showPodcastRemoveModal" :library-item="libraryItem" :episode="selectedEpisode" /> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| @ -25,8 +24,16 @@ export default { | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       episodesCopy: [], | ||||
|       sortKey: 'publishedAt', | ||||
|       sortDesc: true | ||||
|       sortDesc: true, | ||||
|       selectedEpisode: null, | ||||
|       showPodcastRemoveModal: false | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     libraryItem() { | ||||
|       this.init() | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
| @ -41,16 +48,33 @@ export default { | ||||
|     }, | ||||
|     episodes() { | ||||
|       return this.media.episodes || [] | ||||
|     }, | ||||
|     episodesSorted() { | ||||
|       return this.episodesCopy.sort((a, b) => { | ||||
|         if (this.sortDesc) { | ||||
|           return String(b[this.sortKey]).localeCompare(String(a[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' }) | ||||
|         } | ||||
|         return String(a[this.sortKey]).localeCompare(String(b[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' }) | ||||
|       }) | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     removeEpisode(episode) { | ||||
|       this.selectedEpisode = episode | ||||
|       this.showPodcastRemoveModal = true | ||||
|     }, | ||||
|     editEpisode(episode) { | ||||
|       this.$store.commit('setSelectedLibraryItem', this.libraryItem) | ||||
|       this.$store.commit('globals/setSelectedEpisode', episode) | ||||
|       this.$store.commit('globals/setShowEditPodcastEpisodeModal', true) | ||||
|     }, | ||||
|     init() { | ||||
|       this.episodesCopy = this.episodes.map((ep) => ({ ...ep })) | ||||
|     } | ||||
|   }, | ||||
|   mounted() {} | ||||
|   mounted() { | ||||
|     this.init() | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
|  | ||||
| @ -224,24 +224,6 @@ class LibraryItemController { | ||||
|     res.json(libraryItem.toJSON()) | ||||
|   } | ||||
| 
 | ||||
|   // DELETE: api/items/:id/episode/:episodeId
 | ||||
|   async removeEpisode(req, res) { | ||||
|     var episodeId = req.params.episodeId | ||||
|     var libraryItem = req.libraryItem | ||||
|     if (libraryItem.mediaType !== 'podcast') { | ||||
|       Logger.error(`[LibraryItemController] removeEpisode invalid media type ${libraryItem.id}`) | ||||
|       return res.sendStatus(500) | ||||
|     } | ||||
|     if (!libraryItem.media.episodes.find(ep => ep.id === episodeId)) { | ||||
|       Logger.error(`[LibraryItemController] removeEpisode episode ${episodeId} not found for item ${libraryItem.id}`) | ||||
|       return res.sendStatus(404) | ||||
|     } | ||||
|     libraryItem.media.removeEpisode(episodeId) | ||||
|     await this.db.updateLibraryItem(libraryItem) | ||||
|     this.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||
|     res.json(libraryItem.toJSON()) | ||||
|   } | ||||
| 
 | ||||
|   // POST api/items/:id/match
 | ||||
|   async match(req, res) { | ||||
|     var libraryItem = req.libraryItem | ||||
|  | ||||
| @ -190,6 +190,35 @@ class PodcastController { | ||||
|     res.json(libraryItem.toJSONExpanded()) | ||||
|   } | ||||
| 
 | ||||
|   // DELETE: api/podcasts/:id/episode/:episodeId
 | ||||
|   async removeEpisode(req, res) { | ||||
|     var episodeId = req.params.episodeId | ||||
|     var libraryItem = req.libraryItem | ||||
|     var hardDelete = req.query.hard === '1' | ||||
| 
 | ||||
|     var episode = libraryItem.media.episodes.find(ep => ep.id === episodeId) | ||||
|     if (!episode) { | ||||
|       Logger.error(`[PodcastController] removeEpisode episode ${episodeId} not found for item ${libraryItem.id}`) | ||||
|       return res.sendStatus(404) | ||||
|     } | ||||
| 
 | ||||
|     if (hardDelete) { | ||||
|       var audioFile = episode.audioFile | ||||
|       // TODO: this will trigger the watcher. should maybe handle this gracefully
 | ||||
|       await fs.remove(audioFile.metadata.path).then(() => { | ||||
|         Logger.info(`[PodcastController] Hard deleted episode file at "${audioFile.metadata.path}"`) | ||||
|       }).catch((error) => { | ||||
|         Logger.error(`[PodcastController] Failed to hard delete episode file at "${audioFile.metadata.path}"`, error) | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     libraryItem.media.removeEpisode(episodeId) | ||||
| 
 | ||||
|     await this.db.updateLibraryItem(libraryItem) | ||||
|     this.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||
|     res.json(libraryItem.toJSON()) | ||||
|   } | ||||
| 
 | ||||
|   middleware(req, res, next) { | ||||
|     var item = this.db.libraryItems.find(li => li.id === req.params.id) | ||||
|     if (!item || !item.media) return res.sendStatus(404) | ||||
|  | ||||
| @ -90,7 +90,6 @@ class ApiRouter { | ||||
|     this.router.post('/items/:id/play', LibraryItemController.middleware.bind(this), LibraryItemController.startPlaybackSession.bind(this)) | ||||
|     this.router.post('/items/:id/play/:episodeId', LibraryItemController.middleware.bind(this), LibraryItemController.startEpisodePlaybackSession.bind(this)) | ||||
|     this.router.patch('/items/:id/tracks', LibraryItemController.middleware.bind(this), LibraryItemController.updateTracks.bind(this)) | ||||
|     this.router.delete('/items/:id/episode/:episodeId', LibraryItemController.middleware.bind(this), LibraryItemController.removeEpisode.bind(this)) | ||||
|     this.router.get('/items/:id/scan', LibraryItemController.middleware.bind(this), LibraryItemController.scan.bind(this)) | ||||
|     this.router.get('/items/:id/audio-metadata', LibraryItemController.middleware.bind(this), LibraryItemController.updateAudioFileMetadata.bind(this)) | ||||
|     this.router.post('/items/:id/chapters', LibraryItemController.middleware.bind(this), LibraryItemController.updateMediaChapters.bind(this)) | ||||
| @ -188,6 +187,7 @@ class ApiRouter { | ||||
|     this.router.get('/podcasts/:id/clear-queue', PodcastController.middleware.bind(this), PodcastController.clearEpisodeDownloadQueue.bind(this)) | ||||
|     this.router.post('/podcasts/:id/download-episodes', PodcastController.middleware.bind(this), PodcastController.downloadEpisodes.bind(this)) | ||||
|     this.router.patch('/podcasts/:id/episode/:episodeId', PodcastController.middleware.bind(this), PodcastController.updateEpisode.bind(this)) | ||||
|     this.router.delete('/podcasts/:id/episode/:episodeId', PodcastController.middleware.bind(this), PodcastController.removeEpisode.bind(this)) | ||||
| 
 | ||||
|     //
 | ||||
|     // Misc Routes
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user