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,
 | 
					      type: Object,
 | 
				
			||||||
      default: () => {}
 | 
					      default: () => {}
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    // isDragging: Boolean
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
@ -54,15 +53,6 @@ export default {
 | 
				
			|||||||
      isHovering: false
 | 
					      isHovering: false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  // watch: {
 | 
					 | 
				
			||||||
  //   isDragging: {
 | 
					 | 
				
			||||||
  //     handler(newVal) {
 | 
					 | 
				
			||||||
  //       if (newVal) {
 | 
					 | 
				
			||||||
  //         this.isHovering = false
 | 
					 | 
				
			||||||
  //       }
 | 
					 | 
				
			||||||
  //     }
 | 
					 | 
				
			||||||
  //   }
 | 
					 | 
				
			||||||
  // },
 | 
					 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
    userCanUpdate() {
 | 
					    userCanUpdate() {
 | 
				
			||||||
      return this.$store.getters['user/getUserCanUpdate']
 | 
					      return this.$store.getters['user/getUserCanUpdate']
 | 
				
			||||||
@ -149,22 +139,7 @@ export default {
 | 
				
			|||||||
        })
 | 
					        })
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    removeClick() {
 | 
					    removeClick() {
 | 
				
			||||||
      if (confirm(`Are you sure you want to remove episode ${this.title}?\nNote: Does not delete from file system`)) {
 | 
					      this.$emit('remove', this.episode)
 | 
				
			||||||
        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
 | 
					 | 
				
			||||||
          })
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -3,15 +3,14 @@
 | 
				
			|||||||
    <div class="flex items-center mb-4">
 | 
					    <div class="flex items-center mb-4">
 | 
				
			||||||
      <p class="text-lg mb-0 font-semibold">Episodes</p>
 | 
					      <p class="text-lg mb-0 font-semibold">Episodes</p>
 | 
				
			||||||
      <div class="flex-grow" />
 | 
					      <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" />
 | 
					      <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 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>
 | 
					 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <p v-if="!episodes.length" class="py-4 text-center text-lg">No Episodes</p>
 | 
					    <p v-if="!episodes.length" class="py-4 text-center text-lg">No Episodes</p>
 | 
				
			||||||
    <template v-for="episode in episodes">
 | 
					    <template v-for="episode in episodesSorted">
 | 
				
			||||||
      <tables-podcast-episode-table-row :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" class="item" @edit="editEpisode" />
 | 
					      <tables-podcast-episode-table-row :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" class="item" @remove="removeEpisode" @edit="editEpisode" />
 | 
				
			||||||
    </template>
 | 
					    </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <modals-podcast-remove-episode v-model="showPodcastRemoveModal" :library-item="libraryItem" :episode="selectedEpisode" />
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -25,8 +24,16 @@ export default {
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
 | 
					      episodesCopy: [],
 | 
				
			||||||
      sortKey: 'publishedAt',
 | 
					      sortKey: 'publishedAt',
 | 
				
			||||||
      sortDesc: true
 | 
					      sortDesc: true,
 | 
				
			||||||
 | 
					      selectedEpisode: null,
 | 
				
			||||||
 | 
					      showPodcastRemoveModal: false
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  watch: {
 | 
				
			||||||
 | 
					    libraryItem() {
 | 
				
			||||||
 | 
					      this.init()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
@ -41,16 +48,33 @@ export default {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    episodes() {
 | 
					    episodes() {
 | 
				
			||||||
      return this.media.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: {
 | 
					  methods: {
 | 
				
			||||||
 | 
					    removeEpisode(episode) {
 | 
				
			||||||
 | 
					      this.selectedEpisode = episode
 | 
				
			||||||
 | 
					      this.showPodcastRemoveModal = true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    editEpisode(episode) {
 | 
					    editEpisode(episode) {
 | 
				
			||||||
      this.$store.commit('setSelectedLibraryItem', this.libraryItem)
 | 
					      this.$store.commit('setSelectedLibraryItem', this.libraryItem)
 | 
				
			||||||
      this.$store.commit('globals/setSelectedEpisode', episode)
 | 
					      this.$store.commit('globals/setSelectedEpisode', episode)
 | 
				
			||||||
      this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
 | 
					      this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    init() {
 | 
				
			||||||
 | 
					      this.episodesCopy = this.episodes.map((ep) => ({ ...ep }))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  mounted() {}
 | 
					  mounted() {
 | 
				
			||||||
 | 
					    this.init()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -224,24 +224,6 @@ class LibraryItemController {
 | 
				
			|||||||
    res.json(libraryItem.toJSON())
 | 
					    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
 | 
					  // POST api/items/:id/match
 | 
				
			||||||
  async match(req, res) {
 | 
					  async match(req, res) {
 | 
				
			||||||
    var libraryItem = req.libraryItem
 | 
					    var libraryItem = req.libraryItem
 | 
				
			||||||
 | 
				
			|||||||
@ -190,6 +190,35 @@ class PodcastController {
 | 
				
			|||||||
    res.json(libraryItem.toJSONExpanded())
 | 
					    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) {
 | 
					  middleware(req, res, next) {
 | 
				
			||||||
    var item = this.db.libraryItems.find(li => li.id === req.params.id)
 | 
					    var item = this.db.libraryItems.find(li => li.id === req.params.id)
 | 
				
			||||||
    if (!item || !item.media) return res.sendStatus(404)
 | 
					    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', 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.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.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/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.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))
 | 
					    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.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.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.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
 | 
					    // Misc Routes
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user