Add:Option to hard delete podcast episode from file system #488

This commit is contained in:
advplyr 2022-05-24 18:38:25 -05:00
parent 3e98b6f749
commit 5187d0e55f
6 changed files with 153 additions and 53 deletions

View 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>

View File

@ -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)
}
}
}

View File

@ -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>

View File

@ -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

View File

@ -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)

View File

@ -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