This commit is contained in:
Rhys Tyers 2023-01-09 21:48:56 +00:00
parent ebedaeb3b0
commit 51b3f0d465
8 changed files with 157 additions and 200 deletions

View file

@ -1,174 +0,0 @@
<template>
<modals-modal v-model="show" name="podcast-episodes-modal" :width="1200" :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" id="podcast-wrapper" class="p-4 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
<div ref="episodeContainer" id="episodes-scroll" class="w-full overflow-x-hidden overflow-y-auto">
<div
v-for="(episode, index) in episodes"
:key="index"
class="relative"
:class="itemEpisodeMap[episode.enclosure.url] ? 'bg-primary bg-opacity-40' : selectedEpisodes[String(index)] ? 'cursor-pointer bg-success bg-opacity-10' : index % 2 == 0 ? 'cursor-pointer bg-primary bg-opacity-25 hover:bg-opacity-40' : 'cursor-pointer bg-primary bg-opacity-5 hover:bg-opacity-25'"
@click="toggleSelectEpisode(index, episode)"
>
<div class="absolute top-0 left-0 h-full flex items-center p-2">
<span v-if="itemEpisodeMap[episode.enclosure.url]" class="material-icons text-success text-xl">download_done</span>
<ui-checkbox v-else v-model="selectedEpisodes[String(index)]" small checkbox-bg="primary" border-color="gray-600" />
</div>
<div class="px-8 py-2">
<p v-if="episode.episode" class="font-semibold text-gray-200">#{{ episode.episode }}</p>
<p class="break-words mb-1">{{ episode.title }}</p>
<p v-if="episode.subtitle" class="break-words mb-1 text-sm text-gray-300 episode-subtitle">{{ episode.subtitle }}</p>
<p class="text-xs text-gray-300">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
</div>
</div>
</div>
<div class="flex justify-end pt-4">
<ui-checkbox v-if="!allDownloaded" v-model="selectAll" @input="toggleSelectAll" label="Select all episodes" small checkbox-bg="primary" border-color="gray-600" class="mx-8" />
<ui-btn v-if="!allDownloaded" :disabled="!episodesSelected.length" @click="submit">{{ buttonText }}</ui-btn>
<p v-else class="text-success text-base px-2 py-4">All episodes are downloaded</p>
</div>
</div>
</modals-modal>
</template>
<script>
export default {
props: {
value: Boolean,
libraryItem: {
type: Object,
default: () => {}
},
episodes: {
type: Array,
default: () => []
}
},
data() {
return {
processing: false,
selectedEpisodes: {},
selectAll: false
}
},
watch: {
show: {
immediate: true,
handler(newVal) {
if (newVal) this.init()
}
}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
title() {
if (!this.libraryItem) return ''
return this.libraryItem.media.metadata.title || 'Unknown'
},
allDownloaded() {
return !this.episodes.some((episode) => !this.itemEpisodeMap[episode.enclosure.url])
},
episodesSelected() {
return Object.keys(this.selectedEpisodes).filter((key) => !!this.selectedEpisodes[key])
},
buttonText() {
if (!this.episodesSelected.length) return 'No Episodes Selected'
return `Download ${this.episodesSelected.length} Episode${this.episodesSelected.length > 1 ? 's' : ''}`
},
itemEpisodes() {
if (!this.libraryItem) return []
return this.libraryItem.media.episodes || []
},
itemEpisodeMap() {
var map = {}
this.itemEpisodes.forEach((item) => {
if (item.enclosure) map[item.enclosure.url] = true
})
return map
}
},
methods: {
toggleSelectAll(val) {
for (let i = 0; i < this.episodes.length; i++) {
const episode = this.episodes[i]
if (this.itemEpisodeMap[episode.enclosure.url]) this.selectedEpisodes[String(i)] = false
else this.$set(this.selectedEpisodes, String(i), val)
}
},
checkSetIsSelectedAll() {
for (let i = 0; i < this.episodes.length; i++) {
const episode = this.episodes[i]
if (!this.itemEpisodeMap[episode.enclosure.url] && !this.selectedEpisodes[String(i)]) {
this.selectAll = false
return
}
}
this.selectAll = true
},
toggleSelectEpisode(index, episode) {
if (this.itemEpisodeMap[episode.enclosure.url]) return
this.$set(this.selectedEpisodes, String(index), !this.selectedEpisodes[String(index)])
this.checkSetIsSelectedAll()
},
submit() {
var episodesToDownload = []
if (this.episodesSelected.length) {
episodesToDownload = this.episodesSelected.map((episodeIndex) => this.episodes[Number(episodeIndex)])
}
var payloadSize = JSON.stringify(episodesToDownload).length
var sizeInMb = payloadSize / 1024 / 1024
var sizeInMbPretty = sizeInMb.toFixed(2) + 'MB'
console.log('Request size', sizeInMb)
if (sizeInMb > 4.99) {
return this.$toast.error(`Request is too large (${sizeInMbPretty}) should be < 5Mb`)
}
this.processing = true
this.$axios
.$post(`/api/podcasts/${this.libraryItem.id}/download-episodes`, episodesToDownload)
.then(() => {
this.processing = false
this.$toast.success('Started downloading episodes')
this.show = false
})
.catch((error) => {
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to download episodes'
console.error('Failed to download episodes', error)
this.processing = false
this.$toast.error(errorMsg)
this.selectedEpisodes = {}
this.selectAll = false
})
},
init() {
this.episodes.sort((a, b) => (a.publishedAt < b.publishedAt ? 1 : -1))
this.selectAll = false
this.selectedEpisodes = {}
}
},
mounted() {}
}
</script>
<style scoped>
#podcast-wrapper {
min-height: 400px;
max-height: 80vh;
}
#episodes-scroll {
max-height: calc(80vh - 200px);
}
</style>

View file

@ -15,29 +15,36 @@
</div>
<div class="flex items-center pt-2">
<button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="userIsFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick">
<button v-if="episode.audioTrack" class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="userIsFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick">
<span class="material-icons text-2xl" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
<p class="pl-2 pr-1 text-sm font-semibold">{{ timeRemaining }}</p>
</button>
<button v-if="!episode.id && !isDownloading && !isDownloadQueued && userIsAdminOrUp" class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="userIsFinished ? 'text-white text-opacity-40' : ''" @click.stop="downloadEpisode">
<span class="material-icons text-success">download</span>
<p class="pl-2 pr-1 text-sm font-semibold">Download</p>
</button>
<!-- <button v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" class="h-8 w-8 flex justify-center items-center mx-2" :class="isQueued ? 'text-success' : ''" @click.stop="queueBtnClick">
<span class="material-icons-outlined">{{ isQueued ? 'playlist_add_check' : 'queue' }}</span>
</button> -->
<ui-tooltip v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" :text="isQueued ? $strings.MessageRemoveFromPlayerQueue : $strings.MessageAddToPlayerQueue" :class="isQueued ? 'text-success' : ''" direction="top">
<ui-tooltip v-if="episode.id && libraryItemIdStreaming && !isStreamingFromDifferentLibrary" :text="isQueued ? $strings.MessageRemoveFromPlayerQueue : $strings.MessageAddToPlayerQueue" :class="isQueued ? 'text-success' : ''" direction="top">
<ui-icon-btn :icon="isQueued ? 'playlist_add_check' : 'playlist_play'" borderless @click="queueBtnClick" />
</ui-tooltip>
<ui-tooltip :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
<ui-tooltip v-if="episode.id" :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
</ui-tooltip>
<ui-tooltip :text="$strings.LabelYourPlaylists" direction="top">
<ui-tooltip v-if="episode.id" :text="$strings.LabelYourPlaylists" direction="top">
<ui-icon-btn icon="playlist_add" borderless @click="clickAddToPlaylist" />
</ui-tooltip>
<ui-icon-btn v-if="userCanUpdate" icon="edit" borderless @click="clickEdit" />
<ui-icon-btn v-if="userCanDelete" icon="close" borderless @click="removeClick" />
<ui-icon-btn v-if="episode.id && userCanUpdate" icon="edit" borderless @click="clickEdit" />
<ui-icon-btn v-if="episode.id && userCanDelete" icon="close" borderless @click="removeClick" />
<p v-if="isDownloading">Downloading...</p>
<p v-if="isDownloadQueued">Queued...</p>
</div>
</div>
<div v-if="isHovering || isSelected || selectionMode" class="hidden md:block w-12 min-w-12" />
@ -63,14 +70,16 @@ export default {
type: Object,
default: () => {}
},
selectionMode: Boolean
selectionMode: Boolean,
isDownloading: Boolean,
isDownloadQueued: Boolean
},
data() {
return {
isProcessingReadUpdate: false,
processingRemove: false,
isHovering: false,
isSelected: false
isSelected: false,
}
},
computed: {
@ -80,6 +89,9 @@ export default {
userCanDelete() {
return this.$store.getters['user/getUserCanDelete']
},
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
audioFile() {
return this.episode.audioFile
},
@ -202,7 +214,33 @@ export default {
// Add to queue
this.$emit('addToQueue', this.episode)
}
},
downloadEpisode() {
var episodesToDownload = [this.episode]
var payloadSize = JSON.stringify(episodesToDownload).length
var sizeInMb = payloadSize / 1024 / 1024
var sizeInMbPretty = sizeInMb.toFixed(2) + 'MB'
console.log('Request size', sizeInMb)
if (sizeInMb > 4.99) {
return this.$toast.error(`Request is too large (${sizeInMbPretty}) should be < 5Mb`)
}
this.processing = true
this.$axios
.$post(`/api/podcasts/${this.libraryItemId}/download-episodes`, episodesToDownload)
.then(() => {
this.processing = false
this.$toast.success('Started downloading episodes')
this.show = false
})
.catch((error) => {
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to download episodes'
console.error('Failed to download episodes', error)
this.processing = false
this.$toast.error(errorMsg)
this.selectedEpisodes = {}
this.selectAll = false
})
},
}
}
</script>

View file

@ -1,13 +1,16 @@
<template>
<div class="w-full py-6">
<p class="text-lg mb-2 font-semibold md:hidden">{{ $strings.HeaderEpisodes }}</p>
<p v-if="media.podcastFeedDownloadedAt" class="text-sm text-gray-300">Feed last updated {{ $formatDate(media.podcastFeedDownloadedAt, 'MMM do, yyyy HH:mm') }}</p>
<div class="flex items-center mb-4">
<p class="text-lg mb-0 font-semibold hidden md:block">{{ $strings.HeaderEpisodes }}</p>
<ui-icon-btn icon="refresh" class="mx-0.5" outlined @click.stop="refreshPodcastFeed" />
<div class="flex-grow hidden md:block" />
<template v-if="isSelectionMode">
<ui-tooltip :text="selectedIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="bottom">
<ui-read-icon-btn :disabled="processing" :is-read="selectedIsFinished" @click="toggleBatchFinished" class="mx-1.5" />
</ui-tooltip>
<ui-btn color="success" :disabled="processing" small class="h-9 mr-1.5" @click="downloadSelectedEpisodes">Download {{ selectedEpisodes.length }} episode(s)</ui-btn>
<ui-btn color="error" :disabled="processing" small class="h-9" @click="removeSelectedEpisodes">{{ $getString('MessageRemoveEpisodes', [selectedEpisodes.length]) }}</ui-btn>
<ui-btn :disabled="processing" small class="ml-2 h-9" @click="clearSelected">{{ $strings.ButtonCancel }}</ui-btn>
</template>
@ -19,10 +22,9 @@
</template>
</div>
<p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p>
<template v-for="episode in episodesSorted">
<tables-podcast-episode-table-row ref="episodeRow" :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" :selection-mode="isSelectionMode" class="item" @play="playEpisode" @remove="removeEpisode" @edit="editEpisode" @view="viewEpisode" @selected="episodeSelected" @addToQueue="addEpisodeToQueue" @addToPlaylist="addToPlaylist" />
<template v-for="episode in this.allEpisodes">
<tables-podcast-episode-table-row ref="episodeRow" :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" :selection-mode="isSelectionMode" :is-downloading="isDownloading(episode)" :is-download-queued="isDownloadQueued(episode)" class="item" @play="playEpisode" @remove="removeEpisode" @edit="editEpisode" @view="viewEpisode" @selected="episodeSelected" @addToQueue="addEpisodeToQueue" @addToPlaylist="addToPlaylist" />
</template>
<modals-podcast-remove-episode v-model="showPodcastRemoveModal" @input="removeEpisodeModalToggled" :library-item="libraryItem" :episodes="episodesToRemove" @clearSelected="clearSelected" />
</div>
</template>
@ -33,6 +35,14 @@ export default {
libraryItem: {
type: Object,
default: () => {}
},
episodesDownloading: {
type: Array,
default: () => []
},
episodesQueued: {
type: Array,
default: () => []
}
},
data() {
@ -54,6 +64,11 @@ export default {
handler() {
this.init()
}
},
episodesDownloading: {
handler() {
console.log(this.episodesDownloading)
}
}
},
computed: {
@ -137,6 +152,17 @@ export default {
return String(a[this.sortKey]).localeCompare(String(b[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' })
})
},
allEpisodes() {
const added = new Set()
const episodes = []
for (const episode of [...this.episodes, ...this.media.podcastFeed.episodes]) {
const key = `${episode.pubDate}-${episode.title}`
if (added.has(key)) continue
episodes.push(episode)
added.add(key)
}
return episodes
},
selectedIsFinished() {
// Find an item that is not finished, if none then all items finished
return !this.selectedEpisodes.find((episode) => {
@ -290,6 +316,49 @@ export default {
this.$store.commit('globals/setSelectedEpisode', episode)
this.$store.commit('globals/setShowViewPodcastEpisodeModal', true)
},
refreshPodcastFeed() {
this.$axios
.get(`/api/podcasts/${this.libraryItem.id}/refresh-episodes-list`)
.then(() => {
this.$toast.success('Fetched episodes!')
})
.catch((error) => {
this.$toast.error('Fetching episodes failed')
console.error('Fetching episodes failed', error)
})
},
downloadSelectedEpisodes() {
var episodesToDownload = this.selectedEpisodes
var payloadSize = JSON.stringify(episodesToDownload).length
var sizeInMb = payloadSize / 1024 / 1024
var sizeInMbPretty = sizeInMb.toFixed(2) + 'MB'
console.log('Request size', sizeInMb)
if (sizeInMb > 4.99) {
return this.$toast.error(`Request is too large (${sizeInMbPretty}) should be < 5Mb`)
}
this.processing = true
this.$axios
.$post(`/api/podcasts/${this.libraryItem.id}/download-episodes`, episodesToDownload)
.then(() => {
this.processing = false
this.$toast.success('Started downloading episodes')
this.show = false
this.clearSelected()
})
.catch((error) => {
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to download episodes'
console.error('Failed to download episodes', error)
this.processing = false
this.$toast.error(errorMsg)
this.clearSelected()
})
},
isDownloading(episode) {
return this.episodesDownloading.map(ep => ep.url).includes(episode?.url || episode?.enclosure?.url)
},
isDownloadQueued(episode) {
return this.episodesQueued.map(ep => ep.url).includes(episode?.url || episode?.enclosure?.url)
},
init() {
this.episodesCopy = this.episodes.map((ep) => ({ ...ep }))
}

View file

@ -198,11 +198,6 @@
<ui-icon-btn icon="playlist_add" class="mx-0.5" outlined @click="playlistsClick" />
</ui-tooltip>
<!-- Only admin or root user can download new episodes -->
<ui-tooltip v-if="isPodcast && userIsAdminOrUp" :text="$strings.LabelFindEpisodes" direction="top">
<ui-icon-btn icon="search" class="mx-0.5" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" />
</ui-tooltip>
<ui-tooltip v-if="bookmarks.length" :text="$strings.LabelYourBookmarks" direction="top">
<ui-icon-btn :icon="bookmarks.length ? 'bookmarks' : 'bookmark_border'" class="mx-0.5" @click="clickBookmarksBtn" />
</ui-tooltip>
@ -225,7 +220,7 @@
<widgets-audiobook-data v-if="tracks.length" :library-item-id="libraryItemId" :is-file="isFile" :media="media" />
<tables-podcast-episodes-table v-if="isPodcast" :library-item="libraryItem" />
<tables-podcast-episodes-table v-if="isPodcast" :library-item="libraryItem" :episodes-downloading="episodesDownloading" :episodes-queued="episodeDownloadsQueued"/>
<tables-chapters-table v-if="chapters.length" :library-item="libraryItem" class="mt-6" />
@ -234,7 +229,6 @@
</div>
</div>
<modals-podcast-episode-feed v-model="showPodcastEpisodeFeed" :library-item="libraryItem" :episodes="podcastFeedEpisodes" />
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :library-item-id="libraryItemId" hide-create @select="selectBookmark" />
</div>
</template>

View file

@ -45,6 +45,9 @@ class PodcastController {
var libraryItemFolderStats = await getFileTimestampsWithIno(podcastPath)
var podcastFeed = await getPodcastFeed(payload.media.metadata.feedUrl)
var podcastFeedDownloadedAt = Date.now()
var relPath = payload.path.replace(folder.fullPath, '')
if (relPath.startsWith('/')) relPath = relPath.slice(1)
@ -57,7 +60,7 @@ class PodcastController {
mtimeMs: libraryItemFolderStats.mtimeMs || 0,
ctimeMs: libraryItemFolderStats.ctimeMs || 0,
birthtimeMs: libraryItemFolderStats.birthtimeMs || 0,
media: payload.media
media: { ...payload.media, podcastFeed, podcastFeedDownloadedAt }
}
var libraryItem = new LibraryItem()
@ -106,6 +109,12 @@ class PodcastController {
res.json({ podcast })
}
async refreshPodcastFeed(req, res) {
var libraryItem = req.libraryItem;
await this.podcastManager.refreshPodcastFeed(libraryItem);
res.json({ episodes: libraryItem.media.episodes });
}
async getOPMLFeeds(req, res) {
if (!req.body.opmlText) {
return res.sendStatus(400)

View file

@ -49,6 +49,13 @@ class PodcastManager {
}
}
async refreshPodcastFeed(libraryItem) {
var feed = await getPodcastFeed(libraryItem.media.metadata.feedUrl);
libraryItem.updatedAt = Date.now();
libraryItem.media.update({podcastFeed: feed, podcastFeedDownloadedAt: Date.now()})
await this.db.updateLibraryItem(libraryItem);
}
async downloadPodcastEpisodes(libraryItem, episodesToDownload, isAutoDownload) {
var index = libraryItem.media.episodes.length + 1
episodesToDownload.forEach((ep) => {

View file

@ -26,6 +26,9 @@ class Podcast {
this.lastCoverSearch = null
this.lastCoverSearchQuery = null
this.podcastFeed = null
this.podcastFeedDownloadedAt = null
if (podcast) {
this.construct(podcast)
}
@ -46,6 +49,8 @@ class Podcast {
this.lastEpisodeCheck = podcast.lastEpisodeCheck || 0
this.maxEpisodesToKeep = podcast.maxEpisodesToKeep || 0
this.maxNewEpisodesToDownload = podcast.maxNewEpisodesToDownload || 3
this.podcastFeed = podcast.podcastFeed
this.podcastFeedDownloadedAt = podcast.podcastFeedDownloadedAt
}
toJSON() {
@ -54,12 +59,14 @@ class Podcast {
metadata: this.metadata.toJSON(),
coverPath: this.coverPath,
tags: [...this.tags],
episodes: this.episodes.map(e => e.toJSON()),
episodes: this.episodes.map((e) => e.toJSON()),
autoDownloadEpisodes: this.autoDownloadEpisodes,
autoDownloadSchedule: this.autoDownloadSchedule,
lastEpisodeCheck: this.lastEpisodeCheck,
maxEpisodesToKeep: this.maxEpisodesToKeep,
maxNewEpisodesToDownload: this.maxNewEpisodesToDownload
maxNewEpisodesToDownload: this.maxNewEpisodesToDownload,
podcastFeed: this.podcastFeed,
podcastFeedDownloadedAt: this.podcastFeedDownloadedAt,
}
}
@ -74,7 +81,9 @@ class Podcast {
lastEpisodeCheck: this.lastEpisodeCheck,
maxEpisodesToKeep: this.maxEpisodesToKeep,
maxNewEpisodesToDownload: this.maxNewEpisodesToDownload,
size: this.size
size: this.size,
podcastFeed: this.podcastFeed,
podcastFeedDownloadedAt: this.podcastFeedDownloadedAt,
}
}
@ -84,13 +93,15 @@ class Podcast {
metadata: this.metadata.toJSONExpanded(),
coverPath: this.coverPath,
tags: [...this.tags],
episodes: this.episodes.map(e => e.toJSONExpanded()),
episodes: this.episodes.map((e) => e.toJSONExpanded()),
autoDownloadEpisodes: this.autoDownloadEpisodes,
autoDownloadSchedule: this.autoDownloadSchedule,
lastEpisodeCheck: this.lastEpisodeCheck,
maxEpisodesToKeep: this.maxEpisodesToKeep,
maxNewEpisodesToDownload: this.maxNewEpisodesToDownload,
size: this.size
size: this.size,
podcastFeed: this.podcastFeed,
podcastFeedDownloadedAt: this.podcastFeedDownloadedAt,
}
}
@ -185,6 +196,9 @@ class Podcast {
this.autoDownloadEpisodes = !!mediaData.autoDownloadEpisodes
this.autoDownloadSchedule = mediaData.autoDownloadSchedule || global.ServerSettings.podcastEpisodeSchedule
this.lastEpisodeCheck = Date.now() // Makes sure new episodes are after this
this.podcastFeed = mediaData.podcastFeed || null
this.podcastFeedDownloadedAt = mediaData.podcastFeedDownloadedAt || null
}
async syncMetadataFiles(textMetadataFiles, opfMetadataOverrideDetails) {

View file

@ -234,7 +234,7 @@ class ApiRouter {
this.router.post('/podcasts/:id/match-episodes', PodcastController.middleware.bind(this), PodcastController.quickMatchEpisodes.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))
this.router.get('/podcasts/:id/refresh-episodes-list', PodcastController.middleware.bind(this), PodcastController.refreshPodcastFeed.bind(this))
//
// Notification Routes (Admin and up)
//