mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-16 14:21:41 +00:00
WIP
This commit is contained in:
parent
ebedaeb3b0
commit
51b3f0d465
8 changed files with 157 additions and 200 deletions
|
|
@ -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>
|
|
||||||
|
|
@ -15,29 +15,36 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center pt-2">
|
<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>
|
<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>
|
<p class="pl-2 pr-1 text-sm font-semibold">{{ timeRemaining }}</p>
|
||||||
</button>
|
</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">
|
<!-- <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>
|
<span class="material-icons-outlined">{{ isQueued ? 'playlist_add_check' : 'queue' }}</span>
|
||||||
</button> -->
|
</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-icon-btn :icon="isQueued ? 'playlist_add_check' : 'playlist_play'" borderless @click="queueBtnClick" />
|
||||||
</ui-tooltip>
|
</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-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
|
||||||
</ui-tooltip>
|
</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-icon-btn icon="playlist_add" borderless @click="clickAddToPlaylist" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-icon-btn v-if="userCanUpdate" icon="edit" borderless @click="clickEdit" />
|
<ui-icon-btn v-if="episode.id && userCanUpdate" icon="edit" borderless @click="clickEdit" />
|
||||||
<ui-icon-btn v-if="userCanDelete" icon="close" borderless @click="removeClick" />
|
<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>
|
</div>
|
||||||
<div v-if="isHovering || isSelected || selectionMode" class="hidden md:block w-12 min-w-12" />
|
<div v-if="isHovering || isSelected || selectionMode" class="hidden md:block w-12 min-w-12" />
|
||||||
|
|
@ -63,14 +70,16 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
selectionMode: Boolean
|
selectionMode: Boolean,
|
||||||
|
isDownloading: Boolean,
|
||||||
|
isDownloadQueued: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isProcessingReadUpdate: false,
|
isProcessingReadUpdate: false,
|
||||||
processingRemove: false,
|
processingRemove: false,
|
||||||
isHovering: false,
|
isHovering: false,
|
||||||
isSelected: false
|
isSelected: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -80,6 +89,9 @@ export default {
|
||||||
userCanDelete() {
|
userCanDelete() {
|
||||||
return this.$store.getters['user/getUserCanDelete']
|
return this.$store.getters['user/getUserCanDelete']
|
||||||
},
|
},
|
||||||
|
userIsAdminOrUp() {
|
||||||
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
|
},
|
||||||
audioFile() {
|
audioFile() {
|
||||||
return this.episode.audioFile
|
return this.episode.audioFile
|
||||||
},
|
},
|
||||||
|
|
@ -202,7 +214,33 @@ export default {
|
||||||
// Add to queue
|
// Add to queue
|
||||||
this.$emit('addToQueue', this.episode)
|
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>
|
</script>
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full py-6">
|
<div class="w-full py-6">
|
||||||
<p class="text-lg mb-2 font-semibold md:hidden">{{ $strings.HeaderEpisodes }}</p>
|
<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">
|
<div class="flex items-center mb-4">
|
||||||
<p class="text-lg mb-0 font-semibold hidden md:block">{{ $strings.HeaderEpisodes }}</p>
|
<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" />
|
<div class="flex-grow hidden md:block" />
|
||||||
<template v-if="isSelectionMode">
|
<template v-if="isSelectionMode">
|
||||||
<ui-tooltip :text="selectedIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="bottom">
|
<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-read-icon-btn :disabled="processing" :is-read="selectedIsFinished" @click="toggleBatchFinished" class="mx-1.5" />
|
||||||
</ui-tooltip>
|
</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 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>
|
<ui-btn :disabled="processing" small class="ml-2 h-9" @click="clearSelected">{{ $strings.ButtonCancel }}</ui-btn>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -19,10 +22,9 @@
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p>
|
<p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p>
|
||||||
<template v-for="episode in episodesSorted">
|
<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" class="item" @play="playEpisode" @remove="removeEpisode" @edit="editEpisode" @view="viewEpisode" @selected="episodeSelected" @addToQueue="addEpisodeToQueue" @addToPlaylist="addToPlaylist" />
|
<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>
|
</template>
|
||||||
|
|
||||||
<modals-podcast-remove-episode v-model="showPodcastRemoveModal" @input="removeEpisodeModalToggled" :library-item="libraryItem" :episodes="episodesToRemove" @clearSelected="clearSelected" />
|
<modals-podcast-remove-episode v-model="showPodcastRemoveModal" @input="removeEpisodeModalToggled" :library-item="libraryItem" :episodes="episodesToRemove" @clearSelected="clearSelected" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -33,6 +35,14 @@ export default {
|
||||||
libraryItem: {
|
libraryItem: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
|
},
|
||||||
|
episodesDownloading: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
episodesQueued: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -54,6 +64,11 @@ export default {
|
||||||
handler() {
|
handler() {
|
||||||
this.init()
|
this.init()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
episodesDownloading: {
|
||||||
|
handler() {
|
||||||
|
console.log(this.episodesDownloading)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -137,6 +152,17 @@ export default {
|
||||||
return String(a[this.sortKey]).localeCompare(String(b[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' })
|
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() {
|
selectedIsFinished() {
|
||||||
// Find an item that is not finished, if none then all items finished
|
// Find an item that is not finished, if none then all items finished
|
||||||
return !this.selectedEpisodes.find((episode) => {
|
return !this.selectedEpisodes.find((episode) => {
|
||||||
|
|
@ -290,6 +316,49 @@ export default {
|
||||||
this.$store.commit('globals/setSelectedEpisode', episode)
|
this.$store.commit('globals/setSelectedEpisode', episode)
|
||||||
this.$store.commit('globals/setShowViewPodcastEpisodeModal', true)
|
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() {
|
init() {
|
||||||
this.episodesCopy = this.episodes.map((ep) => ({ ...ep }))
|
this.episodesCopy = this.episodes.map((ep) => ({ ...ep }))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -198,11 +198,6 @@
|
||||||
<ui-icon-btn icon="playlist_add" class="mx-0.5" outlined @click="playlistsClick" />
|
<ui-icon-btn icon="playlist_add" class="mx-0.5" outlined @click="playlistsClick" />
|
||||||
</ui-tooltip>
|
</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-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-icon-btn :icon="bookmarks.length ? 'bookmarks' : 'bookmark_border'" class="mx-0.5" @click="clickBookmarksBtn" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
@ -225,7 +220,7 @@
|
||||||
|
|
||||||
<widgets-audiobook-data v-if="tracks.length" :library-item-id="libraryItemId" :is-file="isFile" :media="media" />
|
<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" />
|
<tables-chapters-table v-if="chapters.length" :library-item="libraryItem" class="mt-6" />
|
||||||
|
|
||||||
|
|
@ -234,7 +229,6 @@
|
||||||
</div>
|
</div>
|
||||||
</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" />
|
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :library-item-id="libraryItemId" hide-create @select="selectBookmark" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,9 @@ class PodcastController {
|
||||||
|
|
||||||
var libraryItemFolderStats = await getFileTimestampsWithIno(podcastPath)
|
var libraryItemFolderStats = await getFileTimestampsWithIno(podcastPath)
|
||||||
|
|
||||||
|
var podcastFeed = await getPodcastFeed(payload.media.metadata.feedUrl)
|
||||||
|
var podcastFeedDownloadedAt = Date.now()
|
||||||
|
|
||||||
var relPath = payload.path.replace(folder.fullPath, '')
|
var relPath = payload.path.replace(folder.fullPath, '')
|
||||||
if (relPath.startsWith('/')) relPath = relPath.slice(1)
|
if (relPath.startsWith('/')) relPath = relPath.slice(1)
|
||||||
|
|
||||||
|
|
@ -57,7 +60,7 @@ class PodcastController {
|
||||||
mtimeMs: libraryItemFolderStats.mtimeMs || 0,
|
mtimeMs: libraryItemFolderStats.mtimeMs || 0,
|
||||||
ctimeMs: libraryItemFolderStats.ctimeMs || 0,
|
ctimeMs: libraryItemFolderStats.ctimeMs || 0,
|
||||||
birthtimeMs: libraryItemFolderStats.birthtimeMs || 0,
|
birthtimeMs: libraryItemFolderStats.birthtimeMs || 0,
|
||||||
media: payload.media
|
media: { ...payload.media, podcastFeed, podcastFeedDownloadedAt }
|
||||||
}
|
}
|
||||||
|
|
||||||
var libraryItem = new LibraryItem()
|
var libraryItem = new LibraryItem()
|
||||||
|
|
@ -106,6 +109,12 @@ class PodcastController {
|
||||||
res.json({ podcast })
|
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) {
|
async getOPMLFeeds(req, res) {
|
||||||
if (!req.body.opmlText) {
|
if (!req.body.opmlText) {
|
||||||
return res.sendStatus(400)
|
return res.sendStatus(400)
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
async downloadPodcastEpisodes(libraryItem, episodesToDownload, isAutoDownload) {
|
||||||
var index = libraryItem.media.episodes.length + 1
|
var index = libraryItem.media.episodes.length + 1
|
||||||
episodesToDownload.forEach((ep) => {
|
episodesToDownload.forEach((ep) => {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,9 @@ class Podcast {
|
||||||
this.lastCoverSearch = null
|
this.lastCoverSearch = null
|
||||||
this.lastCoverSearchQuery = null
|
this.lastCoverSearchQuery = null
|
||||||
|
|
||||||
|
this.podcastFeed = null
|
||||||
|
this.podcastFeedDownloadedAt = null
|
||||||
|
|
||||||
if (podcast) {
|
if (podcast) {
|
||||||
this.construct(podcast)
|
this.construct(podcast)
|
||||||
}
|
}
|
||||||
|
|
@ -46,6 +49,8 @@ class Podcast {
|
||||||
this.lastEpisodeCheck = podcast.lastEpisodeCheck || 0
|
this.lastEpisodeCheck = podcast.lastEpisodeCheck || 0
|
||||||
this.maxEpisodesToKeep = podcast.maxEpisodesToKeep || 0
|
this.maxEpisodesToKeep = podcast.maxEpisodesToKeep || 0
|
||||||
this.maxNewEpisodesToDownload = podcast.maxNewEpisodesToDownload || 3
|
this.maxNewEpisodesToDownload = podcast.maxNewEpisodesToDownload || 3
|
||||||
|
this.podcastFeed = podcast.podcastFeed
|
||||||
|
this.podcastFeedDownloadedAt = podcast.podcastFeedDownloadedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
|
|
@ -54,12 +59,14 @@ class Podcast {
|
||||||
metadata: this.metadata.toJSON(),
|
metadata: this.metadata.toJSON(),
|
||||||
coverPath: this.coverPath,
|
coverPath: this.coverPath,
|
||||||
tags: [...this.tags],
|
tags: [...this.tags],
|
||||||
episodes: this.episodes.map(e => e.toJSON()),
|
episodes: this.episodes.map((e) => e.toJSON()),
|
||||||
autoDownloadEpisodes: this.autoDownloadEpisodes,
|
autoDownloadEpisodes: this.autoDownloadEpisodes,
|
||||||
autoDownloadSchedule: this.autoDownloadSchedule,
|
autoDownloadSchedule: this.autoDownloadSchedule,
|
||||||
lastEpisodeCheck: this.lastEpisodeCheck,
|
lastEpisodeCheck: this.lastEpisodeCheck,
|
||||||
maxEpisodesToKeep: this.maxEpisodesToKeep,
|
maxEpisodesToKeep: this.maxEpisodesToKeep,
|
||||||
maxNewEpisodesToDownload: this.maxNewEpisodesToDownload
|
maxNewEpisodesToDownload: this.maxNewEpisodesToDownload,
|
||||||
|
podcastFeed: this.podcastFeed,
|
||||||
|
podcastFeedDownloadedAt: this.podcastFeedDownloadedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,7 +81,9 @@ class Podcast {
|
||||||
lastEpisodeCheck: this.lastEpisodeCheck,
|
lastEpisodeCheck: this.lastEpisodeCheck,
|
||||||
maxEpisodesToKeep: this.maxEpisodesToKeep,
|
maxEpisodesToKeep: this.maxEpisodesToKeep,
|
||||||
maxNewEpisodesToDownload: this.maxNewEpisodesToDownload,
|
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(),
|
metadata: this.metadata.toJSONExpanded(),
|
||||||
coverPath: this.coverPath,
|
coverPath: this.coverPath,
|
||||||
tags: [...this.tags],
|
tags: [...this.tags],
|
||||||
episodes: this.episodes.map(e => e.toJSONExpanded()),
|
episodes: this.episodes.map((e) => e.toJSONExpanded()),
|
||||||
autoDownloadEpisodes: this.autoDownloadEpisodes,
|
autoDownloadEpisodes: this.autoDownloadEpisodes,
|
||||||
autoDownloadSchedule: this.autoDownloadSchedule,
|
autoDownloadSchedule: this.autoDownloadSchedule,
|
||||||
lastEpisodeCheck: this.lastEpisodeCheck,
|
lastEpisodeCheck: this.lastEpisodeCheck,
|
||||||
maxEpisodesToKeep: this.maxEpisodesToKeep,
|
maxEpisodesToKeep: this.maxEpisodesToKeep,
|
||||||
maxNewEpisodesToDownload: this.maxNewEpisodesToDownload,
|
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.autoDownloadEpisodes = !!mediaData.autoDownloadEpisodes
|
||||||
this.autoDownloadSchedule = mediaData.autoDownloadSchedule || global.ServerSettings.podcastEpisodeSchedule
|
this.autoDownloadSchedule = mediaData.autoDownloadSchedule || global.ServerSettings.podcastEpisodeSchedule
|
||||||
this.lastEpisodeCheck = Date.now() // Makes sure new episodes are after this
|
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) {
|
async syncMetadataFiles(textMetadataFiles, opfMetadataOverrideDetails) {
|
||||||
|
|
|
||||||
|
|
@ -234,7 +234,7 @@ class ApiRouter {
|
||||||
this.router.post('/podcasts/:id/match-episodes', PodcastController.middleware.bind(this), PodcastController.quickMatchEpisodes.bind(this))
|
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.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.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)
|
// Notification Routes (Admin and up)
|
||||||
//
|
//
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue