mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-11-03 19:07:00 -05: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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user