2022-04-10 20:31:47 -05:00
|
|
|
<template>
|
|
|
|
<div class="w-full">
|
2022-12-10 10:12:58 -06:00
|
|
|
<!-- Podcast episode downloads queue -->
|
2023-12-10 17:53:27 -06:00
|
|
|
<div v-if="episodeDownloadsQueued.length" class="px-4 py-2 my-2 bg-info bg-opacity-40 text-sm font-semibold rounded-md text-fg relative w-full">
|
2022-12-10 10:12:58 -06:00
|
|
|
<div class="flex items-center">
|
2023-12-03 17:37:01 -06:00
|
|
|
<p class="text-sm py-1">{{ $getString('MessageEpisodesQueuedForDownload', [episodeDownloadsQueued.length]) }}</p>
|
2022-12-10 10:12:58 -06:00
|
|
|
<div class="flex-grow" />
|
2025-03-30 23:26:14 -07:00
|
|
|
<span v-if="isAdminOrUp" class="material-symbols text-xl ml-3 cursor-pointer" @click="clearDownloadQueue">close</span>
|
2022-12-10 10:12:58 -06:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Podcast episodes currently downloading -->
|
2023-12-10 17:53:27 -06:00
|
|
|
<div v-if="episodesDownloading.length" class="px-4 py-2 my-2 bg-success bg-opacity-20 text-sm font-semibold rounded-md text-fg relative w-full">
|
2022-12-10 10:12:58 -06:00
|
|
|
<div v-for="episode in episodesDownloading" :key="episode.id" class="flex items-center">
|
|
|
|
<widgets-loading-spinner />
|
2023-12-03 17:37:01 -06:00
|
|
|
<p class="text-sm py-1 pl-4">{{ $strings.MessageDownloadingEpisode }} "{{ episode.episodeDisplayTitle }}"</p>
|
2022-12-10 10:12:58 -06:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
2022-06-01 19:38:26 -05:00
|
|
|
<div class="flex items-center">
|
2023-12-03 17:37:01 -06:00
|
|
|
<p class="text-lg mb-1 font-semibold">{{ $strings.HeaderEpisodes }} ({{ episodesFiltered.length }})</p>
|
2022-12-09 18:03:17 -06:00
|
|
|
|
2022-06-01 19:38:26 -05:00
|
|
|
<div class="flex-grow" />
|
2022-12-09 18:03:17 -06:00
|
|
|
|
|
|
|
<button v-if="isAdminOrUp && !fetchingRSSFeed" class="outline:none mx-1 pt-0.5 relative" @click="searchEpisodes">
|
2025-03-30 23:26:14 -07:00
|
|
|
<span class="material-symbols text-xl text-fg">search</span>
|
2022-12-09 18:03:17 -06:00
|
|
|
</button>
|
|
|
|
<widgets-loading-spinner v-else-if="fetchingRSSFeed" class="mx-1" />
|
|
|
|
|
2022-06-01 19:38:26 -05:00
|
|
|
<button class="outline:none mx-3 pt-0.5 relative" @click="showFilters">
|
2025-03-30 23:26:14 -07:00
|
|
|
<span class="material-symbols text-xl text-fg">filter_alt</span>
|
2022-06-03 19:46:43 -05:00
|
|
|
<div v-show="filterKey !== 'all' && episodesAreFiltered" class="absolute top-0 right-0 w-1.5 h-1.5 rounded-full bg-success border border-green-300 shadow-sm z-10 pointer-events-none" />
|
2022-06-01 19:38:26 -05:00
|
|
|
</button>
|
2022-04-12 18:40:35 -05:00
|
|
|
|
2022-06-01 19:38:26 -05:00
|
|
|
<div class="flex items-center border border-white border-opacity-25 rounded px-2" @click="clickSort">
|
2023-12-10 17:53:27 -06:00
|
|
|
<p class="text-sm text-fg">{{ sortText }}</p>
|
2025-03-30 23:26:14 -07:00
|
|
|
<span class="material-symbols ml-1 text-fg">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
|
2022-06-01 19:38:26 -05:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
2022-06-03 19:46:43 -05:00
|
|
|
<template v-for="episode in episodesSorted">
|
2025-03-22 12:50:08 +01:00
|
|
|
<tables-podcast-episode-row :episode="episode" :local-episode="localEpisodeMap[episode.id]" :library-item-id="libraryItemId" :local-library-item-id="localLibraryItemId" :is-local="isLocal" :sort-key="sortKey" :key="episode.id" @addToPlaylist="addEpisodeToPlaylist" />
|
2022-04-10 20:31:47 -05:00
|
|
|
</template>
|
2022-06-01 19:38:26 -05:00
|
|
|
|
2022-12-09 18:03:17 -06:00
|
|
|
<!-- Huhhh?
|
2022-06-03 19:46:43 -05:00
|
|
|
Without anything below the template it will not re-render -->
|
|
|
|
<p> </p>
|
|
|
|
|
2022-06-01 19:38:26 -05:00
|
|
|
<modals-dialog v-model="showFiltersModal" title="Episode Filter" :items="filterItems" :selected="filterKey" @action="setFilter" />
|
2022-12-09 18:03:17 -06:00
|
|
|
|
|
|
|
<modals-podcast-episodes-feed-modal v-model="showPodcastEpisodeFeed" :library-item="libraryItem" :episodes="podcastFeedEpisodes" />
|
2023-05-20 17:12:36 -05:00
|
|
|
|
|
|
|
<modals-order-modal v-model="showSortModal" :order-by.sync="sortKey" :descending.sync="sortDesc" episodes />
|
2022-04-10 20:31:47 -05:00
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
2022-12-10 10:12:58 -06:00
|
|
|
import { Dialog } from '@capacitor/dialog'
|
|
|
|
|
2022-04-10 20:31:47 -05:00
|
|
|
export default {
|
|
|
|
props: {
|
2022-12-03 17:05:43 -06:00
|
|
|
libraryItem: {
|
|
|
|
type: Object,
|
|
|
|
default: () => {}
|
|
|
|
},
|
2022-04-10 20:31:47 -05:00
|
|
|
episodes: {
|
|
|
|
type: Array,
|
|
|
|
default: () => []
|
2022-04-15 20:48:39 -05:00
|
|
|
},
|
|
|
|
localLibraryItemId: String,
|
|
|
|
localEpisodes: {
|
|
|
|
type: Array,
|
|
|
|
default: () => []
|
|
|
|
},
|
|
|
|
isLocal: Boolean // If is local then episodes and libraryItemId are local, otherwise local is passed in localLibraryItemId and localEpisodes
|
2022-04-10 20:31:47 -05:00
|
|
|
},
|
|
|
|
data() {
|
2022-06-01 19:38:26 -05:00
|
|
|
return {
|
|
|
|
episodesCopy: [],
|
|
|
|
showFiltersModal: false,
|
2023-05-20 17:12:36 -05:00
|
|
|
showSortModal: false,
|
2022-06-01 19:38:26 -05:00
|
|
|
sortKey: 'publishedAt',
|
2022-08-11 17:37:13 -05:00
|
|
|
sortDesc: true,
|
2022-06-03 19:46:43 -05:00
|
|
|
filterKey: 'incomplete',
|
2022-12-09 18:03:17 -06:00
|
|
|
fetchingRSSFeed: false,
|
|
|
|
podcastFeedEpisodes: [],
|
2022-12-10 10:12:58 -06:00
|
|
|
showPodcastEpisodeFeed: false,
|
|
|
|
episodesDownloading: [],
|
|
|
|
episodeDownloadsQueued: []
|
2022-06-01 19:38:26 -05:00
|
|
|
}
|
|
|
|
},
|
|
|
|
watch: {
|
|
|
|
episodes: {
|
|
|
|
immediate: true,
|
|
|
|
handler() {
|
|
|
|
this.init()
|
|
|
|
}
|
|
|
|
}
|
2022-04-10 20:31:47 -05:00
|
|
|
},
|
2022-04-15 20:48:39 -05:00
|
|
|
computed: {
|
2022-12-09 18:03:17 -06:00
|
|
|
isAdminOrUp() {
|
|
|
|
return this.$store.getters['user/getIsAdminOrUp']
|
|
|
|
},
|
2024-10-31 21:14:25 +02:00
|
|
|
socketConnected() {
|
|
|
|
return this.$store.state.socketConnected
|
2022-12-10 10:32:16 -06:00
|
|
|
},
|
2022-12-03 17:05:43 -06:00
|
|
|
libraryItemId() {
|
2023-11-25 12:56:29 -06:00
|
|
|
return this.libraryItem?.id || null
|
2022-12-03 17:05:43 -06:00
|
|
|
},
|
2022-12-09 18:03:17 -06:00
|
|
|
media() {
|
2023-11-25 12:56:29 -06:00
|
|
|
return this.libraryItem?.media || {}
|
2022-12-09 18:03:17 -06:00
|
|
|
},
|
|
|
|
mediaMetadata() {
|
|
|
|
return this.media.metadata || {}
|
|
|
|
},
|
2022-06-03 19:46:43 -05:00
|
|
|
episodesAreFiltered() {
|
|
|
|
return this.episodesFiltered.length !== this.episodesCopy.length
|
|
|
|
},
|
2023-12-04 17:53:36 -06:00
|
|
|
episodeSortItems() {
|
|
|
|
return [
|
|
|
|
{
|
|
|
|
text: this.$strings.LabelPubDate,
|
|
|
|
value: 'publishedAt'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: this.$strings.LabelTitle,
|
|
|
|
value: 'title'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: this.$strings.LabelSeason,
|
|
|
|
value: 'season'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: this.$strings.LabelEpisode,
|
|
|
|
value: 'episode'
|
2025-03-22 12:50:08 +01:00
|
|
|
},
|
|
|
|
{
|
|
|
|
text: this.$strings.LabelFilename,
|
|
|
|
value: 'audioFile.metadata.filename'
|
2023-12-04 17:53:36 -06:00
|
|
|
}
|
|
|
|
]
|
|
|
|
},
|
|
|
|
filterItems() {
|
|
|
|
return [
|
|
|
|
{
|
|
|
|
text: this.$strings.LabelShowAll,
|
|
|
|
value: 'all'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: this.$strings.LabelIncomplete,
|
|
|
|
value: 'incomplete'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: this.$strings.LabelInProgress,
|
|
|
|
value: 'inProgress'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: this.$strings.LabelComplete,
|
|
|
|
value: 'complete'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: this.$strings.LabelDownloaded,
|
|
|
|
value: 'downloaded'
|
|
|
|
}
|
|
|
|
]
|
|
|
|
},
|
2022-06-03 19:46:43 -05:00
|
|
|
episodesFiltered() {
|
|
|
|
return this.episodesCopy.filter((ep) => {
|
2023-11-25 12:56:29 -06:00
|
|
|
if (this.filterKey === 'downloaded') {
|
|
|
|
return !!this.localEpisodeMap[ep.id]
|
|
|
|
}
|
2022-06-03 19:46:43 -05:00
|
|
|
var mediaProgress = this.getEpisodeProgress(ep)
|
|
|
|
if (this.filterKey === 'incomplete') {
|
2023-11-25 12:56:29 -06:00
|
|
|
return !mediaProgress?.isFinished
|
2022-06-03 19:46:43 -05:00
|
|
|
} else if (this.filterKey === 'complete') {
|
2023-11-25 12:56:29 -06:00
|
|
|
return mediaProgress?.isFinished
|
2022-06-03 19:46:43 -05:00
|
|
|
} else if (this.filterKey === 'inProgress') {
|
|
|
|
return mediaProgress && !mediaProgress.isFinished
|
|
|
|
} else if (this.filterKey === 'all') {
|
|
|
|
console.log('Filter key is all')
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
})
|
|
|
|
},
|
2022-06-01 19:38:26 -05:00
|
|
|
episodesSorted() {
|
|
|
|
return this.episodesFiltered.sort((a, b) => {
|
2025-03-22 12:50:08 +01:00
|
|
|
let aValue
|
|
|
|
let bValue
|
|
|
|
|
|
|
|
if (this.sortKey.includes('.')) {
|
|
|
|
const getNestedValue = (ob, s) => s.split('.').reduce((o, k) => o?.[k], ob)
|
|
|
|
aValue = getNestedValue(a, this.sortKey)
|
|
|
|
bValue = getNestedValue(b, this.sortKey)
|
|
|
|
} else {
|
|
|
|
aValue = a[this.sortKey]
|
|
|
|
bValue = b[this.sortKey]
|
|
|
|
}
|
2023-05-20 17:12:36 -05:00
|
|
|
|
|
|
|
// Sort episodes with no pub date as the oldest
|
|
|
|
if (this.sortKey === 'publishedAt') {
|
|
|
|
if (!aValue) aValue = Number.MAX_VALUE
|
|
|
|
if (!bValue) bValue = Number.MAX_VALUE
|
|
|
|
}
|
|
|
|
|
2022-06-01 19:38:26 -05:00
|
|
|
if (this.sortDesc) {
|
2023-05-20 17:12:36 -05:00
|
|
|
return String(bValue).localeCompare(String(aValue), undefined, { numeric: true, sensitivity: 'base' })
|
2022-06-01 19:38:26 -05:00
|
|
|
}
|
2023-05-20 17:12:36 -05:00
|
|
|
return String(aValue).localeCompare(String(bValue), undefined, { numeric: true, sensitivity: 'base' })
|
2022-06-01 19:38:26 -05:00
|
|
|
})
|
|
|
|
},
|
2022-04-15 20:48:39 -05:00
|
|
|
// Map of local episodes where server episode id is key
|
|
|
|
localEpisodeMap() {
|
|
|
|
var epmap = {}
|
|
|
|
this.localEpisodes.forEach((localEp) => {
|
|
|
|
if (localEp.serverEpisodeId) {
|
|
|
|
epmap[localEp.serverEpisodeId] = localEp
|
|
|
|
}
|
|
|
|
})
|
|
|
|
return epmap
|
2022-06-01 19:38:26 -05:00
|
|
|
},
|
|
|
|
sortText() {
|
|
|
|
if (!this.sortKey) return ''
|
2023-05-20 17:12:36 -05:00
|
|
|
const _sel = this.episodeSortItems.find((i) => i.value === this.sortKey)
|
|
|
|
return _sel?.text || ''
|
2022-06-01 19:38:26 -05:00
|
|
|
}
|
|
|
|
},
|
|
|
|
methods: {
|
2022-12-10 10:12:58 -06:00
|
|
|
async clearDownloadQueue() {
|
|
|
|
const { value } = await Dialog.confirm({
|
|
|
|
title: 'Confirm',
|
|
|
|
message: `Are you sure you want to clear episode download queue?`
|
|
|
|
})
|
|
|
|
|
|
|
|
if (value) {
|
2023-09-17 12:43:50 -05:00
|
|
|
this.$nativeHttp
|
|
|
|
.get(`/api/podcasts/${this.libraryItemId}/clear-queue`)
|
2022-12-10 10:12:58 -06:00
|
|
|
.then(() => {
|
|
|
|
this.$toast.success('Episode download queue cleared')
|
|
|
|
this.episodeDownloadQueued = []
|
|
|
|
})
|
|
|
|
.catch((error) => {
|
|
|
|
console.error('Failed to clear queue', error)
|
|
|
|
this.$toast.error('Failed to clear queue')
|
|
|
|
})
|
|
|
|
}
|
|
|
|
},
|
2022-12-09 18:03:17 -06:00
|
|
|
async searchEpisodes() {
|
2024-10-31 21:14:25 +02:00
|
|
|
if (!this.socketConnected) {
|
2023-12-04 17:53:36 -06:00
|
|
|
return this.$toast.error(this.$strings.MessageNoNetworkConnection)
|
2022-12-10 10:32:16 -06:00
|
|
|
}
|
|
|
|
|
2022-12-09 18:03:17 -06:00
|
|
|
if (!this.mediaMetadata.feedUrl) {
|
|
|
|
return this.$toast.error('Podcast does not have an RSS Feed')
|
|
|
|
}
|
|
|
|
this.fetchingRSSFeed = true
|
2023-09-15 17:35:44 -05:00
|
|
|
const payload = await this.$nativeHttp.post(`/api/podcasts/feed`, { rssFeed: this.mediaMetadata.feedUrl }).catch((error) => {
|
2022-12-09 18:03:17 -06:00
|
|
|
console.error('Failed to get feed', error)
|
|
|
|
this.$toast.error('Failed to get podcast feed')
|
|
|
|
return null
|
|
|
|
})
|
|
|
|
this.fetchingRSSFeed = false
|
|
|
|
if (!payload) return
|
|
|
|
|
|
|
|
console.log('Podcast feed', payload)
|
|
|
|
const podcastfeed = payload.podcast
|
|
|
|
if (!podcastfeed.episodes || !podcastfeed.episodes.length) {
|
|
|
|
this.$toast.info('No episodes found in RSS feed')
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
this.podcastFeedEpisodes = podcastfeed.episodes
|
|
|
|
this.showPodcastEpisodeFeed = true
|
|
|
|
},
|
2022-12-03 17:05:43 -06:00
|
|
|
addEpisodeToPlaylist(episode) {
|
|
|
|
this.$store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem, episode }])
|
|
|
|
this.$store.commit('globals/setShowPlaylistsAddCreateModal', true)
|
|
|
|
},
|
2022-06-01 19:38:26 -05:00
|
|
|
setFilter(filter) {
|
|
|
|
this.filterKey = filter
|
|
|
|
this.showFiltersModal = false
|
|
|
|
},
|
|
|
|
showFilters() {
|
|
|
|
this.showFiltersModal = true
|
|
|
|
},
|
|
|
|
clickSort() {
|
2023-05-20 17:12:36 -05:00
|
|
|
this.showSortModal = true
|
2022-06-01 19:38:26 -05:00
|
|
|
},
|
|
|
|
getEpisodeProgress(episode) {
|
|
|
|
if (this.isLocal) return this.$store.getters['globals/getLocalMediaProgressById'](this.libraryItemId, episode.id)
|
|
|
|
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, episode.id)
|
|
|
|
},
|
|
|
|
init() {
|
2025-01-26 07:43:44 -08:00
|
|
|
this.sortDesc = this.mediaMetadata.type === 'episodic'
|
2022-06-01 19:38:26 -05:00
|
|
|
this.episodesCopy = this.episodes.map((ep) => {
|
|
|
|
return { ...ep }
|
|
|
|
})
|
2022-12-10 10:12:58 -06:00
|
|
|
},
|
|
|
|
episodeDownloadQueued(episodeDownload) {
|
|
|
|
if (episodeDownload.libraryItemId === this.libraryItemId) {
|
|
|
|
this.episodeDownloadsQueued.push(episodeDownload)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
episodeDownloadStarted(episodeDownload) {
|
|
|
|
if (episodeDownload.libraryItemId === this.libraryItemId) {
|
|
|
|
this.episodeDownloadsQueued = this.episodeDownloadsQueued.filter((d) => d.id !== episodeDownload.id)
|
|
|
|
this.episodesDownloading.push(episodeDownload)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
episodeDownloadFinished(episodeDownload) {
|
|
|
|
if (episodeDownload.libraryItemId === this.libraryItemId) {
|
|
|
|
this.episodeDownloadsQueued = this.episodeDownloadsQueued.filter((d) => d.id !== episodeDownload.id)
|
|
|
|
this.episodesDownloading = this.episodesDownloading.filter((d) => d.id !== episodeDownload.id)
|
|
|
|
}
|
2022-04-15 20:48:39 -05:00
|
|
|
}
|
|
|
|
},
|
2022-12-10 10:12:58 -06:00
|
|
|
mounted() {
|
2023-11-25 13:09:43 -06:00
|
|
|
if (this.$route.query['episodefilter'] === 'downloaded') {
|
|
|
|
this.filterKey = 'downloaded'
|
|
|
|
}
|
2022-12-10 10:12:58 -06:00
|
|
|
this.$socket.$on('episode_download_queued', this.episodeDownloadQueued)
|
|
|
|
this.$socket.$on('episode_download_started', this.episodeDownloadStarted)
|
|
|
|
this.$socket.$on('episode_download_finished', this.episodeDownloadFinished)
|
|
|
|
},
|
|
|
|
beforeDestroy() {
|
|
|
|
this.$socket.$off('episode_download_queued', this.episodeDownloadQueued)
|
|
|
|
this.$socket.$off('episode_download_started', this.episodeDownloadStarted)
|
|
|
|
this.$socket.$off('episode_download_finished', this.episodeDownloadFinished)
|
|
|
|
}
|
2022-04-10 20:31:47 -05:00
|
|
|
}
|
2024-10-31 21:14:25 +02:00
|
|
|
</script>
|