mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Update podcast search page to support manually entering podcast RSS feed
This commit is contained in:
		
							parent
							
								
									2c6e1cc2b5
								
							
						
					
					
						commit
						4edba20e9e
					
				| @ -56,7 +56,6 @@ | ||||
|                 <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> | ||||
| @ -246,17 +245,15 @@ export default { | ||||
|           this.$toast.error(errorMsg) | ||||
|         }) | ||||
|     }, | ||||
|     saveEpisode(episode) { | ||||
|       console.log('Save episode', episode) | ||||
|     }, | ||||
|     init() { | ||||
|       this.podcast.title = this._podcastData.title | ||||
|       this.podcast.author = this._podcastData.artistName || '' | ||||
|       this.podcast.description = this._podcastData.description || this.feedMetadata.description || '' | ||||
|       // Prefer using itunes podcast data but not always passed in if manually entering rss feed | ||||
|       this.podcast.title = this._podcastData.title || this.feedMetadata.title || '' | ||||
|       this.podcast.author = this._podcastData.artistName || this.feedMetadata.author || '' | ||||
|       this.podcast.description = this._podcastData.description || this.feedMetadata.descriptionPlain || '' | ||||
|       this.podcast.releaseDate = this._podcastData.releaseDate || '' | ||||
|       this.podcast.genres = this._podcastData.genres || [] | ||||
|       this.podcast.feedUrl = this._podcastData.feedUrl | ||||
|       this.podcast.imageUrl = this._podcastData.cover || '' | ||||
|       this.podcast.genres = this._podcastData.genres || this.feedMetadata.categories || [] | ||||
|       this.podcast.feedUrl = this._podcastData.feedUrl || this.feedMetadata.feedUrl || '' | ||||
|       this.podcast.imageUrl = this._podcastData.cover || this.feedMetadata.image || '' | ||||
|       this.podcast.itunesPageUrl = this._podcastData.pageUrl || '' | ||||
|       this.podcast.itunesId = this._podcastData.id || '' | ||||
|       this.podcast.itunesArtistId = this._podcastData.artistId || '' | ||||
|  | ||||
| @ -342,15 +342,16 @@ export default { | ||||
|         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) => { | ||||
|       var payload = 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 | ||||
|       if (!payload) return | ||||
| 
 | ||||
|       console.log('Podcast feed', podcastfeed) | ||||
|       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 | ||||
|  | ||||
| @ -6,9 +6,9 @@ | ||||
|         <app-book-shelf-toolbar page="podcast-search" /> | ||||
|         <div class="w-full h-full overflow-y-auto p-12 relative"> | ||||
|           <div class="w-full max-w-3xl mx-auto"> | ||||
|             <form @submit.prevent="submitSearch" class="flex"> | ||||
|               <ui-text-input v-model="searchTerm" :disabled="processing" placeholder="Search term" class="flex-grow mr-2" /> | ||||
|               <ui-btn type="submit" :disabled="processing">Search Podcasts</ui-btn> | ||||
|             <form @submit.prevent="submit" class="flex"> | ||||
|               <ui-text-input v-model="searchInput" :disabled="processing" placeholder="Enter search term or RSS feed URL" class="flex-grow mr-2" /> | ||||
|               <ui-btn type="submit" :disabled="processing">Submit</ui-btn> | ||||
|             </form> | ||||
|           </div> | ||||
| 
 | ||||
| @ -54,11 +54,10 @@ export default { | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       searchTerm: '', | ||||
|       searchInput: '', | ||||
|       results: [], | ||||
|       termSearched: '', | ||||
|       processing: false, | ||||
| 
 | ||||
|       showNewPodcastModal: false, | ||||
|       selectedPodcast: null, | ||||
|       selectedPodcastFeed: null | ||||
| @ -70,13 +69,35 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     async submitSearch() { | ||||
|       if (!this.searchTerm) return | ||||
|       console.log('Searching', this.searchTerm) | ||||
|       var term = this.searchTerm | ||||
|     submit() { | ||||
|       if (!this.searchInput) return | ||||
| 
 | ||||
|       if (this.searchInput.startsWith('http:') || this.searchInput.startsWith('https:')) { | ||||
|         this.termSearched = '' | ||||
|         this.results = [] | ||||
|         this.checkRSSFeed(this.searchInput) | ||||
|       } else { | ||||
|         this.submitSearch(this.searchInput) | ||||
|       } | ||||
|     }, | ||||
|     async checkRSSFeed(rssFeed) { | ||||
|       this.processing = true | ||||
|       var payload = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed }).catch((error) => { | ||||
|         console.error('Failed to get feed', error) | ||||
|         this.$toast.error('Failed to get podcast feed') | ||||
|         return null | ||||
|       }) | ||||
|       this.processing = false | ||||
|       if (!payload) return | ||||
| 
 | ||||
|       this.selectedPodcastFeed = payload.podcast | ||||
|       this.selectedPodcast = null | ||||
|       this.showNewPodcastModal = true | ||||
|     }, | ||||
|     async submitSearch(term) { | ||||
|       this.processing = true | ||||
|       this.termSearched = '' | ||||
|       var results = await this.$axios.$get(`/api/search/podcast?term=${encodeURIComponent(this.searchTerm)}`).catch((error) => { | ||||
|       var results = await this.$axios.$get(`/api/search/podcast?term=${encodeURIComponent(term)}`).catch((error) => { | ||||
|         console.error('Search request failed', error) | ||||
|         return [] | ||||
|       }) | ||||
| @ -92,17 +113,18 @@ export default { | ||||
|         return | ||||
|       } | ||||
|       this.processing = true | ||||
|       var podcastfeed = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed: podcast.feedUrl }).catch((error) => { | ||||
|       var payload = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed: podcast.feedUrl }).catch((error) => { | ||||
|         console.error('Failed to get feed', error) | ||||
|         this.$toast.error('Failed to get podcast feed') | ||||
|         return null | ||||
|       }) | ||||
|       this.processing = false | ||||
|       if (!podcastfeed) return | ||||
|       this.selectedPodcastFeed = podcastfeed | ||||
|       if (!payload) return | ||||
| 
 | ||||
|       this.selectedPodcastFeed = payload.podcast | ||||
|       this.selectedPodcast = podcast | ||||
|       this.showNewPodcastModal = true | ||||
|       console.log('Got podcast feed', podcastfeed) | ||||
|       console.log('Got podcast feed', payload.podcast) | ||||
|     } | ||||
|   }, | ||||
|   mounted() {} | ||||
|  | ||||
| @ -94,17 +94,18 @@ class PodcastController { | ||||
|     if (!url) { | ||||
|       return res.status(400).send('Bad request') | ||||
|     } | ||||
|     var includeRaw = req.query.raw == 1 // Include raw json
 | ||||
| 
 | ||||
|     axios.get(url).then(async (data) => { | ||||
|       if (!data || !data.data) { | ||||
|         Logger.error('Invalid podcast feed request response') | ||||
|         return res.status(500).send('Bad response from feed request') | ||||
|       } | ||||
|       var podcast = await parsePodcastRssFeedXml(data.data) | ||||
|       if (!podcast) { | ||||
|       var payload = await parsePodcastRssFeedXml(data.data, includeRaw) | ||||
|       if (!payload) { | ||||
|         return res.status(500).send('Invalid podcast RSS feed') | ||||
|       } | ||||
|       res.json(podcast) | ||||
|       res.json(payload) | ||||
|     }).catch((error) => { | ||||
|       console.error('Failed', error) | ||||
|       res.status(500).send(error) | ||||
|  | ||||
| @ -190,11 +190,11 @@ class PodcastManager { | ||||
|         Logger.error('Invalid podcast feed request response') | ||||
|         return false | ||||
|       } | ||||
|       var podcast = await parsePodcastRssFeedXml(data.data) | ||||
|       if (!podcast) { | ||||
|       var payload = await parsePodcastRssFeedXml(data.data) | ||||
|       if (!payload) { | ||||
|         return false | ||||
|       } | ||||
|       return podcast | ||||
|       return payload.podcast | ||||
|     }).catch((error) => { | ||||
|       console.error('Failed', error) | ||||
|       return false | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| const Logger = require('../Logger') | ||||
| const { xmlToJSON } = require('./index') | ||||
| const { stripHtml } = require('string-strip-html') | ||||
| 
 | ||||
| function extractFirstArrayItem(json, key) { | ||||
|   if (!json[key] || !json[key].length) return null | ||||
| @ -39,11 +40,26 @@ function extractCategories(channel) { | ||||
| } | ||||
| 
 | ||||
| function extractPodcastMetadata(channel) { | ||||
|   var arrayFields = ['title', 'language', 'description', 'itunes:explicit', 'itunes:author'] | ||||
|   var metadata = { | ||||
|     image: extractImage(channel), | ||||
|     categories: extractCategories(channel) | ||||
|     categories: extractCategories(channel), | ||||
|     feedUrl: null, | ||||
|     description: null, | ||||
|     descriptionPlain: null | ||||
|   } | ||||
| 
 | ||||
|   if (channel['itunes:new-feed-url']) { | ||||
|     metadata.feedUrl = extractFirstArrayItem(channel, 'itunes:new-feed-url') | ||||
|   } else if (channel['atom:link'] && channel['atom:link'].length && channel['atom:link'][0]['$']) { | ||||
|     metadata.feedUrl = channel['atom:link'][0]['$'].href || null | ||||
|   } | ||||
| 
 | ||||
|   if (channel['description']) { | ||||
|     metadata.description = extractFirstArrayItem(channel, 'description') | ||||
|     metadata.descriptionPlain = stripHtml(metadata.description || '').result | ||||
|   } | ||||
| 
 | ||||
|   var arrayFields = ['title', 'language', 'itunes:explicit', 'itunes:author', 'pubDate', 'link'] | ||||
|   arrayFields.forEach((key) => { | ||||
|     var cleanKey = key.split(':').pop() | ||||
|     metadata[cleanKey] = extractFirstArrayItem(channel, key) | ||||
| @ -114,12 +130,25 @@ function cleanPodcastJson(rssJson) { | ||||
|   return podcast | ||||
| } | ||||
| 
 | ||||
| module.exports.parsePodcastRssFeedXml = async (xml) => { | ||||
| module.exports.parsePodcastRssFeedXml = async (xml, includeRaw = false) => { | ||||
|   if (!xml) return null | ||||
|   var json = await xmlToJSON(xml) | ||||
|   if (!json || !json.rss) { | ||||
|     Logger.error('[podcastUtils] Invalid XML or RSS feed') | ||||
|     return null | ||||
|   } | ||||
|   return cleanPodcastJson(json.rss) | ||||
| 
 | ||||
|   const podcast = cleanPodcastJson(json.rss) | ||||
|   if (!podcast) return null | ||||
| 
 | ||||
|   if (includeRaw) { | ||||
|     return { | ||||
|       podcast, | ||||
|       rawJson: json | ||||
|     } | ||||
|   } else { | ||||
|     return { | ||||
|       podcast | ||||
|     } | ||||
|   } | ||||
| } | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user