mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Merge pull request #2491 from liaochuan/liaocl
Add Podcast Search Region
This commit is contained in:
		
						commit
						fdc1fc1b2a
					
				| @ -49,6 +49,9 @@ | ||||
|         </ui-tooltip> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div v-if="isPodcastLibrary" class="py-3"> | ||||
|       <ui-dropdown :label="$strings.LabelPodcastSearchRegion" v-model="podcastSearchRegion" :items="$podcastSearchRegionOptions" small class="max-w-52" @input="formUpdated" /> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| @ -69,7 +72,8 @@ export default { | ||||
|       skipMatchingMediaWithAsin: false, | ||||
|       skipMatchingMediaWithIsbn: false, | ||||
|       audiobooksOnly: false, | ||||
|       hideSingleBookSeries: false | ||||
|       hideSingleBookSeries: false, | ||||
|       podcastSearchRegion: 'us' | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
| @ -85,6 +89,9 @@ export default { | ||||
|     isBookLibrary() { | ||||
|       return this.mediaType === 'book' | ||||
|     }, | ||||
|     isPodcastLibrary() { | ||||
|       return this.mediaType === 'podcast' | ||||
|     }, | ||||
|     providers() { | ||||
|       if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders | ||||
|       return this.$store.state.scanners.providers | ||||
| @ -99,7 +106,8 @@ export default { | ||||
|           skipMatchingMediaWithAsin: !!this.skipMatchingMediaWithAsin, | ||||
|           skipMatchingMediaWithIsbn: !!this.skipMatchingMediaWithIsbn, | ||||
|           audiobooksOnly: !!this.audiobooksOnly, | ||||
|           hideSingleBookSeries: !!this.hideSingleBookSeries | ||||
|           hideSingleBookSeries: !!this.hideSingleBookSeries, | ||||
|           podcastSearchRegion: this.podcastSearchRegion | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
| @ -113,6 +121,7 @@ export default { | ||||
|       this.skipMatchingMediaWithIsbn = !!this.librarySettings.skipMatchingMediaWithIsbn | ||||
|       this.audiobooksOnly = !!this.librarySettings.audiobooksOnly | ||||
|       this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries | ||||
|       this.podcastSearchRegion = this.librarySettings.podcastSearchRegion || 'us' | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|  | ||||
| @ -86,6 +86,9 @@ export default { | ||||
|     }, | ||||
|     streamLibraryItem() { | ||||
|       return this.$store.state.streamLibraryItem | ||||
|     }, | ||||
|     librarySettings() { | ||||
|       return this.$store.getters['libraries/getCurrentLibrarySettings'] | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
| @ -151,7 +154,12 @@ export default { | ||||
|     async submitSearch(term) { | ||||
|       this.processing = true | ||||
|       this.termSearched = '' | ||||
|       let results = await this.$axios.$get(`/api/search/podcast?term=${encodeURIComponent(term)}`).catch((error) => { | ||||
| 
 | ||||
|       const searchParams = new URLSearchParams({ | ||||
|         term, | ||||
|         country: this.librarySettings?.podcastSearchRegion || 'us' | ||||
|       }) | ||||
|       let results = await this.$axios.$get(`/api/search/podcast?${searchParams.toString()}`).catch((error) => { | ||||
|         console.error('Search request failed', error) | ||||
|         return [] | ||||
|       }) | ||||
|  | ||||
| @ -29,6 +29,18 @@ Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => { | ||||
|   } | ||||
| }) | ||||
| 
 | ||||
| // iTunes search API uses ISO 3166 country codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
 | ||||
| const podcastSearchRegionMap = { | ||||
|   'us': { label: 'United States' }, | ||||
|   'cn': { label: '中国' } | ||||
| } | ||||
| Vue.prototype.$podcastSearchRegionOptions = Object.keys(podcastSearchRegionMap).map(code => { | ||||
|   return { | ||||
|     text: podcastSearchRegionMap[code].label, | ||||
|     value: code | ||||
|   } | ||||
| }) | ||||
| 
 | ||||
| Vue.prototype.$languageCodes = { | ||||
|   default: defaultCode, | ||||
|   current: defaultCode, | ||||
|  | ||||
| @ -396,6 +396,7 @@ | ||||
|   "LabelPlayMethod": "Metoda přehrávání", | ||||
|   "LabelPodcast": "Podcast", | ||||
|   "LabelPodcasts": "Podcasty", | ||||
|   "LabelPodcastSearchRegion": "Oblast vyhledávání podcastu", | ||||
|   "LabelPodcastType": "Typ podcastu", | ||||
|   "LabelPort": "Port", | ||||
|   "LabelPrefixesToIgnore": "Předpony, které se mají ignorovat (nerozlišují se malá a velká písmena)", | ||||
|  | ||||
| @ -396,6 +396,7 @@ | ||||
|   "LabelPlayMethod": "Afspilningsmetode", | ||||
|   "LabelPodcast": "Podcast", | ||||
|   "LabelPodcasts": "Podcasts", | ||||
|   "LabelPodcastSearchRegion": "Podcast søgeområde", | ||||
|   "LabelPodcastType": "Podcast type", | ||||
|   "LabelPort": "Port", | ||||
|   "LabelPrefixesToIgnore": "Præfikser der skal ignoreres (skal ikke skelne mellem store og små bogstaver)", | ||||
|  | ||||
| @ -396,6 +396,7 @@ | ||||
|   "LabelPlayMethod": "Abspielmethode", | ||||
|   "LabelPodcast": "Podcast", | ||||
|   "LabelPodcasts": "Podcasts", | ||||
|   "LabelPodcastSearchRegion": "Podcast-Suchregion", | ||||
|   "LabelPodcastType": "Podcast Typ", | ||||
|   "LabelPort": "Port", | ||||
|   "LabelPrefixesToIgnore": "Zu ignorierende(s) Vorwort(e) (Groß- und Kleinschreibung wird nicht berücksichtigt)", | ||||
|  | ||||
| @ -396,6 +396,7 @@ | ||||
|   "LabelPlayMethod": "Play Method", | ||||
|   "LabelPodcast": "Podcast", | ||||
|   "LabelPodcasts": "Podcasts", | ||||
|   "LabelPodcastSearchRegion": "Podcast search region", | ||||
|   "LabelPodcastType": "Podcast Type", | ||||
|   "LabelPort": "Port", | ||||
|   "LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)", | ||||
|  | ||||
| @ -396,6 +396,7 @@ | ||||
|   "LabelPlayMethod": "Método de Reproducción", | ||||
|   "LabelPodcast": "Podcast", | ||||
|   "LabelPodcasts": "Podcasts", | ||||
|   "LabelPodcastSearchRegion": "Región de búsqueda de podcasts", | ||||
|   "LabelPodcastType": "Tipo Podcast", | ||||
|   "LabelPort": "Puerto", | ||||
|   "LabelPrefixesToIgnore": "Prefijos para Ignorar (no distingue entre mayúsculas y minúsculas.)", | ||||
|  | ||||
| @ -396,6 +396,7 @@ | ||||
|   "LabelPlayMethod": "Méthode d’écoute", | ||||
|   "LabelPodcast": "Podcast", | ||||
|   "LabelPodcasts": "Podcasts", | ||||
|   "LabelPodcastSearchRegion": "Région de recherche de podcasts", | ||||
|   "LabelPodcastType": "Type de Podcast", | ||||
|   "LabelPort": "Port", | ||||
|   "LabelPrefixesToIgnore": "Préfixes à Ignorer (Insensible à la Casse)", | ||||
|  | ||||
| @ -396,6 +396,7 @@ | ||||
|   "LabelPlayMethod": "Play Method", | ||||
|   "LabelPodcast": "Podcast", | ||||
|   "LabelPodcasts": "Podcasts", | ||||
|   "LabelPodcastSearchRegion": "પોડકાસ્ટ શોધ પ્રદેશ", | ||||
|   "LabelPodcastType": "Podcast Type", | ||||
|   "LabelPort": "Port", | ||||
|   "LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)", | ||||
|  | ||||
| @ -396,6 +396,7 @@ | ||||
|   "LabelPlayMethod": "Play Method", | ||||
|   "LabelPodcast": "Podcast", | ||||
|   "LabelPodcasts": "Podcasts", | ||||
|   "LabelPodcastSearchRegion": "पॉडकास्ट खोज क्षेत्र", | ||||
|   "LabelPodcastType": "Podcast Type", | ||||
|   "LabelPort": "Port", | ||||
|   "LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)", | ||||
|  | ||||
| @ -396,6 +396,7 @@ | ||||
|   "LabelPlayMethod": "Vrsta reprodukcije", | ||||
|   "LabelPodcast": "Podcast", | ||||
|   "LabelPodcasts": "Podcasts", | ||||
|   "LabelPodcastSearchRegion": "Područje pretrage podcasta", | ||||
|   "LabelPodcastType": "Podcast Type", | ||||
|   "LabelPort": "Port", | ||||
|   "LabelPrefixesToIgnore": "Prefiksi za ignorirati (mala i velika slova nisu bitna)", | ||||
|  | ||||
| @ -396,6 +396,7 @@ | ||||
|   "LabelPlayMethod": "Metodo di riproduzione", | ||||
|   "LabelPodcast": "Podcast", | ||||
|   "LabelPodcasts": "Podcasts", | ||||
|   "LabelPodcastSearchRegion": "Area di ricerca podcast", | ||||
|   "LabelPodcastType": "Tipo di Podcast", | ||||
|   "LabelPort": "Port", | ||||
|   "LabelPrefixesToIgnore": "Suffissi da ignorare (specificando maiuscole e minuscole)", | ||||
|  | ||||
| @ -396,6 +396,7 @@ | ||||
|   "LabelPlayMethod": "Grojimo metodas", | ||||
|   "LabelPodcast": "Tinklalaidė", | ||||
|   "LabelPodcasts": "Tinklalaidės", | ||||
|   "LabelPodcastSearchRegion": "Podcast paieškos regionas", | ||||
|   "LabelPodcastType": "Tinklalaidės tipas", | ||||
|   "LabelPort": "Prievadas", | ||||
|   "LabelPrefixesToIgnore": "Ignoruojami priešdėliai (didžiosios/mažosios nesvarbu)", | ||||
|  | ||||
| @ -396,6 +396,7 @@ | ||||
|   "LabelPlayMethod": "Afspeelwijze", | ||||
|   "LabelPodcast": "Podcast", | ||||
|   "LabelPodcasts": "Podcasts", | ||||
|   "LabelPodcastSearchRegion": "Podcast zoekregio", | ||||
|   "LabelPodcastType": "Podcasttype", | ||||
|   "LabelPort": "Poort", | ||||
|   "LabelPrefixesToIgnore": "Te negeren voorzetsels (ongeacht hoofdlettergebruik)", | ||||
|  | ||||
| @ -396,6 +396,7 @@ | ||||
|   "LabelPlayMethod": "Avspillingsmetode", | ||||
|   "LabelPodcast": "Podcast", | ||||
|   "LabelPodcasts": "Podcaster", | ||||
|   "LabelPodcastSearchRegion": "Podcast-søkeområde", | ||||
|   "LabelPodcastType": "Podcast type", | ||||
|   "LabelPort": "Port", | ||||
|   "LabelPrefixesToIgnore": "Prefiks som skal ignoreres (skiller ikke mellom store og små bokstaver)", | ||||
|  | ||||
| @ -396,6 +396,7 @@ | ||||
|   "LabelPlayMethod": "Metoda odtwarzania", | ||||
|   "LabelPodcast": "Podcast", | ||||
|   "LabelPodcasts": "Podcasty", | ||||
|   "LabelPodcastSearchRegion": "Obszar wyszukiwania podcastów", | ||||
|   "LabelPodcastType": "Podcast Type", | ||||
|   "LabelPort": "Port", | ||||
|   "LabelPrefixesToIgnore": "Ignorowane prefiksy (wielkość liter nie ma znaczenia)", | ||||
|  | ||||
| @ -40,8 +40,8 @@ | ||||
|   "ButtonLookup": "Procurar", | ||||
|   "ButtonManageTracks": "Gerenciar Faixas", | ||||
|   "ButtonMapChapterTitles": "Designar Títulos de Capítulos", | ||||
|   "ButtonitensAllAuthors": "Consultar Todos os Autores", | ||||
|   "ButtonitensBooks": "Consultar Livros", | ||||
|   "ButtonMatchAllAuthors": "Consultar Todos os Autores", | ||||
|   "ButtonMatchBooks": "Consultar Livros", | ||||
|   "ButtonNevermind": "Cancelar", | ||||
|   "ButtonNextChapter": "Próximo Capítulo", | ||||
|   "ButtonOk": "Ok", | ||||
| @ -57,7 +57,7 @@ | ||||
|   "ButtonPurgeMediaProgress": "Apagar o Progresso nas Mídias", | ||||
|   "ButtonQueueAddItem": "Adicionar à Lista", | ||||
|   "ButtonQueueRemoveItem": "Remover da Lista", | ||||
|   "ButtonQuickitens": "Consulta rápida", | ||||
|   "ButtonQuickMatch": "Consulta rápida", | ||||
|   "ButtonRead": "Ler", | ||||
|   "ButtonRemove": "Remover", | ||||
|   "ButtonRemoveAll": "Remover Todos", | ||||
| @ -133,9 +133,9 @@ | ||||
|   "HeaderLogin": "Login", | ||||
|   "HeaderLogs": "Logs", | ||||
|   "HeaderManageGenres": "Gerenciar Gêneros", | ||||
|   "HeaderManageetiquetas": "Gerenciar Etiquetas", | ||||
|   "HeaderManageTags": "Gerenciar Etiquetas", | ||||
|   "HeaderMapDetails": "Designar Detalhes", | ||||
|   "Headeritens": "Consultar", | ||||
|   "HeaderMatch": "Consultar", | ||||
|   "HeaderMetadataOrderOfPrecedence": "Ordem de Prioridade dos Metadados", | ||||
|   "HeaderMetadataToEmbed": "Metadados a Serem Incluídos", | ||||
|   "HeaderNewAccount": "Nova Conta", | ||||
| @ -396,6 +396,7 @@ | ||||
|   "LabelPlayMethod": "Método de Reprodução", | ||||
|   "LabelPodcast": "Podcast", | ||||
|   "LabelPodcasts": "Podcasts", | ||||
|   "LabelPodcastSearchRegion": "Podcast search region", | ||||
|   "LabelPodcastType": "Tipo de Podcast", | ||||
|   "LabelPort": "Porta", | ||||
|   "LabelPrefixesToIgnore": "Prefixos para Ignorar (sem distinção entre maiúsculas e minúsculas)", | ||||
| @ -764,4 +765,4 @@ | ||||
|   "ToastSocketFailedToConnect": "Falha na conexão do socket", | ||||
|   "ToastUserDeleteFailed": "Falha ao apagar usuário", | ||||
|   "ToastUserDeleteSuccess": "Usuário apagado" | ||||
| } | ||||
| } | ||||
| @ -396,6 +396,7 @@ | ||||
|   "LabelPlayMethod": "Метод воспроизведения", | ||||
|   "LabelPodcast": "Подкаст", | ||||
|   "LabelPodcasts": "Подкасты", | ||||
|   "LabelPodcastSearchRegion": "Регион поиска подкастов", | ||||
|   "LabelPodcastType": "Тип подкаста", | ||||
|   "LabelPort": "Порт", | ||||
|   "LabelPrefixesToIgnore": "Игнорируемые префиксы (без учета регистра)", | ||||
|  | ||||
| @ -396,6 +396,7 @@ | ||||
|   "LabelPlayMethod": "Spelläge", | ||||
|   "LabelPodcast": "Podcast", | ||||
|   "LabelPodcasts": "Podcasts", | ||||
|   "LabelPodcastSearchRegion": "Podcast-sökområde", | ||||
|   "LabelPodcastType": "Podcasttyp", | ||||
|   "LabelPort": "Port", | ||||
|   "LabelPrefixesToIgnore": "Prefix att ignorera (skiftlägesokänsligt)", | ||||
|  | ||||
| @ -396,6 +396,7 @@ | ||||
|   "LabelPlayMethod": "播放方法", | ||||
|   "LabelPodcast": "播客", | ||||
|   "LabelPodcasts": "播客", | ||||
|   "LabelPodcastSearchRegion": "播客搜索地区", | ||||
|   "LabelPodcastType": "播客类型", | ||||
|   "LabelPort": "端口", | ||||
|   "LabelPrefixesToIgnore": "忽略的前缀 (不区分大小写)", | ||||
|  | ||||
| @ -43,12 +43,15 @@ class SearchController { | ||||
|    */ | ||||
|   async findPodcasts(req, res) { | ||||
|     const term = req.query.term | ||||
|     const country = req.query.country || 'us' | ||||
|     if (!term) { | ||||
|       Logger.error('[SearchController] Invalid request query param "term" is required') | ||||
|       return res.status(400).send('Invalid request query param "term" is required') | ||||
|     } | ||||
| 
 | ||||
|     const results = await PodcastFinder.search(term) | ||||
|     const results = await PodcastFinder.search(term, { | ||||
|       country | ||||
|     }) | ||||
|     res.json(results) | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -6,10 +6,16 @@ class PodcastFinder { | ||||
|     this.iTunesApi = new iTunes() | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    *  | ||||
|    * @param {string} term  | ||||
|    * @param {{country:string}} options  | ||||
|    * @returns {Promise<import('../providers/iTunes').iTunesPodcastSearchResult[]>} | ||||
|    */ | ||||
|   async search(term, options = {}) { | ||||
|     if (!term) return null | ||||
|     Logger.debug(`[iTunes] Searching for podcast with term "${term}"`) | ||||
|     var results = await this.iTunesApi.searchPodcasts(term, options) | ||||
|     const results = await this.iTunesApi.searchPodcasts(term, options) | ||||
|     Logger.debug(`[iTunes] Podcast search for "${term}" returned ${results.length} results`) | ||||
|     return results | ||||
|   } | ||||
|  | ||||
| @ -10,6 +10,7 @@ class LibrarySettings { | ||||
|     this.audiobooksOnly = false | ||||
|     this.hideSingleBookSeries = false // Do not show series that only have 1 book 
 | ||||
|     this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'] | ||||
|     this.podcastSearchRegion = 'us' | ||||
| 
 | ||||
|     if (settings) { | ||||
|       this.construct(settings) | ||||
| @ -30,6 +31,7 @@ class LibrarySettings { | ||||
|       // Added in v2.4.5
 | ||||
|       this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'] | ||||
|     } | ||||
|     this.podcastSearchRegion = settings.podcastSearchRegion || 'us' | ||||
|   } | ||||
| 
 | ||||
|   toJSON() { | ||||
| @ -41,7 +43,8 @@ class LibrarySettings { | ||||
|       autoScanCronExpression: this.autoScanCronExpression, | ||||
|       audiobooksOnly: this.audiobooksOnly, | ||||
|       hideSingleBookSeries: this.hideSingleBookSeries, | ||||
|       metadataPrecedence: [...this.metadataPrecedence] | ||||
|       metadataPrecedence: [...this.metadataPrecedence], | ||||
|       podcastSearchRegion: this.podcastSearchRegion | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -2,16 +2,46 @@ const axios = require('axios') | ||||
| const Logger = require('../Logger') | ||||
| const htmlSanitizer = require('../utils/htmlSanitizer') | ||||
| 
 | ||||
| /** | ||||
|  * @typedef iTunesSearchParams | ||||
|  * @property {string} term | ||||
|  * @property {string} country | ||||
|  * @property {string} media | ||||
|  * @property {string} entity | ||||
|  * @property {number} limit | ||||
|  */ | ||||
| 
 | ||||
| /** | ||||
|  * @typedef iTunesPodcastSearchResult | ||||
|  * @property {string} id | ||||
|  * @property {string} artistId | ||||
|  * @property {string} title | ||||
|  * @property {string} artistName | ||||
|  * @property {string} description | ||||
|  * @property {string} descriptionPlain | ||||
|  * @property {string} releaseDate | ||||
|  * @property {string[]} genres | ||||
|  * @property {string} cover | ||||
|  * @property {string} feedUrl | ||||
|  * @property {string} pageUrl | ||||
|  * @property {boolean} explicit | ||||
|  */ | ||||
| 
 | ||||
| class iTunes { | ||||
|   constructor() { } | ||||
| 
 | ||||
|   // https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/iTuneSearchAPI/Searching.html
 | ||||
|   /** | ||||
|    * @see https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/iTuneSearchAPI/Searching.html
 | ||||
|    *  | ||||
|    * @param {iTunesSearchParams} options  | ||||
|    * @returns {Promise<Object[]>} | ||||
|    */ | ||||
|   search(options) { | ||||
|     if (!options.term) { | ||||
|       Logger.error('[iTunes] Invalid search options - no term') | ||||
|       return [] | ||||
|     } | ||||
|     var query = { | ||||
|     const query = { | ||||
|       term: options.term, | ||||
|       media: options.media, | ||||
|       entity: options.entity, | ||||
| @ -82,6 +112,11 @@ class iTunes { | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    *  | ||||
|    * @param {Object} data  | ||||
|    * @returns {iTunesPodcastSearchResult} | ||||
|    */ | ||||
|   cleanPodcast(data) { | ||||
|     return { | ||||
|       id: data.collectionId, | ||||
| @ -100,6 +135,12 @@ class iTunes { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    *  | ||||
|    * @param {string} term  | ||||
|    * @param {{country:string}} options  | ||||
|    * @returns {Promise<iTunesPodcastSearchResult[]>} | ||||
|    */ | ||||
|   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