mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Add: author object, author search api, author images #187
This commit is contained in:
		
							parent
							
								
									979fb70c31
								
							
						
					
					
						commit
						5308801540
					
				
							
								
								
									
										56
									
								
								client/components/cards/PersonCard.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								client/components/cards/PersonCard.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,56 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="h-24 flex"> | ||||||
|  |     <div class="w-32"> | ||||||
|  |       <img :src="imgSrc" class="w-full object-cover" /> | ||||||
|  |     </div> | ||||||
|  |     <div class="flex-grow"> | ||||||
|  |       <p>{{ name }}</p> | ||||||
|  |       <p class="text-sm text-gray-300">{{ description }}</p> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   props: { | ||||||
|  |     person: { | ||||||
|  |       type: Object, | ||||||
|  |       default: () => {} | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       placeholder: '/Logo.png' | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     userToken() { | ||||||
|  |       return this.$store.getters['user/getToken'] | ||||||
|  |     }, | ||||||
|  |     _person() { | ||||||
|  |       return this.person || {} | ||||||
|  |     }, | ||||||
|  |     name() { | ||||||
|  |       return this._person.name || '' | ||||||
|  |     }, | ||||||
|  |     image() { | ||||||
|  |       return this._person.image || null | ||||||
|  |     }, | ||||||
|  |     description() { | ||||||
|  |       return this._person.description | ||||||
|  |     }, | ||||||
|  |     lastUpdate() { | ||||||
|  |       return this._person.lastUpdate | ||||||
|  |     }, | ||||||
|  |     imgSrc() { | ||||||
|  |       if (!this.image) return this.placeholder | ||||||
|  |       var encodedImg = this.image.replace(/%/g, '%25').replace(/#/g, '%23') | ||||||
|  | 
 | ||||||
|  |       var url = new URL(encodedImg, document.baseURI) | ||||||
|  |       return url.href + `?token=${this.userToken}&ts=${this.lastUpdate}` | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: {}, | ||||||
|  |   mounted() {} | ||||||
|  | } | ||||||
|  | </script> | ||||||
							
								
								
									
										73
									
								
								client/components/cards/SearchAuthorCard.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								client/components/cards/SearchAuthorCard.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,73 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <form @submit.prevent="submitSearch"> | ||||||
|  |       <div class="flex items-center justify-start -mx-1 h-20"> | ||||||
|  |         <!-- <div class="w-40 px-1"> | ||||||
|  |           <ui-dropdown v-model="provider" :items="providers" label="Provider" small /> | ||||||
|  |         </div> --> | ||||||
|  |         <div class="flex-grow px-1"> | ||||||
|  |           <ui-text-input-with-label v-model="searchAuthor" label="Author" /> | ||||||
|  |         </div> | ||||||
|  |         <ui-btn class="mt-5 ml-1" type="submit">Search</ui-btn> | ||||||
|  |       </div> | ||||||
|  |     </form> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   props: { | ||||||
|  |     authorName: String | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       searchAuthor: null, | ||||||
|  |       lastSearch: null, | ||||||
|  |       isProcessing: false, | ||||||
|  |       provider: 'audnexus', | ||||||
|  |       providers: [ | ||||||
|  |         { | ||||||
|  |           text: 'Audnexus', | ||||||
|  |           value: 'audnexus' | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   watch: { | ||||||
|  |     authorName: { | ||||||
|  |       immediate: true, | ||||||
|  |       handler(newVal) { | ||||||
|  |         this.searchAuthor = newVal | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: {}, | ||||||
|  |   methods: { | ||||||
|  |     getSearchQuery() { | ||||||
|  |       return `q=${this.searchAuthor}` | ||||||
|  |     }, | ||||||
|  |     submitSearch() { | ||||||
|  |       if (!this.searchAuthor) { | ||||||
|  |         this.$toast.warning('Author name is required') | ||||||
|  |         return | ||||||
|  |       } | ||||||
|  |       this.runSearch() | ||||||
|  |     }, | ||||||
|  |     async runSearch() { | ||||||
|  |       var searchQuery = this.getSearchQuery() | ||||||
|  |       if (this.lastSearch === searchQuery) return | ||||||
|  |       this.isProcessing = true | ||||||
|  |       this.lastSearch = searchQuery | ||||||
|  |       var result = await this.$axios.$get(`/api/authors/search?${searchQuery}`).catch((error) => { | ||||||
|  |         console.error('Failed', error) | ||||||
|  |         return [] | ||||||
|  |       }) | ||||||
|  |       this.isProcessing = false | ||||||
|  |       if (result) { | ||||||
|  |         this.$emit('match', result) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   mounted() {} | ||||||
|  | } | ||||||
|  | </script> | ||||||
| @ -44,11 +44,11 @@ export default { | |||||||
|           title: 'Cover', |           title: 'Cover', | ||||||
|           component: 'modals-edit-tabs-cover' |           component: 'modals-edit-tabs-cover' | ||||||
|         }, |         }, | ||||||
|         { |         // { | ||||||
|           id: 'tracks', |         //   id: 'tracks', | ||||||
|           title: 'Tracks', |         //   title: 'Tracks', | ||||||
|           component: 'modals-edit-tabs-tracks' |         //   component: 'modals-edit-tabs-tracks' | ||||||
|         }, |         // }, | ||||||
|         { |         { | ||||||
|           id: 'chapters', |           id: 'chapters', | ||||||
|           title: 'Chapters', |           title: 'Chapters', | ||||||
| @ -69,6 +69,11 @@ export default { | |||||||
|           title: 'Match', |           title: 'Match', | ||||||
|           component: 'modals-edit-tabs-match' |           component: 'modals-edit-tabs-match' | ||||||
|         } |         } | ||||||
|  |         // { | ||||||
|  |         //   id: 'authors', | ||||||
|  |         //   title: 'Authors', | ||||||
|  |         //   component: 'modals-edit-tabs-authors' | ||||||
|  |         // } | ||||||
|       ] |       ] | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
| @ -130,8 +135,8 @@ export default { | |||||||
|       if (!this.userCanUpdate && !this.userCanDownload) return [] |       if (!this.userCanUpdate && !this.userCanDownload) return [] | ||||||
|       return this.tabs.filter((tab) => { |       return this.tabs.filter((tab) => { | ||||||
|         if (tab.id === 'download' && this.isMissing) return false |         if (tab.id === 'download' && this.isMissing) return false | ||||||
|         if ((tab.id === 'download' || tab.id === 'tracks') && this.userCanDownload) return true |         if ((tab.id === 'download' || tab.id === 'files' || tab.id === 'authors') && this.userCanDownload) return true | ||||||
|         if (tab.id !== 'download' && tab.id !== 'tracks' && this.userCanUpdate) return true |         if (tab.id !== 'download' && tab.id !== 'files' && tab.id !== 'authors' && this.userCanUpdate) return true | ||||||
|         if (tab.id === 'match' && this.userCanUpdate && this.showExperimentalFeatures) return true |         if (tab.id === 'match' && this.userCanUpdate && this.showExperimentalFeatures) return true | ||||||
|         return false |         return false | ||||||
|       }) |       }) | ||||||
|  | |||||||
							
								
								
									
										201
									
								
								client/components/modals/edit-tabs/Authors.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								client/components/modals/edit-tabs/Authors.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,201 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="w-full h-full overflow-hidden px-4 py-6 relative"> | ||||||
|  |     <template v-for="(authorName, index) in searchAuthors"> | ||||||
|  |       <cards-search-author-card :key="index" :author-name="authorName" @match="setSelectedMatch" /> | ||||||
|  |     </template> | ||||||
|  | 
 | ||||||
|  |     <div v-show="processing" class="flex h-full items-center justify-center"> | ||||||
|  |       <p>Loading...</p> | ||||||
|  |     </div> | ||||||
|  |     <div v-if="selectedMatch" class="absolute top-0 left-0 w-full bg-bg h-full p-8 max-h-full overflow-y-auto overflow-x-hidden"> | ||||||
|  |       <div class="flex mb-2"> | ||||||
|  |         <div class="w-8 h-8 rounded-full hover:bg-white hover:bg-opacity-10 flex items-center justify-center cursor-pointer" @click="selectedMatch = null"> | ||||||
|  |           <span class="material-icons text-3xl">arrow_back</span> | ||||||
|  |         </div> | ||||||
|  |         <p class="text-xl pl-3">Update Author Details</p> | ||||||
|  |       </div> | ||||||
|  |       <form @submit.prevent="submitMatchUpdate"> | ||||||
|  |         <div v-if="selectedMatch.image" class="flex items-center py-2"> | ||||||
|  |           <ui-checkbox v-model="selectedMatchUsage.image" /> | ||||||
|  |           <img :src="selectedMatch.image" class="w-24 object-contain ml-4" /> | ||||||
|  |           <ui-text-input-with-label v-model="selectedMatch.image" :disabled="!selectedMatchUsage.image" label="Image" class="flex-grow ml-4" /> | ||||||
|  |         </div> | ||||||
|  |         <div v-if="selectedMatch.name" class="flex items-center py-2"> | ||||||
|  |           <ui-checkbox v-model="selectedMatchUsage.name" /> | ||||||
|  |           <ui-text-input-with-label v-model="selectedMatch.name" :disabled="!selectedMatchUsage.name" label="Name" class="flex-grow ml-4" /> | ||||||
|  |         </div> | ||||||
|  |         <div v-if="selectedMatch.description" class="flex items-center py-2"> | ||||||
|  |           <ui-checkbox v-model="selectedMatchUsage.description" /> | ||||||
|  |           <ui-textarea-with-label v-model="selectedMatch.description" :rows="3" :disabled="!selectedMatchUsage.description" label="Description" class="flex-grow ml-4" /> | ||||||
|  |         </div> | ||||||
|  |         <div class="flex items-center justify-end py-2"> | ||||||
|  |           <ui-btn color="success" type="submit">Update</ui-btn> | ||||||
|  |         </div> | ||||||
|  |       </form> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   props: { | ||||||
|  |     processing: Boolean, | ||||||
|  |     audiobook: { | ||||||
|  |       type: Object, | ||||||
|  |       default: () => {} | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       searchAuthors: [], | ||||||
|  |       audiobookId: null, | ||||||
|  |       searchAuthor: null, | ||||||
|  |       lastSearch: null, | ||||||
|  |       hasSearched: false, | ||||||
|  |       selectedMatch: null, | ||||||
|  | 
 | ||||||
|  |       selectedMatchUsage: { | ||||||
|  |         image: true, | ||||||
|  |         name: true, | ||||||
|  |         description: true | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   watch: { | ||||||
|  |     audiobook: { | ||||||
|  |       immediate: true, | ||||||
|  |       handler(newVal) { | ||||||
|  |         if (newVal) this.init() | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     isProcessing: { | ||||||
|  |       get() { | ||||||
|  |         return this.processing | ||||||
|  |       }, | ||||||
|  |       set(val) { | ||||||
|  |         this.$emit('update:processing', val) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     // getSearchQuery() { | ||||||
|  |     //   return `q=${this.searchAuthor}` | ||||||
|  |     // }, | ||||||
|  |     // submitSearch() { | ||||||
|  |     //   if (!this.searchTitle) { | ||||||
|  |     //     this.$toast.warning('Search title is required') | ||||||
|  |     //     return | ||||||
|  |     //   } | ||||||
|  |     //   this.runSearch() | ||||||
|  |     // }, | ||||||
|  |     // async runSearch() { | ||||||
|  |     //   var searchQuery = this.getSearchQuery() | ||||||
|  |     //   if (this.lastSearch === searchQuery) return | ||||||
|  |     //   this.selectedMatch = null | ||||||
|  |     //   this.isProcessing = true | ||||||
|  |     //   this.lastSearch = searchQuery | ||||||
|  |     //   var result = await this.$axios.$get(`/api/authors/search?${searchQuery}`).catch((error) => { | ||||||
|  |     //     console.error('Failed', error) | ||||||
|  |     //     return [] | ||||||
|  |     //   }) | ||||||
|  |     //   if (result) { | ||||||
|  |     //     this.selectedMatch = result | ||||||
|  |     //   } | ||||||
|  |     //   this.isProcessing = false | ||||||
|  |     //   this.hasSearched = true | ||||||
|  |     // }, | ||||||
|  |     init() { | ||||||
|  |       this.selectedMatch = null | ||||||
|  |       // this.selectedMatchUsage = { | ||||||
|  |       //   title: true, | ||||||
|  |       //   subtitle: true, | ||||||
|  |       //   cover: true, | ||||||
|  |       //   author: true, | ||||||
|  |       //   description: true, | ||||||
|  |       //   isbn: true, | ||||||
|  |       //   publisher: true, | ||||||
|  |       //   publishYear: true | ||||||
|  |       // } | ||||||
|  | 
 | ||||||
|  |       if (this.audiobook.id !== this.audiobookId) { | ||||||
|  |         this.selectedMatch = null | ||||||
|  |         this.hasSearched = false | ||||||
|  |         this.audiobookId = this.audiobook.id | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (!this.audiobook.book || !this.audiobook.book.authorFL) { | ||||||
|  |         this.searchAuthors = [] | ||||||
|  |         return | ||||||
|  |       } | ||||||
|  |       this.searchAuthors = (this.audiobook.book.authorFL || '').split(', ') | ||||||
|  |     }, | ||||||
|  |     selectMatch(match) { | ||||||
|  |       this.selectedMatch = match | ||||||
|  |     }, | ||||||
|  |     buildMatchUpdatePayload() { | ||||||
|  |       var updatePayload = {} | ||||||
|  |       for (const key in this.selectedMatchUsage) { | ||||||
|  |         if (this.selectedMatchUsage[key] && this.selectedMatch[key]) { | ||||||
|  |           updatePayload[key] = this.selectedMatch[key] | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       return updatePayload | ||||||
|  |     }, | ||||||
|  |     async submitMatchUpdate() { | ||||||
|  |       var updatePayload = this.buildMatchUpdatePayload() | ||||||
|  |       if (!Object.keys(updatePayload).length) { | ||||||
|  |         return | ||||||
|  |       } | ||||||
|  |       this.isProcessing = true | ||||||
|  | 
 | ||||||
|  |       if (updatePayload.cover) { | ||||||
|  |         var coverPayload = { | ||||||
|  |           url: updatePayload.cover | ||||||
|  |         } | ||||||
|  |         var success = await this.$axios.$post(`/api/audiobook/${this.audiobook.id}/cover`, coverPayload).catch((error) => { | ||||||
|  |           console.error('Failed to update', error) | ||||||
|  |           return false | ||||||
|  |         }) | ||||||
|  |         if (success) { | ||||||
|  |           this.$toast.success('Book Cover Updated') | ||||||
|  |         } else { | ||||||
|  |           this.$toast.error('Book Cover Failed to Update') | ||||||
|  |         } | ||||||
|  |         console.log('Updated cover') | ||||||
|  |         delete updatePayload.cover | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (Object.keys(updatePayload).length) { | ||||||
|  |         var bookUpdatePayload = { | ||||||
|  |           book: updatePayload | ||||||
|  |         } | ||||||
|  |         var success = await this.$axios.$patch(`/api/audiobook/${this.audiobook.id}`, bookUpdatePayload).catch((error) => { | ||||||
|  |           console.error('Failed to update', error) | ||||||
|  |           return false | ||||||
|  |         }) | ||||||
|  |         if (success) { | ||||||
|  |           this.$toast.success('Book Details Updated') | ||||||
|  |           this.selectedMatch = null | ||||||
|  |           this.$emit('selectTab', 'details') | ||||||
|  |         } else { | ||||||
|  |           this.$toast.error('Book Details Failed to Update') | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         this.selectedMatch = null | ||||||
|  |       } | ||||||
|  |       this.isProcessing = false | ||||||
|  |     }, | ||||||
|  |     setSelectedMatch(authorMatchObj) { | ||||||
|  |       this.selectedMatch = authorMatchObj | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  | .matchListWrapper { | ||||||
|  |   height: calc(100% - 80px); | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @ -1,5 +1,48 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6"> |   <div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6"> | ||||||
|  |     <div class="mb-4"> | ||||||
|  |       <template v-if="hasTracks"> | ||||||
|  |         <div class="w-full bg-primary px-4 py-2 flex items-center"> | ||||||
|  |           <p class="pr-4">Audio Tracks</p> | ||||||
|  |           <div class="h-7 w-7 rounded-full bg-white bg-opacity-10 flex items-center justify-center"> | ||||||
|  |             <span class="text-sm font-mono">{{ tracks.length }}</span> | ||||||
|  |           </div> | ||||||
|  |           <div class="flex-grow" /> | ||||||
|  |           <ui-btn small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">Full Path</ui-btn> | ||||||
|  |           <nuxt-link v-if="userCanUpdate" :to="`/audiobook/${audiobook.id}/edit`"> | ||||||
|  |             <ui-btn small color="primary">Manage Tracks</ui-btn> | ||||||
|  |           </nuxt-link> | ||||||
|  |         </div> | ||||||
|  |         <table class="text-sm tracksTable"> | ||||||
|  |           <tr class="font-book"> | ||||||
|  |             <th>#</th> | ||||||
|  |             <th class="text-left">Filename</th> | ||||||
|  |             <th class="text-left">Size</th> | ||||||
|  |             <th class="text-left">Duration</th> | ||||||
|  |             <th v-if="showDownload" class="text-center">Download</th> | ||||||
|  |           </tr> | ||||||
|  |           <template v-for="track in tracksCleaned"> | ||||||
|  |             <tr :key="track.index"> | ||||||
|  |               <td class="text-center"> | ||||||
|  |                 <p>{{ track.index }}</p> | ||||||
|  |               </td> | ||||||
|  |               <td class="font-sans">{{ showFullPath ? track.fullPath : track.filename }}</td> | ||||||
|  |               <td class="font-mono"> | ||||||
|  |                 {{ $bytesPretty(track.size) }} | ||||||
|  |               </td> | ||||||
|  |               <td class="font-mono"> | ||||||
|  |                 {{ $secondsToTimestamp(track.duration) }} | ||||||
|  |               </td> | ||||||
|  |               <td v-if="showDownload" class="font-mono text-center"> | ||||||
|  |                 <a :href="`/s/book/${audiobook.id}/${track.relativePath}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a> | ||||||
|  |               </td> | ||||||
|  |             </tr> | ||||||
|  |           </template> | ||||||
|  |         </table> | ||||||
|  |       </template> | ||||||
|  |       <div v-else class="flex my-4 text-center justify-center text-xl">No Audio Tracks</div> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|     <tables-all-files-table :audiobook="audiobook" /> |     <tables-all-files-table :audiobook="audiobook" /> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| @ -13,9 +56,60 @@ export default { | |||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return {} |     return { | ||||||
|  |       tracks: null, | ||||||
|  |       showFullPath: false | ||||||
|  |     } | ||||||
|   }, |   }, | ||||||
|   computed: {}, |   watch: { | ||||||
|   methods: {} |     audiobook: { | ||||||
|  |       immediate: true, | ||||||
|  |       handler(newVal) { | ||||||
|  |         if (newVal) this.init() | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     audiobookPath() { | ||||||
|  |       return this.audiobook.path | ||||||
|  |     }, | ||||||
|  |     tracksCleaned() { | ||||||
|  |       return this.tracks.map((track) => { | ||||||
|  |         var trackPath = track.path.replace(/\\/g, '/') | ||||||
|  |         var audiobookPath = this.audiobookPath.replace(/\\/g, '/') | ||||||
|  | 
 | ||||||
|  |         return { | ||||||
|  |           ...track, | ||||||
|  |           relativePath: trackPath | ||||||
|  |             .replace(audiobookPath + '/', '') | ||||||
|  |             .replace(/%/g, '%25') | ||||||
|  |             .replace(/#/g, '%23') | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |     }, | ||||||
|  |     userToken() { | ||||||
|  |       return this.$store.getters['user/getToken'] | ||||||
|  |     }, | ||||||
|  |     userCanUpdate() { | ||||||
|  |       return this.$store.getters['user/getUserCanUpdate'] | ||||||
|  |     }, | ||||||
|  |     userCanDownload() { | ||||||
|  |       return this.$store.getters['user/getUserCanDownload'] | ||||||
|  |     }, | ||||||
|  |     isMissing() { | ||||||
|  |       return this.audiobook.isMissing | ||||||
|  |     }, | ||||||
|  |     showDownload() { | ||||||
|  |       return this.userCanDownload && !this.isMissing | ||||||
|  |     }, | ||||||
|  |     hasTracks() { | ||||||
|  |       return this.audiobook.tracks.length | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     init() { | ||||||
|  |       this.tracks = this.audiobook.tracks | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
| </script> | </script> | ||||||
| @ -1,7 +1,7 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="w-full my-2"> |   <div class="w-full my-2"> | ||||||
|     <div class="w-full bg-primary px-6 py-2 flex items-center cursor-pointer"> |     <div class="w-full bg-primary px-4 py-2 flex items-center cursor-pointer"> | ||||||
|       <p class="pr-4">Files</p> |       <p class="pr-4">All Files</p> | ||||||
|       <span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ allFiles.length }}</span> |       <span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ allFiles.length }}</span> | ||||||
|       <div class="flex-grow" /> |       <div class="flex-grow" /> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ const { isObject, getId } = require('./utils/index') | |||||||
| const audioFileScanner = require('./utils/audioFileScanner') | const audioFileScanner = require('./utils/audioFileScanner') | ||||||
| 
 | 
 | ||||||
| const BookFinder = require('./BookFinder') | const BookFinder = require('./BookFinder') | ||||||
|  | const AuthorController = require('./AuthorController') | ||||||
| 
 | 
 | ||||||
| const Library = require('./objects/Library') | const Library = require('./objects/Library') | ||||||
| const User = require('./objects/User') | const User = require('./objects/User') | ||||||
| @ -29,6 +30,7 @@ class ApiController { | |||||||
|     this.MetadataPath = MetadataPath |     this.MetadataPath = MetadataPath | ||||||
| 
 | 
 | ||||||
|     this.bookFinder = new BookFinder() |     this.bookFinder = new BookFinder() | ||||||
|  |     this.authorController = new AuthorController(this.MetadataPath) | ||||||
| 
 | 
 | ||||||
|     this.router = express() |     this.router = express() | ||||||
|     this.init() |     this.init() | ||||||
| @ -88,6 +90,13 @@ class ApiController { | |||||||
|     this.router.patch('/collection/:id', this.updateUserCollection.bind(this)) |     this.router.patch('/collection/:id', this.updateUserCollection.bind(this)) | ||||||
|     this.router.delete('/collection/:id', this.deleteUserCollection.bind(this)) |     this.router.delete('/collection/:id', this.deleteUserCollection.bind(this)) | ||||||
| 
 | 
 | ||||||
|  |     this.router.get('/authors', this.getAuthors.bind(this)) | ||||||
|  |     this.router.get('/authors/search', this.searchAuthor.bind(this)) | ||||||
|  |     this.router.get('/authors/:id', this.getAuthor.bind(this)) | ||||||
|  |     this.router.post('/authors', this.createAuthor.bind(this)) | ||||||
|  |     this.router.patch('/authors/:id', this.updateAuthor.bind(this)) | ||||||
|  |     this.router.delete('/authors/:id', this.deleteAuthor.bind(this)) | ||||||
|  | 
 | ||||||
|     this.router.patch('/serverSettings', this.updateServerSettings.bind(this)) |     this.router.patch('/serverSettings', this.updateServerSettings.bind(this)) | ||||||
| 
 | 
 | ||||||
|     this.router.delete('/backup/:id', this.deleteBackup.bind(this)) |     this.router.delete('/backup/:id', this.deleteBackup.bind(this)) | ||||||
| @ -897,6 +906,63 @@ class ApiController { | |||||||
|     res.sendStatus(200) |     res.sendStatus(200) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   async getAuthors(req, res) { | ||||||
|  |     var authors = this.db.authors.filter(p => p.isAuthor) | ||||||
|  |     res.json(authors) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async getAuthor(req, res) { | ||||||
|  |     var author = this.db.authors.find(p => p.id === req.params.id) | ||||||
|  |     if (!author) { | ||||||
|  |       return res.status(404).send('Author not found') | ||||||
|  |     } | ||||||
|  |     res.json(author.toJSON()) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async searchAuthor(req, res) { | ||||||
|  |     var query = req.query.q | ||||||
|  |     var author = await this.authorController.findAuthorByName(query) | ||||||
|  |     res.json(author) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async createAuthor(req, res) { | ||||||
|  |     var author = await this.authorController.createAuthor(req.body) | ||||||
|  |     if (!author) { | ||||||
|  |       return res.status(500).send('Failed to create author') | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     await this.db.insertEntity('author', author) | ||||||
|  |     this.emitter('author_added', author.toJSON()) | ||||||
|  |     res.json(author) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async updateAuthor(req, res) { | ||||||
|  |     var author = this.db.authors.find(p => p.id === req.params.id) | ||||||
|  |     if (!author) { | ||||||
|  |       return res.status(404).send('Author not found') | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var wasUpdated = author.update(req.body) | ||||||
|  |     if (wasUpdated) { | ||||||
|  |       await this.db.updateEntity('author', author) | ||||||
|  |       this.emitter('author_updated', author.toJSON()) | ||||||
|  |     } | ||||||
|  |     res.json(author) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async deleteAuthor(req, res) { | ||||||
|  |     var author = this.db.authors.find(p => p.id === req.params.id) | ||||||
|  |     if (!author) { | ||||||
|  |       return res.status(404).send('Author not found') | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var authorJson = author.toJSON() | ||||||
|  | 
 | ||||||
|  |     await this.db.removeEntity('author', author.id) | ||||||
|  |     this.emitter('author_removed', authorJson) | ||||||
|  |     res.sendStatus(200) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   async updateServerSettings(req, res) { |   async updateServerSettings(req, res) { | ||||||
|     if (!req.user.isRoot) { |     if (!req.user.isRoot) { | ||||||
|       Logger.error('User other than root attempting to update server settings', req.user) |       Logger.error('User other than root attempting to update server settings', req.user) | ||||||
|  | |||||||
							
								
								
									
										110
									
								
								server/AuthorController.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								server/AuthorController.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,110 @@ | |||||||
|  | const fs = require('fs-extra') | ||||||
|  | const Logger = require('./Logger') | ||||||
|  | const Path = require('path') | ||||||
|  | const Author = require('./objects/Author') | ||||||
|  | const Audnexus = require('./providers/Audnexus') | ||||||
|  | 
 | ||||||
|  | const { downloadFile } = require('./utils/fileUtils') | ||||||
|  | 
 | ||||||
|  | class AuthorController { | ||||||
|  |   constructor(MetadataPath) { | ||||||
|  |     this.MetadataPath = MetadataPath | ||||||
|  |     this.AuthorPath = Path.join(MetadataPath, 'authors') | ||||||
|  | 
 | ||||||
|  |     this.audnexus = new Audnexus() | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async downloadImage(url, outputPath) { | ||||||
|  |     return downloadFile(url, outputPath).then(() => true).catch((error) => { | ||||||
|  |       Logger.error('[AuthorController] Failed to download author image', error) | ||||||
|  |       return null | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async findAuthorByName(name, options = {}) { | ||||||
|  |     if (!name) return null | ||||||
|  |     const maxLevenshtein = !isNaN(options.maxLevenshtein) ? Number(options.maxLevenshtein) : 2 | ||||||
|  | 
 | ||||||
|  |     var author = await this.audnexus.findAuthorByName(name, maxLevenshtein) | ||||||
|  |     if (!author || !author.name) { | ||||||
|  |       return null | ||||||
|  |     } | ||||||
|  |     return author | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async createAuthor(payload) { | ||||||
|  |     if (!payload || !payload.name) return null | ||||||
|  | 
 | ||||||
|  |     var authorDir = Path.posix.join(this.AuthorPath, payload.name) | ||||||
|  |     var relAuthorDir = Path.posix.join('/metadata', 'authors', payload.name) | ||||||
|  | 
 | ||||||
|  |     if (payload.image && payload.image.startsWith('http')) { | ||||||
|  |       await fs.ensureDir(authorDir) | ||||||
|  | 
 | ||||||
|  |       var imageExtension = payload.image.toLowerCase().split('.').pop() | ||||||
|  |       var ext = imageExtension === 'png' ? 'png' : 'jpg' | ||||||
|  |       var filename = 'photo.' + ext | ||||||
|  |       var outputPath = Path.posix.join(authorDir, filename) | ||||||
|  |       var relPath = Path.posix.join(relAuthorDir, filename) | ||||||
|  | 
 | ||||||
|  |       var success = await this.downloadImage(payload.image, outputPath) | ||||||
|  |       if (!success) { | ||||||
|  |         await fs.rmdir(authorDir).catch((error) => { | ||||||
|  |           Logger.error(`[AuthorController] Failed to remove author dir`, authorDir, error) | ||||||
|  |         }) | ||||||
|  |         payload.image = null | ||||||
|  |         payload.imageFullPath = null | ||||||
|  |       } else { | ||||||
|  |         payload.image = relPath | ||||||
|  |         payload.imageFullPath = outputPath | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       payload.image = null | ||||||
|  |       payload.imageFullPath = null | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var author = new Author() | ||||||
|  |     author.setData(payload) | ||||||
|  | 
 | ||||||
|  |     return author | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async getAuthorByName(name, options = {}) { | ||||||
|  |     var authorData = await this.findAuthorByName(name, options) | ||||||
|  |     if (!authorData) return null | ||||||
|  | 
 | ||||||
|  |     var authorDir = Path.posix.join(this.AuthorPath, authorData.name) | ||||||
|  |     var relAuthorDir = Path.posix.join('/metadata', 'authors', authorData.name) | ||||||
|  | 
 | ||||||
|  |     if (authorData.image) { | ||||||
|  |       await fs.ensureDir(authorDir) | ||||||
|  | 
 | ||||||
|  |       var imageExtension = authorData.image.toLowerCase().split('.').pop() | ||||||
|  |       var ext = imageExtension === 'png' ? 'png' : 'jpg' | ||||||
|  |       var filename = 'photo.' + ext | ||||||
|  |       var outputPath = Path.posix.join(authorDir, filename) | ||||||
|  |       var relPath = Path.posix.join(relAuthorDir, filename) | ||||||
|  | 
 | ||||||
|  |       var success = await this.downloadImage(authorData.image, outputPath) | ||||||
|  |       if (!success) { | ||||||
|  |         await fs.rmdir(authorDir).catch((error) => { | ||||||
|  |           Logger.error(`[AuthorController] Failed to remove author dir`, authorDir, error) | ||||||
|  |         }) | ||||||
|  |         authorData.image = null | ||||||
|  |         authorData.imageFullPath = null | ||||||
|  |       } else { | ||||||
|  |         authorData.image = relPath | ||||||
|  |         authorData.imageFullPath = outputPath | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       authorData.image = null | ||||||
|  |       authorData.imageFullPath = null | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var author = new Author() | ||||||
|  |     author.setData(authorData) | ||||||
|  | 
 | ||||||
|  |     return author | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | module.exports = AuthorController | ||||||
| @ -7,6 +7,7 @@ const imageType = require('image-type') | |||||||
| 
 | 
 | ||||||
| const globals = require('./utils/globals') | const globals = require('./utils/globals') | ||||||
| const { CoverDestination } = require('./utils/constants') | const { CoverDestination } = require('./utils/constants') | ||||||
|  | const { downloadFile } = require('./utils/fileUtils') | ||||||
| 
 | 
 | ||||||
| class CoverController { | class CoverController { | ||||||
|   constructor(db, MetadataPath, AudiobookPath) { |   constructor(db, MetadataPath, AudiobookPath) { | ||||||
| @ -123,28 +124,13 @@ class CoverController { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async downloadFile(url, filepath) { |  | ||||||
|     Logger.debug(`[CoverController] Starting file download to ${filepath}`) |  | ||||||
|     const writer = fs.createWriteStream(filepath) |  | ||||||
|     const response = await axios({ |  | ||||||
|       url, |  | ||||||
|       method: 'GET', |  | ||||||
|       responseType: 'stream' |  | ||||||
|     }) |  | ||||||
|     response.data.pipe(writer) |  | ||||||
|     return new Promise((resolve, reject) => { |  | ||||||
|       writer.on('finish', resolve) |  | ||||||
|       writer.on('error', reject) |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async downloadCoverFromUrl(audiobook, url) { |   async downloadCoverFromUrl(audiobook, url) { | ||||||
|     try { |     try { | ||||||
|       var { fullPath, relPath } = this.getCoverDirectory(audiobook) |       var { fullPath, relPath } = this.getCoverDirectory(audiobook) | ||||||
|       await fs.ensureDir(fullPath) |       await fs.ensureDir(fullPath) | ||||||
| 
 | 
 | ||||||
|       var temppath = Path.posix.join(fullPath, 'cover') |       var temppath = Path.posix.join(fullPath, 'cover') | ||||||
|       var success = await this.downloadFile(url, temppath).then(() => true).catch((err) => { |       var success = await downloadFile(url, temppath).then(() => true).catch((err) => { | ||||||
|         Logger.error(`[CoverController] Download image file failed for "${url}"`, err) |         Logger.error(`[CoverController] Download image file failed for "${url}"`, err) | ||||||
|         return false |         return false | ||||||
|       }) |       }) | ||||||
|  | |||||||
							
								
								
									
										13
									
								
								server/Db.js
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								server/Db.js
									
									
									
									
									
								
							| @ -8,6 +8,7 @@ const Audiobook = require('./objects/Audiobook') | |||||||
| const User = require('./objects/User') | const User = require('./objects/User') | ||||||
| const UserCollection = require('./objects/UserCollection') | const UserCollection = require('./objects/UserCollection') | ||||||
| const Library = require('./objects/Library') | const Library = require('./objects/Library') | ||||||
|  | const Author = require('./objects/Author') | ||||||
| const ServerSettings = require('./objects/ServerSettings') | const ServerSettings = require('./objects/ServerSettings') | ||||||
| 
 | 
 | ||||||
| class Db { | class Db { | ||||||
| @ -21,6 +22,7 @@ class Db { | |||||||
|     this.LibrariesPath = Path.join(ConfigPath, 'libraries') |     this.LibrariesPath = Path.join(ConfigPath, 'libraries') | ||||||
|     this.SettingsPath = Path.join(ConfigPath, 'settings') |     this.SettingsPath = Path.join(ConfigPath, 'settings') | ||||||
|     this.CollectionsPath = Path.join(ConfigPath, 'collections') |     this.CollectionsPath = Path.join(ConfigPath, 'collections') | ||||||
|  |     this.AuthorsPath = Path.join(ConfigPath, 'authors') | ||||||
| 
 | 
 | ||||||
|     this.audiobooksDb = new njodb.Database(this.AudiobooksPath) |     this.audiobooksDb = new njodb.Database(this.AudiobooksPath) | ||||||
|     this.usersDb = new njodb.Database(this.UsersPath) |     this.usersDb = new njodb.Database(this.UsersPath) | ||||||
| @ -28,6 +30,7 @@ class Db { | |||||||
|     this.librariesDb = new njodb.Database(this.LibrariesPath, { datastores: 2 }) |     this.librariesDb = new njodb.Database(this.LibrariesPath, { datastores: 2 }) | ||||||
|     this.settingsDb = new njodb.Database(this.SettingsPath, { datastores: 2 }) |     this.settingsDb = new njodb.Database(this.SettingsPath, { datastores: 2 }) | ||||||
|     this.collectionsDb = new njodb.Database(this.CollectionsPath, { datastores: 2 }) |     this.collectionsDb = new njodb.Database(this.CollectionsPath, { datastores: 2 }) | ||||||
|  |     this.authorsDb = new njodb.Database(this.AuthorsPath) | ||||||
| 
 | 
 | ||||||
|     this.users = [] |     this.users = [] | ||||||
|     this.sessions = [] |     this.sessions = [] | ||||||
| @ -35,6 +38,7 @@ class Db { | |||||||
|     this.audiobooks = [] |     this.audiobooks = [] | ||||||
|     this.settings = [] |     this.settings = [] | ||||||
|     this.collections = [] |     this.collections = [] | ||||||
|  |     this.authors = [] | ||||||
| 
 | 
 | ||||||
|     this.serverSettings = null |     this.serverSettings = null | ||||||
| 
 | 
 | ||||||
| @ -49,6 +53,7 @@ class Db { | |||||||
|     else if (entityName === 'library') return this.librariesDb |     else if (entityName === 'library') return this.librariesDb | ||||||
|     else if (entityName === 'settings') return this.settingsDb |     else if (entityName === 'settings') return this.settingsDb | ||||||
|     else if (entityName === 'collection') return this.collectionsDb |     else if (entityName === 'collection') return this.collectionsDb | ||||||
|  |     else if (entityName === 'author') return this.authorsDb | ||||||
|     return null |     return null | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -59,6 +64,7 @@ class Db { | |||||||
|     else if (entityName === 'library') return 'libraries' |     else if (entityName === 'library') return 'libraries' | ||||||
|     else if (entityName === 'settings') return 'settings' |     else if (entityName === 'settings') return 'settings' | ||||||
|     else if (entityName === 'collection') return 'collections' |     else if (entityName === 'collection') return 'collections' | ||||||
|  |     else if (entityName === 'author') return 'authors' | ||||||
|     return null |     return null | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -96,6 +102,7 @@ class Db { | |||||||
|     this.librariesDb = new njodb.Database(this.LibrariesPath, { datastores: 2 }) |     this.librariesDb = new njodb.Database(this.LibrariesPath, { datastores: 2 }) | ||||||
|     this.settingsDb = new njodb.Database(this.SettingsPath, { datastores: 2 }) |     this.settingsDb = new njodb.Database(this.SettingsPath, { datastores: 2 }) | ||||||
|     this.collectionsDb = new njodb.Database(this.CollectionsPath, { datastores: 2 }) |     this.collectionsDb = new njodb.Database(this.CollectionsPath, { datastores: 2 }) | ||||||
|  |     this.authorsDb = new njodb.Database(this.AuthorsPath) | ||||||
|     return this.init() |     return this.init() | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -154,7 +161,11 @@ class Db { | |||||||
|       this.collections = results.data.map(l => new UserCollection(l)) |       this.collections = results.data.map(l => new UserCollection(l)) | ||||||
|       Logger.info(`[DB] ${this.collections.length} Collections Loaded`) |       Logger.info(`[DB] ${this.collections.length} Collections Loaded`) | ||||||
|     }) |     }) | ||||||
|     await Promise.all([p1, p2, p3, p4, p5]) |     var p6 = this.authorsDb.select(() => true).then((results) => { | ||||||
|  |       this.authors = results.data.map(l => new Author(l)) | ||||||
|  |       Logger.info(`[DB] ${this.authors.length} Authors Loaded`) | ||||||
|  |     }) | ||||||
|  |     await Promise.all([p1, p2, p3, p4, p5, p6]) | ||||||
| 
 | 
 | ||||||
|     // Update server version in server settings
 |     // Update server version in server settings
 | ||||||
|     if (this.previousVersion) { |     if (this.previousVersion) { | ||||||
|  | |||||||
							
								
								
									
										72
									
								
								server/objects/Author.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								server/objects/Author.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,72 @@ | |||||||
|  | const { getId } = require('../utils/index') | ||||||
|  | const Logger = require('../Logger') | ||||||
|  | 
 | ||||||
|  | class Author { | ||||||
|  |   constructor(author = null) { | ||||||
|  |     this.id = null | ||||||
|  |     this.name = null | ||||||
|  |     this.description = null | ||||||
|  |     this.asin = null | ||||||
|  |     this.image = null | ||||||
|  |     this.imageFullPath = null | ||||||
|  | 
 | ||||||
|  |     this.createdAt = null | ||||||
|  |     this.lastUpdate = null | ||||||
|  | 
 | ||||||
|  |     if (author) { | ||||||
|  |       this.construct(author) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   construct(author) { | ||||||
|  |     this.id = author.id | ||||||
|  |     this.name = author.name | ||||||
|  |     this.description = author.description | ||||||
|  |     this.asin = author.asin | ||||||
|  |     this.image = author.image | ||||||
|  |     this.imageFullPath = author.imageFullPath | ||||||
|  | 
 | ||||||
|  |     this.createdAt = author.createdAt | ||||||
|  |     this.lastUpdate = author.lastUpdate | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   toJSON() { | ||||||
|  |     return { | ||||||
|  |       id: this.id, | ||||||
|  |       name: this.name, | ||||||
|  |       description: this.description, | ||||||
|  |       asin: this.asin, | ||||||
|  |       image: this.image, | ||||||
|  |       imageFullPath: this.imageFullPath, | ||||||
|  |       createdAt: this.createdAt, | ||||||
|  |       lastUpdate: this.lastUpdate | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   setData(data) { | ||||||
|  |     this.id = data.id ? data.id : getId('per') | ||||||
|  |     this.name = data.name | ||||||
|  |     this.description = data.description | ||||||
|  |     this.asin = data.asin || null | ||||||
|  |     this.image = data.image || null | ||||||
|  |     this.imageFullPath = data.imageFullPath || null | ||||||
|  |     this.createdAt = Date.now() | ||||||
|  |     this.lastUpdate = Date.now() | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   update(payload) { | ||||||
|  |     var hasUpdates = false | ||||||
|  |     for (const key in payload) { | ||||||
|  |       if (this[key] === undefined) continue; | ||||||
|  |       if (this[key] !== payload[key]) { | ||||||
|  |         hasUpdates = true | ||||||
|  |         this[key] = payload[key] | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     if (hasUpdates) { | ||||||
|  |       this.lastUpdate = Date.now() | ||||||
|  |     } | ||||||
|  |     return hasUpdates | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | module.exports = Author | ||||||
| @ -9,6 +9,7 @@ class Book { | |||||||
|     this.author = null |     this.author = null | ||||||
|     this.authorFL = null |     this.authorFL = null | ||||||
|     this.authorLF = null |     this.authorLF = null | ||||||
|  |     this.authors = [] | ||||||
|     this.narrator = null |     this.narrator = null | ||||||
|     this.series = null |     this.series = null | ||||||
|     this.volumeNumber = null |     this.volumeNumber = null | ||||||
| @ -51,6 +52,7 @@ class Book { | |||||||
|     this.title = book.title |     this.title = book.title | ||||||
|     this.subtitle = book.subtitle || null |     this.subtitle = book.subtitle || null | ||||||
|     this.author = book.author |     this.author = book.author | ||||||
|  |     this.authors = (book.authors || []).map(a => ({ ...a })) | ||||||
|     this.authorFL = book.authorFL || null |     this.authorFL = book.authorFL || null | ||||||
|     this.authorLF = book.authorLF || null |     this.authorLF = book.authorLF || null | ||||||
|     this.narrator = book.narrator || book.narrarator || null // Mispelled initially... need to catch those
 |     this.narrator = book.narrator || book.narrarator || null // Mispelled initially... need to catch those
 | ||||||
| @ -81,6 +83,7 @@ class Book { | |||||||
|       title: this.title, |       title: this.title, | ||||||
|       subtitle: this.subtitle, |       subtitle: this.subtitle, | ||||||
|       author: this.author, |       author: this.author, | ||||||
|  |       authors: this.authors, | ||||||
|       authorFL: this.authorFL, |       authorFL: this.authorFL, | ||||||
|       authorLF: this.authorLF, |       authorLF: this.authorLF, | ||||||
|       narrator: this.narrator, |       narrator: this.narrator, | ||||||
| @ -142,6 +145,7 @@ class Book { | |||||||
|     this.title = data.title || null |     this.title = data.title || null | ||||||
|     this.subtitle = data.subtitle || null |     this.subtitle = data.subtitle || null | ||||||
|     this.author = data.author || null |     this.author = data.author || null | ||||||
|  |     this.authors = data.authors || [] | ||||||
|     this.narrator = data.narrator || data.narrarator || null |     this.narrator = data.narrator || data.narrarator || null | ||||||
|     this.series = data.series || null |     this.series = data.series || null | ||||||
|     this.volumeNumber = data.volumeNumber || null |     this.volumeNumber = data.volumeNumber || null | ||||||
|  | |||||||
							
								
								
									
										47
									
								
								server/providers/Audnexus.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								server/providers/Audnexus.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | |||||||
|  | const axios = require('axios') | ||||||
|  | const { levenshteinDistance } = require('../utils/index') | ||||||
|  | const Logger = require('../Logger') | ||||||
|  | 
 | ||||||
|  | class Audnexus { | ||||||
|  |   constructor() { | ||||||
|  |     this.baseUrl = 'https://api.audnex.us' | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   authorASINsRequest(name) { | ||||||
|  |     return axios.get(`${this.baseUrl}/authors?name=${name}`).then((res) => { | ||||||
|  |       return res.data || [] | ||||||
|  |     }).catch((error) => { | ||||||
|  |       Logger.error(`[Audnexus] Author ASIN request failed for ${name}`, error) | ||||||
|  |       return [] | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   authorRequest(asin) { | ||||||
|  |     return axios.get(`${this.baseUrl}/authors/${asin}`).then((res) => { | ||||||
|  |       return res.data | ||||||
|  |     }).catch((error) => { | ||||||
|  |       Logger.error(`[Audnexus] Author request failed for ${asin}`, error) | ||||||
|  |       return null | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async findAuthorByName(name, maxLevenshtein = 2) { | ||||||
|  |     Logger.debug(`[Audnexus] Looking up author by name ${name}`) | ||||||
|  |     var asins = await this.authorASINsRequest(name) | ||||||
|  |     var matchingAsin = asins.find(obj => levenshteinDistance(obj.name, name) <= maxLevenshtein) | ||||||
|  |     if (!matchingAsin) { | ||||||
|  |       return null | ||||||
|  |     } | ||||||
|  |     var author = await this.authorRequest(matchingAsin.asin) | ||||||
|  |     if (!author) { | ||||||
|  |       return null | ||||||
|  |     } | ||||||
|  |     return { | ||||||
|  |       asin: author.asin, | ||||||
|  |       description: author.description, | ||||||
|  |       image: author.image, | ||||||
|  |       name: author.name | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | module.exports = Audnexus | ||||||
| @ -142,3 +142,19 @@ async function recurseFiles(path) { | |||||||
|   return list |   return list | ||||||
| } | } | ||||||
| module.exports.recurseFiles = recurseFiles | module.exports.recurseFiles = recurseFiles | ||||||
|  | 
 | ||||||
|  | module.exports.downloadFile = async (url, filepath) => { | ||||||
|  |   Logger.debug(`[fileUtils] Downloading file to ${filepath}`) | ||||||
|  | 
 | ||||||
|  |   const writer = fs.createWriteStream(filepath) | ||||||
|  |   const response = await axios({ | ||||||
|  |     url, | ||||||
|  |     method: 'GET', | ||||||
|  |     responseType: 'stream' | ||||||
|  |   }) | ||||||
|  |   response.data.pipe(writer) | ||||||
|  |   return new Promise((resolve, reject) => { | ||||||
|  |     writer.on('finish', resolve) | ||||||
|  |     writer.on('error', reject) | ||||||
|  |   }) | ||||||
|  | } | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user