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',
 | 
			
		||||
          component: 'modals-edit-tabs-cover'
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          id: 'tracks',
 | 
			
		||||
          title: 'Tracks',
 | 
			
		||||
          component: 'modals-edit-tabs-tracks'
 | 
			
		||||
        },
 | 
			
		||||
        // {
 | 
			
		||||
        //   id: 'tracks',
 | 
			
		||||
        //   title: 'Tracks',
 | 
			
		||||
        //   component: 'modals-edit-tabs-tracks'
 | 
			
		||||
        // },
 | 
			
		||||
        {
 | 
			
		||||
          id: 'chapters',
 | 
			
		||||
          title: 'Chapters',
 | 
			
		||||
@ -69,6 +69,11 @@ export default {
 | 
			
		||||
          title: '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 []
 | 
			
		||||
      return this.tabs.filter((tab) => {
 | 
			
		||||
        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 !== 'tracks' && this.userCanUpdate) return true
 | 
			
		||||
        if ((tab.id === 'download' || tab.id === 'files' || tab.id === 'authors') && this.userCanDownload) 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
 | 
			
		||||
        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>
 | 
			
		||||
  <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" />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
@ -13,9 +56,60 @@ export default {
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {}
 | 
			
		||||
    return {
 | 
			
		||||
      tracks: null,
 | 
			
		||||
      showFullPath: false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {},
 | 
			
		||||
  methods: {}
 | 
			
		||||
  watch: {
 | 
			
		||||
    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>
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="w-full my-2">
 | 
			
		||||
    <div class="w-full bg-primary px-6 py-2 flex items-center cursor-pointer">
 | 
			
		||||
      <p class="pr-4">Files</p>
 | 
			
		||||
    <div class="w-full bg-primary px-4 py-2 flex items-center cursor-pointer">
 | 
			
		||||
      <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>
 | 
			
		||||
      <div class="flex-grow" />
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ const { isObject, getId } = require('./utils/index')
 | 
			
		||||
const audioFileScanner = require('./utils/audioFileScanner')
 | 
			
		||||
 | 
			
		||||
const BookFinder = require('./BookFinder')
 | 
			
		||||
const AuthorController = require('./AuthorController')
 | 
			
		||||
 | 
			
		||||
const Library = require('./objects/Library')
 | 
			
		||||
const User = require('./objects/User')
 | 
			
		||||
@ -29,6 +30,7 @@ class ApiController {
 | 
			
		||||
    this.MetadataPath = MetadataPath
 | 
			
		||||
 | 
			
		||||
    this.bookFinder = new BookFinder()
 | 
			
		||||
    this.authorController = new AuthorController(this.MetadataPath)
 | 
			
		||||
 | 
			
		||||
    this.router = express()
 | 
			
		||||
    this.init()
 | 
			
		||||
@ -88,6 +90,13 @@ class ApiController {
 | 
			
		||||
    this.router.patch('/collection/:id', this.updateUserCollection.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.delete('/backup/:id', this.deleteBackup.bind(this))
 | 
			
		||||
@ -897,6 +906,63 @@ class ApiController {
 | 
			
		||||
    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) {
 | 
			
		||||
    if (!req.user.isRoot) {
 | 
			
		||||
      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 { CoverDestination } = require('./utils/constants')
 | 
			
		||||
const { downloadFile } = require('./utils/fileUtils')
 | 
			
		||||
 | 
			
		||||
class CoverController {
 | 
			
		||||
  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) {
 | 
			
		||||
    try {
 | 
			
		||||
      var { fullPath, relPath } = this.getCoverDirectory(audiobook)
 | 
			
		||||
      await fs.ensureDir(fullPath)
 | 
			
		||||
 | 
			
		||||
      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)
 | 
			
		||||
        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 UserCollection = require('./objects/UserCollection')
 | 
			
		||||
const Library = require('./objects/Library')
 | 
			
		||||
const Author = require('./objects/Author')
 | 
			
		||||
const ServerSettings = require('./objects/ServerSettings')
 | 
			
		||||
 | 
			
		||||
class Db {
 | 
			
		||||
@ -21,6 +22,7 @@ class Db {
 | 
			
		||||
    this.LibrariesPath = Path.join(ConfigPath, 'libraries')
 | 
			
		||||
    this.SettingsPath = Path.join(ConfigPath, 'settings')
 | 
			
		||||
    this.CollectionsPath = Path.join(ConfigPath, 'collections')
 | 
			
		||||
    this.AuthorsPath = Path.join(ConfigPath, 'authors')
 | 
			
		||||
 | 
			
		||||
    this.audiobooksDb = new njodb.Database(this.AudiobooksPath)
 | 
			
		||||
    this.usersDb = new njodb.Database(this.UsersPath)
 | 
			
		||||
@ -28,6 +30,7 @@ class Db {
 | 
			
		||||
    this.librariesDb = new njodb.Database(this.LibrariesPath, { datastores: 2 })
 | 
			
		||||
    this.settingsDb = new njodb.Database(this.SettingsPath, { datastores: 2 })
 | 
			
		||||
    this.collectionsDb = new njodb.Database(this.CollectionsPath, { datastores: 2 })
 | 
			
		||||
    this.authorsDb = new njodb.Database(this.AuthorsPath)
 | 
			
		||||
 | 
			
		||||
    this.users = []
 | 
			
		||||
    this.sessions = []
 | 
			
		||||
@ -35,6 +38,7 @@ class Db {
 | 
			
		||||
    this.audiobooks = []
 | 
			
		||||
    this.settings = []
 | 
			
		||||
    this.collections = []
 | 
			
		||||
    this.authors = []
 | 
			
		||||
 | 
			
		||||
    this.serverSettings = null
 | 
			
		||||
 | 
			
		||||
@ -49,6 +53,7 @@ class Db {
 | 
			
		||||
    else if (entityName === 'library') return this.librariesDb
 | 
			
		||||
    else if (entityName === 'settings') return this.settingsDb
 | 
			
		||||
    else if (entityName === 'collection') return this.collectionsDb
 | 
			
		||||
    else if (entityName === 'author') return this.authorsDb
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -59,6 +64,7 @@ class Db {
 | 
			
		||||
    else if (entityName === 'library') return 'libraries'
 | 
			
		||||
    else if (entityName === 'settings') return 'settings'
 | 
			
		||||
    else if (entityName === 'collection') return 'collections'
 | 
			
		||||
    else if (entityName === 'author') return 'authors'
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -96,6 +102,7 @@ class Db {
 | 
			
		||||
    this.librariesDb = new njodb.Database(this.LibrariesPath, { datastores: 2 })
 | 
			
		||||
    this.settingsDb = new njodb.Database(this.SettingsPath, { datastores: 2 })
 | 
			
		||||
    this.collectionsDb = new njodb.Database(this.CollectionsPath, { datastores: 2 })
 | 
			
		||||
    this.authorsDb = new njodb.Database(this.AuthorsPath)
 | 
			
		||||
    return this.init()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -154,7 +161,11 @@ class Db {
 | 
			
		||||
      this.collections = results.data.map(l => new UserCollection(l))
 | 
			
		||||
      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
 | 
			
		||||
    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.authorFL = null
 | 
			
		||||
    this.authorLF = null
 | 
			
		||||
    this.authors = []
 | 
			
		||||
    this.narrator = null
 | 
			
		||||
    this.series = null
 | 
			
		||||
    this.volumeNumber = null
 | 
			
		||||
@ -51,6 +52,7 @@ class Book {
 | 
			
		||||
    this.title = book.title
 | 
			
		||||
    this.subtitle = book.subtitle || null
 | 
			
		||||
    this.author = book.author
 | 
			
		||||
    this.authors = (book.authors || []).map(a => ({ ...a }))
 | 
			
		||||
    this.authorFL = book.authorFL || null
 | 
			
		||||
    this.authorLF = book.authorLF || null
 | 
			
		||||
    this.narrator = book.narrator || book.narrarator || null // Mispelled initially... need to catch those
 | 
			
		||||
@ -81,6 +83,7 @@ class Book {
 | 
			
		||||
      title: this.title,
 | 
			
		||||
      subtitle: this.subtitle,
 | 
			
		||||
      author: this.author,
 | 
			
		||||
      authors: this.authors,
 | 
			
		||||
      authorFL: this.authorFL,
 | 
			
		||||
      authorLF: this.authorLF,
 | 
			
		||||
      narrator: this.narrator,
 | 
			
		||||
@ -142,6 +145,7 @@ class Book {
 | 
			
		||||
    this.title = data.title || null
 | 
			
		||||
    this.subtitle = data.subtitle || null
 | 
			
		||||
    this.author = data.author || null
 | 
			
		||||
    this.authors = data.authors || []
 | 
			
		||||
    this.narrator = data.narrator || data.narrarator || null
 | 
			
		||||
    this.series = data.series || 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
 | 
			
		||||
}
 | 
			
		||||
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