mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-08 00:08:14 +01:00
Podcasts add get episode feed and download, add edit podcast episode modal
This commit is contained in:
parent
08e1782253
commit
3f8e685d64
@ -8,7 +8,7 @@
|
||||
</div>
|
||||
|
||||
<div v-if="loaded && !shelves.length && isRootUser && !search" class="w-full flex flex-col items-center justify-center py-12">
|
||||
<p class="text-center text-2xl font-book mb-4 py-4">Library is empty!</p>
|
||||
<p class="text-center text-2xl font-book mb-4 py-4">{{ libraryName }} Library is empty!</p>
|
||||
<div class="flex">
|
||||
<ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn>
|
||||
<ui-btn color="success" class="w-52" @click="scan">Scan Library</ui-btn>
|
||||
@ -53,6 +53,9 @@ export default {
|
||||
currentLibraryId() {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
},
|
||||
libraryName() {
|
||||
return this.$store.getters['libraries/getCurrentLibraryName']
|
||||
},
|
||||
bookCoverWidth() {
|
||||
var coverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
|
||||
if (this.isCoverSquareAspectRatio) return coverSize * 1.6
|
||||
|
@ -7,7 +7,7 @@
|
||||
</template>
|
||||
|
||||
<div v-if="initialized && !totalShelves && !hasFilter && isRootUser && entityName === 'books'" class="w-full flex flex-col items-center justify-center py-12">
|
||||
<p class="text-center text-2xl font-book mb-4 py-4">Library is empty!</p>
|
||||
<p class="text-center text-2xl font-book mb-4 py-4">{{ libraryName }} Library is empty!</p>
|
||||
<div class="flex">
|
||||
<ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn>
|
||||
<ui-btn color="success" class="w-52" @click="scan">Scan Library</ui-btn>
|
||||
@ -143,6 +143,9 @@ export default {
|
||||
currentLibraryId() {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
},
|
||||
libraryName() {
|
||||
return this.$store.getters['libraries/getCurrentLibraryName']
|
||||
},
|
||||
isEntityBook() {
|
||||
return this.entityName === 'series-books' || this.entityName === 'books'
|
||||
},
|
||||
|
@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6">
|
||||
<div class="w-full mb-4">
|
||||
<div class="flex items-center mb-4">
|
||||
<!-- <div class="flex items-center mb-4">
|
||||
<p v-if="autoDownloadEpisodes">Last new episode check {{ $formatDate(lastEpisodeCheck) }}</p>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn :loading="checkingNewEpisodes" @click="checkForNewEpisodes">Check for new episodes</ui-btn>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="w-full p-4 bg-primary">
|
||||
<p>Podcast Episodes</p>
|
||||
|
129
client/components/modals/podcast/EditEpisode.vue
Normal file
129
client/components/modals/podcast/EditEpisode.vue
Normal file
@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<modals-modal v-model="show" name="podcast-episode-edit-modal" :width="800" :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="p-4 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
||||
<div class="flex flex-wrap">
|
||||
<div class="w-1/3 p-1">
|
||||
<ui-text-input-with-label v-model="newEpisode.episode" label="Episode" />
|
||||
</div>
|
||||
<div class="w-1/3 p-1">
|
||||
<ui-text-input-with-label v-model="newEpisode.episodeType" label="Episode Type" />
|
||||
</div>
|
||||
<div class="w-1/3 p-1">
|
||||
<ui-text-input-with-label v-model="newEpisode.pubDate" label="Pub Date" />
|
||||
</div>
|
||||
<div class="w-full p-1">
|
||||
<ui-text-input-with-label v-model="newEpisode.title" label="Title" />
|
||||
</div>
|
||||
<div class="w-full p-1">
|
||||
<ui-textarea-with-label v-model="newEpisode.subtitle" label="Subtitle" :rows="3" />
|
||||
</div>
|
||||
<div class="w-full p-1">
|
||||
<ui-textarea-with-label v-model="newEpisode.description" label="Description" :rows="8" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end pt-4">
|
||||
<ui-btn @click="submit">Submit</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: Boolean,
|
||||
libraryItem: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
episode: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
processing: false,
|
||||
newEpisode: {
|
||||
episode: null,
|
||||
episodeType: null,
|
||||
title: null,
|
||||
subtitle: null,
|
||||
description: null,
|
||||
pubDate: null
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
episode: {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
if (newVal) this.init()
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
show: {
|
||||
get() {
|
||||
return this.value
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('input', val)
|
||||
}
|
||||
},
|
||||
episodeId() {
|
||||
return this.episode ? this.episode.id : null
|
||||
},
|
||||
title() {
|
||||
if (!this.libraryItem) return ''
|
||||
return this.libraryItem.media.metadata.title || 'Unknown'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
this.newEpisode.episode = this.episode.episode || ''
|
||||
this.newEpisode.episodeType = this.episode.episodeType || ''
|
||||
this.newEpisode.title = this.episode.title || ''
|
||||
this.newEpisode.subtitle = this.episode.subtitle || ''
|
||||
this.newEpisode.description = this.episode.description || ''
|
||||
this.newEpisode.pubDate = this.episode.pubDate || ''
|
||||
},
|
||||
getUpdatePayload() {
|
||||
var updatePayload = {}
|
||||
for (const key in this.newEpisode) {
|
||||
if (this.newEpisode[key] != this.episode[key]) {
|
||||
updatePayload[key] = this.newEpisode[key]
|
||||
}
|
||||
}
|
||||
return updatePayload
|
||||
},
|
||||
submit() {
|
||||
const payload = this.getUpdatePayload()
|
||||
if (!Object.keys(payload).length) {
|
||||
return this.$toast.info('No updates were made')
|
||||
}
|
||||
|
||||
this.processing = true
|
||||
this.$axios
|
||||
.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, payload)
|
||||
.then(() => {
|
||||
this.processing = false
|
||||
this.$toast.success('Podcast episode updated')
|
||||
this.show = false
|
||||
})
|
||||
.catch((error) => {
|
||||
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed update episode'
|
||||
console.error('Failed update episode', error)
|
||||
this.processing = false
|
||||
this.$toast.error(errorMsg)
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
129
client/components/modals/podcast/EpisodeFeed.vue
Normal file
129
client/components/modals/podcast/EpisodeFeed.vue
Normal file
@ -0,0 +1,129 @@
|
||||
<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="episode.enclosure && 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)"
|
||||
>
|
||||
<div class="absolute top-0 left-0 h-full flex items-center p-2">
|
||||
<span v-if="episode.enclosure && 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 class="text-gray-400 text-xs mb-0.5">Published {{ $dateDistanceFromNow(episode.publishedAt) }}</p>
|
||||
<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>
|
||||
<!-- <span class="material-icons cursor-pointer text-lg hover:text-success" @click="saveEpisode(episode)">save</span> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end pt-4">
|
||||
<ui-btn :disabled="!episodesSelected.length" @click="submit">{{ buttonText }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: Boolean,
|
||||
libraryItem: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
episodes: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
processing: false,
|
||||
selectedEpisodes: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
show: {
|
||||
get() {
|
||||
return this.value
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('input', val)
|
||||
}
|
||||
},
|
||||
title() {
|
||||
if (!this.libraryItem) return ''
|
||||
return this.libraryItem.media.metadata.title || 'Unknown'
|
||||
},
|
||||
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: {
|
||||
toggleSelectEpisode(index) {
|
||||
this.$set(this.selectedEpisodes, String(index), !this.selectedEpisodes[String(index)])
|
||||
},
|
||||
submit() {
|
||||
var episodesToDownload = []
|
||||
if (this.episodesSelected.length) {
|
||||
episodesToDownload = this.episodesSelected.map((episodeIndex) => this.episodes[Number(episodeIndex)])
|
||||
}
|
||||
|
||||
console.log('Podcast payload', episodesToDownload)
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#podcast-wrapper {
|
||||
min-height: 400px;
|
||||
max-height: 80vh;
|
||||
}
|
||||
#episodes-scroll {
|
||||
max-height: calc(80vh - 200px);
|
||||
}
|
||||
</style>
|
@ -14,7 +14,7 @@
|
||||
<img :src="podcast.imageUrl" class="h-16 w-16 object-contain" />
|
||||
</div>
|
||||
<div class="p-1 w-full">
|
||||
<ui-text-input-with-label v-model="podcast.title" label="Title" />
|
||||
<ui-text-input-with-label v-model="podcast.title" label="Title" @input="titleUpdated" />
|
||||
</div>
|
||||
<div class="p-1 w-full">
|
||||
<ui-text-input-with-label v-model="podcast.author" label="Author" />
|
||||
@ -169,6 +169,9 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
titleUpdated() {
|
||||
this.folderUpdated()
|
||||
},
|
||||
folderUpdated() {
|
||||
if (!this.selectedFolderPath || !this.podcast.title) {
|
||||
this.fullPath = ''
|
||||
@ -219,9 +222,10 @@ export default {
|
||||
this.$router.push(`/item/${libraryItem.id}`)
|
||||
})
|
||||
.catch((error) => {
|
||||
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to create podcast'
|
||||
console.error('Failed to create podcast', error)
|
||||
this.processing = false
|
||||
this.$toast.error('Failed to create podcast')
|
||||
this.$toast.error(errorMsg)
|
||||
})
|
||||
},
|
||||
saveEpisode(episode) {
|
||||
@ -251,13 +255,11 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
console.log('Podcast feed data', this.podcastFeedData)
|
||||
}
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style scoped>
|
||||
#podcast-wrapper {
|
||||
min-height: 400px;
|
||||
max-height: 80vh;
|
||||
|
@ -113,7 +113,9 @@ export default {
|
||||
mouseleave() {
|
||||
this.isHovering = false
|
||||
},
|
||||
clickEdit() {},
|
||||
clickEdit() {
|
||||
this.$emit('edit', this.episode)
|
||||
},
|
||||
playClick() {
|
||||
if (this.streamIsPlaying) {
|
||||
this.$eventBus.$emit('pause-item')
|
||||
|
@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<div class="w-full py-6">
|
||||
<p class="text-lg mb-0 font-semibold">Episodes</p>
|
||||
<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>
|
||||
<draggable v-model="episodesCopy" v-bind="dragOptions" class="list-group" handle=".drag-handle" draggable=".item" tag="div" @start="drag = true" @end="drag = false" @update="draggableUpdate">
|
||||
<transition-group type="transition" :name="!drag ? 'episode' : null">
|
||||
<template v-for="episode in episodesCopy">
|
||||
<tables-podcast-episode-table-row :key="episode.id" :is-dragging="drag" :episode="episode" :library-item-id="libraryItem.id" class="item" :class="drag ? '' : 'episode'" />
|
||||
<tables-podcast-episode-table-row :key="episode.id" :is-dragging="drag" :episode="episode" :library-item-id="libraryItem.id" class="item" :class="drag ? '' : 'episode'" @edit="editEpisode" />
|
||||
</template>
|
||||
</transition-group>
|
||||
</draggable>
|
||||
|
||||
<modals-podcast-edit-episode v-model="showEditEpisodeModal" :library-item="libraryItem" :episode="selectedEpisode" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -35,7 +35,9 @@ export default {
|
||||
group: 'description',
|
||||
ghostClass: 'ghost'
|
||||
},
|
||||
episodesCopy: []
|
||||
episodesCopy: [],
|
||||
selectedEpisode: null,
|
||||
showEditEpisodeModal: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@ -57,6 +59,10 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
editEpisode(episode) {
|
||||
this.selectedEpisode = episode
|
||||
this.showEditEpisodeModal = true
|
||||
},
|
||||
draggableUpdate() {
|
||||
var episodesUpdate = {
|
||||
episodes: this.episodesCopy.map((b) => b.id)
|
||||
|
@ -1,6 +1,11 @@
|
||||
<template>
|
||||
<button class="icon-btn rounded-md flex items-center justify-center h-9 w-9 relative" @mousedown.prevent :disabled="disabled" :class="className" @click="clickBtn">
|
||||
<span :class="outlined ? 'material-icons-outlined' : 'material-icons'" :style="{ fontSize }">{{ icon }}</span>
|
||||
<button class="icon-btn rounded-md flex items-center justify-center h-9 w-9 relative" @mousedown.prevent :disabled="disabled || loading" :class="className" @click="clickBtn">
|
||||
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
|
||||
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span v-else :class="outlined ? 'material-icons-outlined' : 'material-icons'" :style="{ fontSize }">{{ icon }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@ -14,7 +19,8 @@ export default {
|
||||
default: 'primary'
|
||||
},
|
||||
outlined: Boolean,
|
||||
borderless: Boolean
|
||||
borderless: Boolean,
|
||||
loading: Boolean
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
@ -34,7 +40,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
clickBtn(e) {
|
||||
if (this.disabled) {
|
||||
if (this.disabled || this.loading) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<p class="text-xl">Stats for library {{ currentLibraryName }}</p>
|
||||
|
||||
<stats-preview-icons :library-stats="libraryStats" />
|
||||
<stats-preview-icons v-if="totalItems" :library-stats="libraryStats" />
|
||||
|
||||
<div class="flex md:flex-row flex-wrap justify-between flex-col mt-12">
|
||||
<div class="w-80 my-6 mx-auto">
|
||||
|
@ -107,6 +107,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Icon buttons -->
|
||||
<div class="flex items-center justify-center md:justify-start pt-4">
|
||||
<ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="startStream">
|
||||
<span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
|
||||
@ -137,6 +138,10 @@
|
||||
<ui-tooltip v-if="!isPodcast" text="Collections" direction="top">
|
||||
<ui-icon-btn icon="collections_bookmark" class="mx-0.5" outlined @click="collectionsClick" />
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-tooltip v-if="isPodcast" text="Find Episodes" direction="top">
|
||||
<ui-icon-btn icon="search" class="mx-0.5" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" />
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="my-4 max-w-2xl">
|
||||
@ -151,6 +156,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<modals-podcast-episode-feed v-model="showPodcastEpisodeFeed" :library-item="libraryItem" :episodes="podcastFeedEpisodes" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -175,7 +182,10 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
resettingProgress: false,
|
||||
isProcessingReadUpdate: false
|
||||
isProcessingReadUpdate: false,
|
||||
fetchingRSSFeed: false,
|
||||
showPodcastEpisodeFeed: false,
|
||||
podcastFeedEpisodes: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -330,6 +340,28 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async findEpisodesClick() {
|
||||
if (!this.mediaMetadata.feedUrl) {
|
||||
return this.$toast.error('Podcast does not have an RSS Feed')
|
||||
}
|
||||
this.fetchingRSSFeed = true
|
||||
var podcastfeed = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed: this.mediaMetadata.feedUrl }).catch((error) => {
|
||||
console.error('Failed to get feed', error)
|
||||
this.$toast.error('Failed to get podcast feed')
|
||||
return null
|
||||
})
|
||||
this.fetchingRSSFeed = false
|
||||
if (!podcastfeed) return
|
||||
|
||||
console.log('Podcast feed', podcastfeed)
|
||||
if (!podcastfeed.episodes || !podcastfeed.episodes.length) {
|
||||
this.$toast.info('No episodes found in RSS feed')
|
||||
return
|
||||
}
|
||||
|
||||
this.podcastFeedEpisodes = podcastfeed.episodes
|
||||
this.showPodcastEpisodeFeed = true
|
||||
},
|
||||
showEditCover() {
|
||||
this.$store.commit('setBookshelfBookIds', [])
|
||||
this.$store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'cover' })
|
||||
|
@ -25,7 +25,7 @@ Vue.prototype.$addDaysToToday = (daysToAdd) => {
|
||||
}
|
||||
|
||||
Vue.prototype.$bytesPretty = (bytes, decimals = 2) => {
|
||||
if (isNaN(bytes) || !bytes === 0) {
|
||||
if (isNaN(bytes) || bytes == 0) {
|
||||
return '0 Bytes'
|
||||
}
|
||||
const k = 1024
|
||||
|
@ -126,5 +126,46 @@ class PodcastController {
|
||||
episodes: newEpisodes || []
|
||||
})
|
||||
}
|
||||
|
||||
async downloadEpisodes(req, res) {
|
||||
var libraryItem = this.db.getLibraryItem(req.params.id)
|
||||
if (!libraryItem || libraryItem.mediaType !== 'podcast') {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
if (!req.user.canUpload || !req.user.checkCanAccessLibrary(libraryItem.libraryId)) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
var episodes = req.body
|
||||
if (!episodes || !episodes.length) {
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
|
||||
this.podcastManager.downloadPodcastEpisodes(libraryItem, episodes)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
async updateEpisode(req, res) {
|
||||
var libraryItem = this.db.getLibraryItem(req.params.id)
|
||||
if (!libraryItem || libraryItem.mediaType !== 'podcast') {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
if (!req.user.canUpload || !req.user.checkCanAccessLibrary(libraryItem.libraryId)) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
var episodeId = req.params.episodeId
|
||||
if (!libraryItem.media.checkHasEpisode(episodeId)) {
|
||||
return res.status(500).send('Episode not found')
|
||||
}
|
||||
|
||||
var wasUpdated = libraryItem.media.updateEpisode(episodeId, req.body)
|
||||
if (wasUpdated) {
|
||||
await this.db.insertLibraryItem(libraryItem)
|
||||
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
}
|
||||
|
||||
res.json(libraryItem.toJSONExpanded())
|
||||
}
|
||||
}
|
||||
module.exports = new PodcastController()
|
@ -115,6 +115,20 @@ class PodcastEpisode {
|
||||
this.updatedAt = Date.now()
|
||||
}
|
||||
|
||||
update(payload) {
|
||||
var hasUpdates = false
|
||||
for (const key in this.toJSON()) {
|
||||
if (payload[key] != undefined && payload[key] != this[key]) {
|
||||
this[key] = payload[key]
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
if (hasUpdates) {
|
||||
this.updatedAt = Date.now()
|
||||
}
|
||||
return hasUpdates
|
||||
}
|
||||
|
||||
// Only checks container format
|
||||
checkCanDirectPlay(payload) {
|
||||
var supportedMimeTypes = payload.supportedMimeTypes || []
|
||||
|
@ -115,6 +115,12 @@ class Podcast {
|
||||
return hasUpdates
|
||||
}
|
||||
|
||||
updateEpisode(id, payload) {
|
||||
var episode = this.episodes.find(ep => ep.id == id)
|
||||
if (!episode) return false
|
||||
return episode.update(payload)
|
||||
}
|
||||
|
||||
updateCover(coverPath) {
|
||||
coverPath = coverPath.replace(/\\/g, '/')
|
||||
if (this.coverPath === coverPath) return false
|
||||
|
@ -177,6 +177,8 @@ class ApiRouter {
|
||||
this.router.post('/podcasts', PodcastController.create.bind(this))
|
||||
this.router.post('/podcasts/feed', PodcastController.getPodcastFeed.bind(this))
|
||||
this.router.get('/podcasts/:id/checknew', PodcastController.checkNewEpisodes.bind(this))
|
||||
this.router.post('/podcasts/:id/download-episodes', PodcastController.downloadEpisodes.bind(this))
|
||||
this.router.patch('/podcasts/:id/episode/:episodeId', PodcastController.updateEpisode.bind(this))
|
||||
|
||||
//
|
||||
// Misc Routes
|
||||
|
Loading…
Reference in New Issue
Block a user