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 {
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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')
 | 
			
		||||
  // 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) {
 | 
			
		||||
    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