mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Add:Podcast search page
This commit is contained in:
		
							parent
							
								
									a907c88f66
								
							
						
					
					
						commit
						c6eb1096e8
					
				| @ -12,7 +12,7 @@ | ||||
|       </nuxt-link> | ||||
|     </div> | ||||
|     <div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-30 flex items-center justify-end md:justify-start px-2 md:px-8"> | ||||
|       <template v-if="page !== 'search' && !isHome"> | ||||
|       <template v-if="page !== 'search' && page !== 'podcast-search' && !isHome"> | ||||
|         <p v-if="!selectedSeries" class="font-book hidden md:block">{{ numShowing }} {{ entityName }}</p> | ||||
|         <div v-else class="items-center hidden md:flex"> | ||||
|           <div @click="seriesBackArrow" class="rounded-full h-9 w-9 flex items-center justify-center hover:bg-white hover:bg-opacity-10 cursor-pointer"> | ||||
|  | ||||
| @ -52,6 +52,14 @@ | ||||
|       <div v-show="isAuthorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> | ||||
|     </nuxt-link> | ||||
| 
 | ||||
|     <nuxt-link v-if="showExperimentalFeatures && isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> | ||||
|       <icons-podcasts-svg class="w-6 h-6" /> | ||||
| 
 | ||||
|       <p class="font-book pt-1.5" style="font-size: 0.9rem">Search</p> | ||||
| 
 | ||||
|       <div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> | ||||
|     </nuxt-link> | ||||
| 
 | ||||
|     <nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : ' bg-error bg-opacity-20'"> | ||||
|       <span class="material-icons text-2xl">warning</span> | ||||
| 
 | ||||
| @ -62,36 +70,6 @@ | ||||
|         <p class="text-xs font-mono pb-0.5">{{ numIssues }}</p> | ||||
|       </div> | ||||
|     </nuxt-link> | ||||
| 
 | ||||
|     <!-- <nuxt-link to="/library/collections" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> | ||||
|       <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||||
|         <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /> | ||||
|       </svg> | ||||
| 
 | ||||
|       <p class="font-book pt-1.5" style="font-size: 0.8rem">Collections</p> | ||||
| 
 | ||||
|       <div v-show="paramId === 'collections'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> | ||||
|     </nuxt-link> --> | ||||
| 
 | ||||
|     <!-- <nuxt-link to="/library/tags" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'tags' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> | ||||
|       <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||||
|         <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" /> | ||||
|       </svg> | ||||
| 
 | ||||
|       <p class="font-book pt-1.5" style="font-size: 0.8rem">Tags</p> | ||||
| 
 | ||||
|       <div v-show="paramId === 'tags'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> | ||||
|     </nuxt-link> --> | ||||
| 
 | ||||
|     <!-- <nuxt-link to="/library/authors" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'authors' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> | ||||
|       <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||||
|         <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /> | ||||
|       </svg> | ||||
| 
 | ||||
|       <p class="font-book pt-1.5" style="font-size: 0.8rem">Authors</p> | ||||
| 
 | ||||
|       <div v-show="paramId === 'authors'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> | ||||
|     </nuxt-link> --> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| @ -110,6 +88,15 @@ export default { | ||||
|     currentLibraryId() { | ||||
|       return this.$store.state.libraries.currentLibraryId | ||||
|     }, | ||||
|     currentLibraryMediaType() { | ||||
|       return this.$store.getters['libraries/getCurrentLibraryMediaType'] | ||||
|     }, | ||||
|     isPodcastLibrary() { | ||||
|       return this.currentLibraryMediaType === 'podcasts' | ||||
|     }, | ||||
|     isPodcastSearchPage() { | ||||
|       return this.$route.name === 'library-library-podcast-search' | ||||
|     }, | ||||
|     homePage() { | ||||
|       return this.$route.name === 'library-library' | ||||
|     }, | ||||
|  | ||||
							
								
								
									
										91
									
								
								client/pages/library/_library/podcast/search.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								client/pages/library/_library/podcast/search.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,91 @@ | ||||
| <template> | ||||
|   <div class="page" :class="streamAudiobook ? 'streaming' : ''"> | ||||
|     <div class="flex h-full"> | ||||
|       <app-side-rail class="hidden md:block" /> | ||||
|       <div class="flex-grow"> | ||||
|         <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> | ||||
|           </div> | ||||
| 
 | ||||
|           <div class="w-full max-w-3xl mx-auto py-4"> | ||||
|             <p v-if="termSearched && !results.length && !processing" class="text-center text-xl">No podcasts found</p> | ||||
|             <template v-for="podcast in results"> | ||||
|               <div :key="podcast.id" class="flex p-1 hover:bg-primary hover:bg-opacity-25 cursor-pointer" @click="selectPodcast(podcast)"> | ||||
|                 <div class="w-24 min-w-24 h-24 bg-primary"> | ||||
|                   <img v-if="podcast.cover" :src="podcast.cover" class="h-full w-full" /> | ||||
|                 </div> | ||||
|                 <div class="flex-grow pl-4 max-w-2xl"> | ||||
|                   <a :href="podcast.pageUrl" class="text-lg text-gray-200 hover:underline" target="_blank" @click.stop>{{ podcast.title }}</a> | ||||
|                   <p class="text-base text-gray-300 whitespace-nowrap truncate">by {{ podcast.artistName }}</p> | ||||
|                   <p class="text-xs text-gray-400 leading-5">{{ podcast.genres.join(', ') }}</p> | ||||
|                   <p class="text-xs text-gray-400 leading-5">{{ podcast.trackCount }} Episodes</p> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </template> | ||||
|           </div> | ||||
| 
 | ||||
|           <div v-show="processing" class="absolute top-0 left-0 w-full h-full flex items-center justify-center bg-black bg-opacity-25 z-40"> | ||||
|             <ui-loading-indicator /> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   data() { | ||||
|     return { | ||||
|       searchTerm: '', | ||||
|       results: [], | ||||
|       termSearched: '', | ||||
|       processing: false | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     streamAudiobook() { | ||||
|       return this.$store.state.streamAudiobook | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     async submitSearch() { | ||||
|       if (!this.searchTerm) return | ||||
|       console.log('Searching', this.searchTerm) | ||||
|       var term = this.searchTerm | ||||
|       this.processing = true | ||||
|       this.termSearched = '' | ||||
|       var results = await this.$axios.$get(`/api/search/podcast?term=${encodeURIComponent(this.searchTerm)}`).catch((error) => { | ||||
|         console.error('Search request failed', error) | ||||
|         return [] | ||||
|       }) | ||||
|       console.log('Got results', results) | ||||
|       this.results = results | ||||
|       this.termSearched = term | ||||
|       this.processing = false | ||||
|     }, | ||||
|     async selectPodcast(podcast) { | ||||
|       console.log('Selected podcast', podcast) | ||||
|       if (!podcast.feedUrl) { | ||||
|         this.$toast.error('Invalid podcast - no feed') | ||||
|         return | ||||
|       } | ||||
|       this.processing = true | ||||
|       var podcastfeed = await this.$axios.$post(`/api/getPodcastFeed`, { 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 | ||||
|       console.log('Got podcast feed', podcastfeed) | ||||
|     } | ||||
|   }, | ||||
|   mounted() {} | ||||
| } | ||||
| </script> | ||||
| @ -18,6 +18,10 @@ export const getters = { | ||||
|     if (!currentLibrary) return '' | ||||
|     return currentLibrary.name | ||||
|   }, | ||||
|   getCurrentLibraryMediaType: (state, getters) => { | ||||
|     if (!getters.getCurrentLibrary) return null | ||||
|     return getters.getCurrentLibrary.mediaType | ||||
|   }, | ||||
|   getSortedLibraries: state => () => { | ||||
|     return state.libraries.map(lib => ({ ...lib })).sort((a, b) => a.displayOrder - b.displayOrder) | ||||
|   }, | ||||
|  | ||||
| @ -9,15 +9,7 @@ class PodcastFinder { | ||||
|   async search(term, options = {}) { | ||||
|     if (!term) return null | ||||
|     Logger.debug(`[iTunes] Searching for podcast with term "${term}"`) | ||||
| 
 | ||||
|     var searchOptions = { | ||||
|       term, | ||||
|       media: 'podcast', | ||||
|       entity: 'podcast', | ||||
|       ...options | ||||
|     } | ||||
| 
 | ||||
|     var results = await this.iTunesApi.search(searchOptions) | ||||
|     var results = await this.iTunesApi.searchPodcasts(term, options) | ||||
|     Logger.debug(`[iTunes] Podcast search for "${term}" returned ${results.length} results`) | ||||
|     return results | ||||
|   } | ||||
|  | ||||
| @ -26,11 +26,39 @@ class iTunes { | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   // Example cover art: https://is1-ssl.mzstatic.com/image/thumb/Music118/v4/cb/ea/73/cbea739b-ff3b-11c4-fb93-7889fbec7390/9781598874983_cover.jpg/100x100bb.jpg
 | ||||
|   // 100x100bb can be replaced by other values https://github.com/bendodson/itunes-artwork-finder
 | ||||
|   // Target size 600 or larger
 | ||||
|   getCoverArtwork(data) { | ||||
|     if (data.artworkUrl600) { | ||||
|       return data.artworkUrl600 | ||||
|     } | ||||
|     // Should already be sorted from small to large
 | ||||
|     var artworkSizes = Object.keys(data).filter(key => key.startsWith('artworkUrl')).map(key => { | ||||
|       return { | ||||
|         url: data[key], | ||||
|         size: Number(key.replace('artworkUrl', '')) | ||||
|       } | ||||
|     }) | ||||
|     if (!artworkSizes.length) return null | ||||
| 
 | ||||
|     // Return next biggest size > 600
 | ||||
|     var nextBestSize = artworkSizes.find(size => size.size > 600) | ||||
|     if (nextBestSize) return nextBestSize.url | ||||
| 
 | ||||
|     // Find square artwork
 | ||||
|     var squareArtwork = artworkSizes.find(size => size.url.includes(`${size.size}x${size.size}bb`)) | ||||
| 
 | ||||
|     // Square cover replace with 600x600bb
 | ||||
|     if (squareArtwork) { | ||||
|       return squareArtwork.url.replace(`${squareArtwork.size}x${squareArtwork.size}bb`, '600x600bb') | ||||
|     } | ||||
| 
 | ||||
|     // Last resort just return biggest size
 | ||||
|     return artworkSizes[artworkSizes.length - 1].url | ||||
|   } | ||||
| 
 | ||||
|   cleanAudiobook(data) { | ||||
|     // Example cover art: https://is1-ssl.mzstatic.com/image/thumb/Music118/v4/cb/ea/73/cbea739b-ff3b-11c4-fb93-7889fbec7390/9781598874983_cover.jpg/100x100bb.jpg
 | ||||
|     // 100x100bb can be replaced by other values https://github.com/bendodson/itunes-artwork-finder
 | ||||
|     var cover = data.artworkUrl100 || data.artworkUrl60 || '' | ||||
|     cover = cover.replace('100x100bb', '600x600bb').replace('60x60bb', '600x600bb') | ||||
|     return { | ||||
|       id: data.collectionId, | ||||
|       artistId: data.artistId, | ||||
| @ -39,13 +67,35 @@ class iTunes { | ||||
|       description: stripHtml(data.description || '').result, | ||||
|       publishYear: data.releaseDate ? data.releaseDate.split('-')[0] : null, | ||||
|       genres: data.primaryGenreName ? [data.primaryGenreName] : [], | ||||
|       cover | ||||
|       cover: this.getCoverArtwork(data) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   searchAudiobooks(term) { | ||||
|     return this.search({ term, entity: 'audiobook', media: 'audiobook' }).then((results) => { | ||||
|       return results.map(this.cleanAudiobook) | ||||
|       return results.map(this.cleanAudiobook.bind(this)) | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   cleanPodcast(data) { | ||||
|     return { | ||||
|       id: data.collectionId, | ||||
|       artistId: data.artistId, | ||||
|       title: data.collectionName, | ||||
|       artistName: data.artistName, | ||||
|       description: stripHtml(data.description || '').result, | ||||
|       releaseDate: data.releaseDate, | ||||
|       genres: data.genres || [], | ||||
|       cover: this.getCoverArtwork(data), | ||||
|       trackCount: data.trackCount, | ||||
|       feedUrl: data.feedUrl, | ||||
|       pageUrl: data.collectionViewUrl | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   searchPodcasts(term, options = {}) { | ||||
|     return this.search({ term, entity: 'podcast', media: 'podcast', ...options }).then((results) => { | ||||
|       return results.map(this.cleanPodcast.bind(this)) | ||||
|     }) | ||||
|   } | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user