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