mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	This commit is contained in:
		
							parent
							
								
									0c168b3da4
								
							
						
					
					
						commit
						04f92c33c2
					
				@ -6,6 +6,7 @@ npm-debug.log
 | 
				
			|||||||
/config
 | 
					/config
 | 
				
			||||||
/audiobooks
 | 
					/audiobooks
 | 
				
			||||||
/audiobooks2
 | 
					/audiobooks2
 | 
				
			||||||
 | 
					/media/
 | 
				
			||||||
/metadata
 | 
					/metadata
 | 
				
			||||||
dev.js
 | 
					dev.js
 | 
				
			||||||
test/
 | 
					test/
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -4,6 +4,7 @@ node_modules/
 | 
				
			|||||||
/config/
 | 
					/config/
 | 
				
			||||||
/audiobooks/
 | 
					/audiobooks/
 | 
				
			||||||
/audiobooks2/
 | 
					/audiobooks2/
 | 
				
			||||||
 | 
					/media/
 | 
				
			||||||
/metadata/
 | 
					/metadata/
 | 
				
			||||||
test/
 | 
					test/
 | 
				
			||||||
/client/.nuxt/
 | 
					/client/.nuxt/
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div v-if="value" class="w-screen h-screen fixed top-0 left-0 z-50 bg-white text-black">
 | 
					  <div v-if="show" class="w-screen h-screen fixed top-0 left-0 z-50 bg-white text-black">
 | 
				
			||||||
    <div class="absolute top-4 right-4 z-10">
 | 
					    <div class="absolute top-4 right-4 z-10">
 | 
				
			||||||
      <span class="material-icons cursor-pointer text-4xl" @click="show = false">close</span>
 | 
					      <span class="material-icons cursor-pointer text-4xl" @click="show = false">close</span>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
@ -35,10 +35,6 @@
 | 
				
			|||||||
import ePub from 'epubjs'
 | 
					import ePub from 'epubjs'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  props: {
 | 
					 | 
				
			||||||
    value: Boolean,
 | 
					 | 
				
			||||||
    url: String
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      book: null,
 | 
					      book: null,
 | 
				
			||||||
@ -63,12 +59,34 @@ export default {
 | 
				
			|||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
    show: {
 | 
					    show: {
 | 
				
			||||||
      get() {
 | 
					      get() {
 | 
				
			||||||
        return this.value
 | 
					        return this.$store.state.showEReader
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      set(val) {
 | 
					      set(val) {
 | 
				
			||||||
        this.$emit('input', val)
 | 
					        this.$store.commit('setShowEReader', val)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    selectedAudiobook() {
 | 
				
			||||||
 | 
					      return this.$store.state.selectedAudiobook
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    libraryId() {
 | 
				
			||||||
 | 
					      return this.selectedAudiobook.libraryId
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    folderId() {
 | 
				
			||||||
 | 
					      return this.selectedAudiobook.folderId
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    ebooks() {
 | 
				
			||||||
 | 
					      return this.selectedAudiobook.ebooks || []
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    epubEbook() {
 | 
				
			||||||
 | 
					      return this.ebooks.find((eb) => eb.ext === '.epub')
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    epubPath() {
 | 
				
			||||||
 | 
					      return this.epubEbook ? this.epubEbook.path : null
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    url() {
 | 
				
			||||||
 | 
					      if (!this.epubPath) return null
 | 
				
			||||||
 | 
					      return `/ebook/${this.libraryId}/${this.folderId}/${this.epubPath}`
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    userToken() {
 | 
					    userToken() {
 | 
				
			||||||
      return this.$store.getters['user/getToken']
 | 
					      return this.$store.getters['user/getToken']
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -116,7 +134,7 @@ export default {
 | 
				
			|||||||
    init() {
 | 
					    init() {
 | 
				
			||||||
      this.registerListeners()
 | 
					      this.registerListeners()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      console.log('epub', this.url)
 | 
					      console.log('epub', this.url, this.epubEbook, this.ebooks)
 | 
				
			||||||
      // var book = ePub(this.url, {
 | 
					      // var book = ePub(this.url, {
 | 
				
			||||||
      //   requestHeaders: {
 | 
					      //   requestHeaders: {
 | 
				
			||||||
      //     Authorization: `Bearer ${this.userToken}`
 | 
					      //     Authorization: `Bearer ${this.userToken}`
 | 
				
			||||||
 | 
				
			|||||||
@ -14,11 +14,16 @@
 | 
				
			|||||||
          <cards-book-cover :audiobook="audiobook" :author-override="authorFormat" :width="width" />
 | 
					          <cards-book-cover :audiobook="audiobook" :author-override="authorFormat" :width="width" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <div v-show="isHovering || isSelectionMode" class="absolute top-0 left-0 w-full h-full bg-black rounded" :class="overlayWrapperClasslist">
 | 
					          <div v-show="isHovering || isSelectionMode" class="absolute top-0 left-0 w-full h-full bg-black rounded" :class="overlayWrapperClasslist">
 | 
				
			||||||
            <div v-show="!isSelectionMode && !isMissing" class="h-full flex items-center justify-center">
 | 
					            <div v-show="showPlayButton" class="h-full flex items-center justify-center">
 | 
				
			||||||
              <div class="hover:text-gray-200 hover:scale-110 transform duration-200" @click.stop.prevent="play">
 | 
					              <div class="hover:text-gray-200 hover:scale-110 transform duration-200" @click.stop.prevent="play">
 | 
				
			||||||
                <span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">play_circle_filled</span>
 | 
					                <span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">play_circle_filled</span>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div v-show="showReadButton" class="h-full flex items-center justify-center">
 | 
				
			||||||
 | 
					              <div class="hover:text-gray-200 hover:scale-110 transform duration-200" @click.stop.prevent="clickReadEBook">
 | 
				
			||||||
 | 
					                <span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">auto_stories</span>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <div v-if="userCanUpdate" v-show="!isSelectionMode" class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-50" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="editClick">
 | 
					            <div v-if="userCanUpdate" v-show="!isSelectionMode" class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-50" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="editClick">
 | 
				
			||||||
              <span class="material-icons" :style="{ fontSize: sizeMultiplier + 'rem' }">edit</span>
 | 
					              <span class="material-icons" :style="{ fontSize: sizeMultiplier + 'rem' }">edit</span>
 | 
				
			||||||
@ -34,9 +39,14 @@
 | 
				
			|||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <!-- EBook Icon -->
 | 
					          <!-- EBook Icon -->
 | 
				
			||||||
          <div v-if="showExperimentalFeatures && hasEbook" class="absolute rounded-full bg-blue-500 w-6 h-6 flex items-center justify-center bg-opacity-90" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
 | 
					          <div
 | 
				
			||||||
 | 
					            v-if="showSmallEBookIcon"
 | 
				
			||||||
 | 
					            class="absolute rounded-full bg-blue-500 flex items-center justify-center bg-opacity-90 hover:scale-125 transform duration-200"
 | 
				
			||||||
 | 
					            :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem`, width: 1.5 * sizeMultiplier + 'rem', height: 1.5 * sizeMultiplier + 'rem' }"
 | 
				
			||||||
 | 
					            @click.stop.prevent="clickReadEBook"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
            <!-- <p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">EBook</p> -->
 | 
					            <!-- <p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">EBook</p> -->
 | 
				
			||||||
            <span class="material-icons text-white text-base">auto_stories</span>
 | 
					            <span class="material-icons text-white" :style="{ fontSize: sizeMultiplier * 1 + 'rem' }">auto_stories</span>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <div v-show="!isSelectionMode" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full" :class="userIsRead ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
 | 
					          <div v-show="!isSelectionMode" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full" :class="userIsRead ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
 | 
				
			||||||
@ -90,8 +100,10 @@ export default {
 | 
				
			|||||||
    hasEbook() {
 | 
					    hasEbook() {
 | 
				
			||||||
      return this.audiobook.numEbooks
 | 
					      return this.audiobook.numEbooks
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    hasTracks() {
 | 
				
			||||||
 | 
					      return this.audiobook.numTracks
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    isSelectionMode() {
 | 
					    isSelectionMode() {
 | 
				
			||||||
      // return this.$store.getters['getNumAudiobooksSelected']
 | 
					 | 
				
			||||||
      return !!this.selectedAudiobooks.length
 | 
					      return !!this.selectedAudiobooks.length
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    selectedAudiobooks() {
 | 
					    selectedAudiobooks() {
 | 
				
			||||||
@ -150,11 +162,23 @@ export default {
 | 
				
			|||||||
      return this.userProgress ? !!this.userProgress.isRead : false
 | 
					      return this.userProgress ? !!this.userProgress.isRead : false
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    showError() {
 | 
					    showError() {
 | 
				
			||||||
      return this.hasMissingParts || this.hasInvalidParts || this.isMissing
 | 
					      return this.hasMissingParts || this.hasInvalidParts || this.isMissing || this.isIncomplete
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    showReadButton() {
 | 
				
			||||||
 | 
					      return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    showPlayButton() {
 | 
				
			||||||
 | 
					      return !this.isSelectionMode && !this.isMissing && !this.isIncomplete && this.hasTracks
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    showSmallEBookIcon() {
 | 
				
			||||||
 | 
					      return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    isMissing() {
 | 
					    isMissing() {
 | 
				
			||||||
      return this.audiobook.isMissing
 | 
					      return this.audiobook.isMissing
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    isIncomplete() {
 | 
				
			||||||
 | 
					      return this.audiobook.isIncomplete
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    hasMissingParts() {
 | 
					    hasMissingParts() {
 | 
				
			||||||
      return this.audiobook.hasMissingParts
 | 
					      return this.audiobook.hasMissingParts
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -163,6 +187,7 @@ export default {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    errorText() {
 | 
					    errorText() {
 | 
				
			||||||
      if (this.isMissing) return 'Audiobook directory is missing!'
 | 
					      if (this.isMissing) return 'Audiobook directory is missing!'
 | 
				
			||||||
 | 
					      else if (this.isIncomplete) return 'Audiobook has no audio tracks & ebook'
 | 
				
			||||||
      var txt = ''
 | 
					      var txt = ''
 | 
				
			||||||
      if (this.hasMissingParts) {
 | 
					      if (this.hasMissingParts) {
 | 
				
			||||||
        txt = `${this.hasMissingParts} missing parts.`
 | 
					        txt = `${this.hasMissingParts} missing parts.`
 | 
				
			||||||
@ -211,6 +236,9 @@ export default {
 | 
				
			|||||||
        e.preventDefault()
 | 
					        e.preventDefault()
 | 
				
			||||||
        this.selectBtnClick()
 | 
					        this.selectBtnClick()
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    clickReadEBook() {
 | 
				
			||||||
 | 
					      this.$store.commit('showEReader', this.audiobook)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="w-full h-full overflow-hidden overflow-y-auto px-4 py-6">
 | 
					  <div class="w-full h-full overflow-hidden overflow-y-auto px-4 py-6">
 | 
				
			||||||
    <p class="text-center text-lg mb-4 py-8">Preparing downloads can take several minutes and will be stored in <span class="bg-primary bg-opacity-75 font-mono p-1 text-base">/metadata/downloads</span>. After the download is ready, it will remain available for 60 minutes, then be deleted.<br />Download will timeout after 15 minutes.</p>
 | 
					    <p class="text-center text-lg mb-4 py-8">Preparing downloads can take several minutes and will be stored in <span class="bg-primary bg-opacity-75 font-mono p-1 text-base">/metadata/downloads</span>. After the download is ready, it will remain available for 60 minutes, then be deleted.<br />Download will timeout after 15 minutes.</p>
 | 
				
			||||||
    <div class="w-full border border-black-200 p-4 my-4">
 | 
					    <div v-if="showM4bDownload" class="w-full border border-black-200 p-4 my-4">
 | 
				
			||||||
      <div class="flex items-center">
 | 
					      <div class="flex items-center">
 | 
				
			||||||
        <div>
 | 
					        <div>
 | 
				
			||||||
          <p class="text-lg">M4B Audiobook File <span class="text-error">*</span></p>
 | 
					          <p class="text-lg">M4B Audiobook File <span class="text-error">*</span></p>
 | 
				
			||||||
@ -44,7 +44,7 @@
 | 
				
			|||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="w-full flex items-center justify-center absolute bottom-4 left-0 right-0 text-center">
 | 
					    <div v-if="showM4bDownload" class="w-full flex items-center justify-center absolute bottom-4 left-0 right-0 text-center">
 | 
				
			||||||
      <p class="text-error text-lg">* <strong>Experimental:</strong> Merging multiple .m4b files may have issues. <a href="https://github.com/advplyr/audiobookshelf/issues" class="underline text-blue-600" target="_blank">Report issues here.</a></p>
 | 
					      <p class="text-error text-lg">* <strong>Experimental:</strong> Merging multiple .m4b files may have issues. <a href="https://github.com/advplyr/audiobookshelf/issues" class="underline text-blue-600" target="_blank">Report issues here.</a></p>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -89,6 +89,9 @@ export default {
 | 
				
			|||||||
    audiobookId() {
 | 
					    audiobookId() {
 | 
				
			||||||
      return this.audiobook ? this.audiobook.id : null
 | 
					      return this.audiobook ? this.audiobook.id : null
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    _audiobook() {
 | 
				
			||||||
 | 
					      return this.audiobook || {}
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    downloads() {
 | 
					    downloads() {
 | 
				
			||||||
      return this.$store.getters['downloads/getDownloads'](this.audiobookId)
 | 
					      return this.$store.getters['downloads/getDownloads'](this.audiobookId)
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -120,6 +123,9 @@ export default {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    totalFiles() {
 | 
					    totalFiles() {
 | 
				
			||||||
      return this.audioFiles.length + this.otherFiles.length
 | 
					      return this.audioFiles.length + this.otherFiles.length
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    showM4bDownload() {
 | 
				
			||||||
 | 
					      return !this._audiobook.isMissing && !this._audiobook.isIncomplete && this._audiobook.tracks.length
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
<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">
 | 
				
			||||||
 | 
					    <template v-if="hasTracks">
 | 
				
			||||||
      <div class="w-full bg-primary px-4 py-2 flex items-center">
 | 
					      <div class="w-full bg-primary px-4 py-2 flex items-center">
 | 
				
			||||||
        <div class="h-7 w-7 rounded-full bg-white bg-opacity-10 flex items-center justify-center">
 | 
					        <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>
 | 
					          <span class="text-sm font-mono">{{ tracks.length }}</span>
 | 
				
			||||||
@ -36,6 +37,8 @@
 | 
				
			|||||||
          </tr>
 | 
					          </tr>
 | 
				
			||||||
        </template>
 | 
					        </template>
 | 
				
			||||||
      </table>
 | 
					      </table>
 | 
				
			||||||
 | 
					    </template>
 | 
				
			||||||
 | 
					    <div v-else class="flex my-4 text-center justify-center text-xl">No Audio Tracks</div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -94,6 +97,9 @@ export default {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    showDownload() {
 | 
					    showDownload() {
 | 
				
			||||||
      return this.userCanDownload && !this.isMissing
 | 
					      return this.userCanDownload && !this.isMissing
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    hasTracks() {
 | 
				
			||||||
 | 
					      return this.audiobook.tracks.length
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
 | 
				
			|||||||
@ -11,7 +11,12 @@
 | 
				
			|||||||
    <div class="flex-grow" />
 | 
					    <div class="flex-grow" />
 | 
				
			||||||
    <ui-btn v-show="mouseover && !libraryScan && canScan" small color="bg" @click.stop="scan">Scan</ui-btn>
 | 
					    <ui-btn v-show="mouseover && !libraryScan && canScan" small color="bg" @click.stop="scan">Scan</ui-btn>
 | 
				
			||||||
    <span v-show="mouseover && !libraryScan && showEdit && canEdit" class="material-icons text-xl text-gray-300 hover:text-gray-50 ml-4" @click.stop="editClick">edit</span>
 | 
					    <span v-show="mouseover && !libraryScan && showEdit && canEdit" class="material-icons text-xl text-gray-300 hover:text-gray-50 ml-4" @click.stop="editClick">edit</span>
 | 
				
			||||||
    <span v-show="!libraryScan && mouseover && showEdit && canDelete" class="material-icons text-xl text-gray-300 ml-3" :class="isMain ? 'text-opacity-5 cursor-not-allowed' : 'hover:text-gray-50'" @click.stop="deleteClick">delete</span>
 | 
					    <span v-show="!libraryScan && mouseover && showEdit && canDelete && !isDeleting" class="material-icons text-xl text-gray-300 ml-3" :class="isMain ? 'text-opacity-5 cursor-not-allowed' : 'hover:text-gray-50'" @click.stop="deleteClick">delete</span>
 | 
				
			||||||
 | 
					    <div v-show="isDeleting" class="text-xl text-gray-300 ml-3 animate-spin" :class="isMain ? 'text-opacity-5 cursor-not-allowed' : 'hover:text-gray-50'" @click.stop="deleteClick">
 | 
				
			||||||
 | 
					      <svg viewBox="0 0 24 24" class="w-6 h-6">
 | 
				
			||||||
 | 
					        <path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
 | 
				
			||||||
 | 
					      </svg>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -27,7 +32,8 @@ export default {
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      mouseover: false
 | 
					      mouseover: false,
 | 
				
			||||||
 | 
					      isDeleting: false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
@ -54,12 +60,29 @@ export default {
 | 
				
			|||||||
    editClick() {
 | 
					    editClick() {
 | 
				
			||||||
      this.$emit('edit', this.library)
 | 
					      this.$emit('edit', this.library)
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    deleteClick() {
 | 
					 | 
				
			||||||
      if (this.isMain) return
 | 
					 | 
				
			||||||
      this.$emit('delete', this.library)
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    scan() {
 | 
					    scan() {
 | 
				
			||||||
      this.$root.socket.emit('scan', this.library.id)
 | 
					      this.$root.socket.emit('scan', this.library.id)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    deleteClick() {
 | 
				
			||||||
 | 
					      if (this.isMain) return
 | 
				
			||||||
 | 
					      if (confirm(`Are you sure you want to permanently delete library "${this.library.name}"?`)) {
 | 
				
			||||||
 | 
					        this.isDeleting = true
 | 
				
			||||||
 | 
					        this.$axios
 | 
				
			||||||
 | 
					          .$delete(`/api/library/${this.library.id}`)
 | 
				
			||||||
 | 
					          .then((data) => {
 | 
				
			||||||
 | 
					            this.isDeleting = false
 | 
				
			||||||
 | 
					            if (data.error) {
 | 
				
			||||||
 | 
					              this.$toast.error(data.error)
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              this.$toast.success('Library deleted')
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					          .catch((error) => {
 | 
				
			||||||
 | 
					            console.error('Failed to delete library', error)
 | 
				
			||||||
 | 
					            this.$toast.error('Failed to delete library')
 | 
				
			||||||
 | 
					            this.isDeleting = false
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  mounted() {}
 | 
					  mounted() {}
 | 
				
			||||||
 | 
				
			|||||||
@ -8,7 +8,7 @@
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <template v-for="library in libraries">
 | 
					    <template v-for="library in libraries">
 | 
				
			||||||
      <modals-libraries-library-item :key="library.id" :library="library" :selected="currentLibraryId === library.id" :show-edit="true" @edit="editLibrary" @delete="deleteLibrary" @click="clickLibrary" />
 | 
					      <modals-libraries-library-item :key="library.id" :library="library" :selected="currentLibraryId === library.id" :show-edit="true" @edit="editLibrary" @click="clickLibrary" />
 | 
				
			||||||
    </template>
 | 
					    </template>
 | 
				
			||||||
    <modals-edit-library-modal v-model="showLibraryModal" :library="selectedLibrary" />
 | 
					    <modals-edit-library-modal v-model="showLibraryModal" :library="selectedLibrary" />
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
@ -38,27 +38,6 @@ export default {
 | 
				
			|||||||
      await this.$store.dispatch('libraries/fetch', library.id)
 | 
					      await this.$store.dispatch('libraries/fetch', library.id)
 | 
				
			||||||
      this.$router.push(`/library/${library.id}`)
 | 
					      this.$router.push(`/library/${library.id}`)
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    deleteLibrary(library) {
 | 
					 | 
				
			||||||
      if (library.id === 'main') return
 | 
					 | 
				
			||||||
      // if (confirm(`Are you sure you want to permanently delete user "${user.username}"?`)) {
 | 
					 | 
				
			||||||
      //   this.isDeletingUser = true
 | 
					 | 
				
			||||||
      //   this.$axios
 | 
					 | 
				
			||||||
      //     .$delete(`/api/user/${user.id}`)
 | 
					 | 
				
			||||||
      //     .then((data) => {
 | 
					 | 
				
			||||||
      //       this.isDeletingUser = false
 | 
					 | 
				
			||||||
      //       if (data.error) {
 | 
					 | 
				
			||||||
      //         this.$toast.error(data.error)
 | 
					 | 
				
			||||||
      //       } else {
 | 
					 | 
				
			||||||
      //         this.$toast.success('User deleted')
 | 
					 | 
				
			||||||
      //       }
 | 
					 | 
				
			||||||
      //     })
 | 
					 | 
				
			||||||
      //     .catch((error) => {
 | 
					 | 
				
			||||||
      //       console.error('Failed to delete user', error)
 | 
					 | 
				
			||||||
      //       this.$toast.error('Failed to delete user')
 | 
					 | 
				
			||||||
      //       this.isDeletingUser = false
 | 
					 | 
				
			||||||
      //     })
 | 
					 | 
				
			||||||
      // }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    clickAddLibrary() {
 | 
					    clickAddLibrary() {
 | 
				
			||||||
      this.selectedLibrary = null
 | 
					      this.selectedLibrary = null
 | 
				
			||||||
      this.showLibraryModal = true
 | 
					      this.showLibraryModal = true
 | 
				
			||||||
 | 
				
			|||||||
@ -7,6 +7,7 @@
 | 
				
			|||||||
    <app-stream-container ref="streamContainer" />
 | 
					    <app-stream-container ref="streamContainer" />
 | 
				
			||||||
    <modals-libraries-modal />
 | 
					    <modals-libraries-modal />
 | 
				
			||||||
    <modals-edit-modal />
 | 
					    <modals-edit-modal />
 | 
				
			||||||
 | 
					    <app-reader />
 | 
				
			||||||
    <!-- <widgets-scan-alert /> -->
 | 
					    <!-- <widgets-scan-alert /> -->
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "audiobookshelf-client",
 | 
					  "name": "audiobookshelf-client",
 | 
				
			||||||
  "version": "1.4.3",
 | 
					  "version": "1.4.4",
 | 
				
			||||||
  "description": "Audiobook manager and player",
 | 
					  "description": "Audiobook manager and player",
 | 
				
			||||||
  "main": "index.js",
 | 
					  "main": "index.js",
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
 | 
				
			|||||||
@ -57,7 +57,7 @@
 | 
				
			|||||||
                  </template>
 | 
					                  </template>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
              <div class="flex py-0.5">
 | 
					              <div v-if="tracks.length" class="flex py-0.5">
 | 
				
			||||||
                <div class="w-32">
 | 
					                <div class="w-32">
 | 
				
			||||||
                  <span class="text-white text-opacity-60 uppercase text-sm">Duration</span>
 | 
					                  <span class="text-white text-opacity-60 uppercase text-sm">Duration</span>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
@ -65,7 +65,7 @@
 | 
				
			|||||||
                  {{ durationPretty }}
 | 
					                  {{ durationPretty }}
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
              <div class="flex py-0.5">
 | 
					              <div v-if="tracks.length" class="flex py-0.5">
 | 
				
			||||||
                <div class="w-32">
 | 
					                <div class="w-32">
 | 
				
			||||||
                  <span class="text-white text-opacity-60 uppercase text-sm">Size</span>
 | 
					                  <span class="text-white text-opacity-60 uppercase text-sm">Size</span>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
@ -73,16 +73,20 @@
 | 
				
			|||||||
                  {{ sizePretty }}
 | 
					                  {{ sizePretty }}
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
              <!-- 
 | 
					 | 
				
			||||||
              <p v-if="narrator" class="text-base">
 | 
					 | 
				
			||||||
                <span class="text-white text-opacity-60">By:</span> <nuxt-link :to="`/library/${libraryId}/bookshelf?filter=authors.${$encode(author)}`" class="hover:underline">{{ author }}</nuxt-link>
 | 
					 | 
				
			||||||
              </p> -->
 | 
					 | 
				
			||||||
              <!-- <p v-if="narrator" class="text-base"><span class="text-white text-opacity-60">Narrated by:</span> {{ narrator }}</p>
 | 
					 | 
				
			||||||
              <p v-if="publishYear" class="text-base"><span class="text-white text-opacity-60">Publish year:</span> {{ publishYear }}</p>
 | 
					 | 
				
			||||||
              <p v-if="genres.length" class="text-base"><span class="text-white text-opacity-60">Genres:</span> {{ genres.join(', ') }}</p> -->
 | 
					 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div class="flex-grow" />
 | 
					            <div class="flex-grow" />
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <!-- Alerts -->
 | 
				
			||||||
 | 
					          <div v-show="showExperimentalReadAlert" class="bg-error p-4 rounded-xl flex items-center">
 | 
				
			||||||
 | 
					            <span class="material-icons text-2xl">warning_amber</span>
 | 
				
			||||||
 | 
					            <p class="ml-4">Book has no audio tracks but has valid ebook files. The e-reader is experimental and can be turned on in config.</p>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div v-show="showEpubAlert" class="bg-error p-4 rounded-xl flex items-center mt-2">
 | 
				
			||||||
 | 
					            <span class="material-icons text-2xl">warning_amber</span>
 | 
				
			||||||
 | 
					            <p class="ml-4">Book has valid ebook files, but the experimental e-reader currently only supports epub files.</p>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <div v-if="progressPercent > 0 && progressPercent < 1" class="px-4 py-2 mt-4 bg-primary text-sm font-semibold rounded-md text-gray-200 relative max-w-max" :class="resettingProgress ? 'opacity-25' : ''">
 | 
					          <div v-if="progressPercent > 0 && progressPercent < 1" class="px-4 py-2 mt-4 bg-primary text-sm font-semibold rounded-md text-gray-200 relative max-w-max" :class="resettingProgress ? 'opacity-25' : ''">
 | 
				
			||||||
            <p class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
 | 
					            <p class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
 | 
				
			||||||
            <p class="text-gray-400 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p>
 | 
					            <p class="text-gray-400 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p>
 | 
				
			||||||
@ -92,13 +96,13 @@
 | 
				
			|||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <div class="flex items-center pt-4">
 | 
					          <div class="flex items-center pt-4">
 | 
				
			||||||
            <ui-btn v-if="!isMissing" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="startStream">
 | 
					            <ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="startStream">
 | 
				
			||||||
              <span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
 | 
					              <span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
 | 
				
			||||||
              {{ streaming ? 'Streaming' : 'Play' }}
 | 
					              {{ streaming ? 'Streaming' : 'Play' }}
 | 
				
			||||||
            </ui-btn>
 | 
					            </ui-btn>
 | 
				
			||||||
            <ui-btn v-else color="error" :padding-x="4" small class="flex items-center h-9 mr-2">
 | 
					            <ui-btn v-else-if="isMissing || isIncomplete" color="error" :padding-x="4" small class="flex items-center h-9 mr-2">
 | 
				
			||||||
              <span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">error</span>
 | 
					              <span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">error</span>
 | 
				
			||||||
              Missing
 | 
					              {{ isMissing ? 'Missing' : 'Incomplete' }}
 | 
				
			||||||
            </ui-btn>
 | 
					            </ui-btn>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <ui-btn v-if="showExperimentalFeatures && epubEbook" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook">
 | 
					            <ui-btn v-if="showExperimentalFeatures && epubEbook" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook">
 | 
				
			||||||
@ -141,7 +145,7 @@
 | 
				
			|||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <tables-tracks-table :tracks="tracks" :audiobook="audiobook" class="mt-6" />
 | 
					          <tables-tracks-table v-if="tracks.length" :tracks="tracks" :audiobook="audiobook" class="mt-6" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <tables-audio-files-table v-if="otherAudioFiles.length" :audiobook-id="audiobook.id" :files="otherAudioFiles" class="mt-6" />
 | 
					          <tables-audio-files-table v-if="otherAudioFiles.length" :audiobook-id="audiobook.id" :files="otherAudioFiles" class="mt-6" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -150,7 +154,7 @@
 | 
				
			|||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <app-reader v-if="showExperimentalFeatures" v-model="showReader" :url="epubUrl" />
 | 
					    <!-- <app-reader v-if="showExperimentalFeatures" v-model="showReader" :url="epubUrl" /> -->
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -175,7 +179,6 @@ export default {
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      showReader: false,
 | 
					 | 
				
			||||||
      isRead: false,
 | 
					      isRead: false,
 | 
				
			||||||
      resettingProgress: false,
 | 
					      resettingProgress: false,
 | 
				
			||||||
      isProcessingReadUpdate: false
 | 
					      isProcessingReadUpdate: false
 | 
				
			||||||
@ -230,6 +233,12 @@ export default {
 | 
				
			|||||||
    isMissing() {
 | 
					    isMissing() {
 | 
				
			||||||
      return this.audiobook.isMissing
 | 
					      return this.audiobook.isMissing
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    isIncomplete() {
 | 
				
			||||||
 | 
					      return this.audiobook.isIncomplete
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    showPlayButton() {
 | 
				
			||||||
 | 
					      return !this.isMissing && !this.isIncomplete && this.tracks.length
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    missingParts() {
 | 
					    missingParts() {
 | 
				
			||||||
      return this.audiobook.missingParts || []
 | 
					      return this.audiobook.missingParts || []
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -313,16 +322,15 @@ export default {
 | 
				
			|||||||
    ebooks() {
 | 
					    ebooks() {
 | 
				
			||||||
      return this.audiobook.ebooks
 | 
					      return this.audiobook.ebooks
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    showEpubAlert() {
 | 
				
			||||||
 | 
					      return this.ebooks.length && !this.epubEbook && !this.tracks.length
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    showExperimentalReadAlert() {
 | 
				
			||||||
 | 
					      return !this.tracks.length && this.ebooks.length && !this.showExperimentalFeatures
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    epubEbook() {
 | 
					    epubEbook() {
 | 
				
			||||||
      return this.audiobook.ebooks.find((eb) => eb.ext === '.epub')
 | 
					      return this.audiobook.ebooks.find((eb) => eb.ext === '.epub')
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    epubPath() {
 | 
					 | 
				
			||||||
      return this.epubEbook ? this.epubEbook.path : null
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    epubUrl() {
 | 
					 | 
				
			||||||
      if (!this.epubPath) return null
 | 
					 | 
				
			||||||
      return `/ebook/${this.libraryId}/${this.folderId}/${this.epubPath}`
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    userToken() {
 | 
					    userToken() {
 | 
				
			||||||
      return this.$store.getters['user/getToken']
 | 
					      return this.$store.getters['user/getToken']
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -365,7 +373,7 @@ export default {
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
    openEbook() {
 | 
					    openEbook() {
 | 
				
			||||||
      this.showReader = true
 | 
					      this.$store.commit('showEReader', this.audiobook)
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    toggleRead() {
 | 
					    toggleRead() {
 | 
				
			||||||
      var updatePayload = {
 | 
					      var updatePayload = {
 | 
				
			||||||
 | 
				
			|||||||
@ -7,6 +7,7 @@ export const state = () => ({
 | 
				
			|||||||
  streamAudiobook: null,
 | 
					  streamAudiobook: null,
 | 
				
			||||||
  editModalTab: 'details',
 | 
					  editModalTab: 'details',
 | 
				
			||||||
  showEditModal: false,
 | 
					  showEditModal: false,
 | 
				
			||||||
 | 
					  showEReader: false,
 | 
				
			||||||
  selectedAudiobook: null,
 | 
					  selectedAudiobook: null,
 | 
				
			||||||
  playOnLoad: false,
 | 
					  playOnLoad: false,
 | 
				
			||||||
  developerMode: false,
 | 
					  developerMode: false,
 | 
				
			||||||
@ -111,6 +112,14 @@ export const mutations = {
 | 
				
			|||||||
  setShowEditModal(state, val) {
 | 
					  setShowEditModal(state, val) {
 | 
				
			||||||
    state.showEditModal = val
 | 
					    state.showEditModal = val
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  showEReader(state, audiobook) {
 | 
				
			||||||
 | 
					    console.log('Show EReader', audiobook)
 | 
				
			||||||
 | 
					    state.selectedAudiobook = audiobook
 | 
				
			||||||
 | 
					    state.showEReader = true
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  setShowEReader(state, val) {
 | 
				
			||||||
 | 
					    state.showEReader = val
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  setDeveloperMode(state, val) {
 | 
					  setDeveloperMode(state, val) {
 | 
				
			||||||
    state.developerMode = val
 | 
					    state.developerMode = val
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										11
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										11
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -1,6 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "audiobookshelf",
 | 
					  "name": "audiobookshelf",
 | 
				
			||||||
  "version": "1.4.1",
 | 
					  "version": "1.4.3",
 | 
				
			||||||
  "lockfileVersion": 1,
 | 
					  "lockfileVersion": 1,
 | 
				
			||||||
  "requires": true,
 | 
					  "requires": true,
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
@ -411,15 +411,6 @@
 | 
				
			|||||||
      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
 | 
					      "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "cookie-parser": {
 | 
					 | 
				
			||||||
      "version": "1.4.5",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.5.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw==",
 | 
					 | 
				
			||||||
      "requires": {
 | 
					 | 
				
			||||||
        "cookie": "0.4.0",
 | 
					 | 
				
			||||||
        "cookie-signature": "1.0.6"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "cookie-signature": {
 | 
					    "cookie-signature": {
 | 
				
			||||||
      "version": "1.0.6",
 | 
					      "version": "1.0.6",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "audiobookshelf",
 | 
					  "name": "audiobookshelf",
 | 
				
			||||||
  "version": "1.4.3",
 | 
					  "version": "1.4.4",
 | 
				
			||||||
  "description": "Self-hosted audiobook server for managing and playing audiobooks",
 | 
					  "description": "Self-hosted audiobook server for managing and playing audiobooks",
 | 
				
			||||||
  "main": "index.js",
 | 
					  "main": "index.js",
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
@ -8,7 +8,7 @@
 | 
				
			|||||||
    "start": "node index.js",
 | 
					    "start": "node index.js",
 | 
				
			||||||
    "client": "cd client && npm install && npm run generate",
 | 
					    "client": "cd client && npm install && npm run generate",
 | 
				
			||||||
    "prod": "npm run client && npm install && node prod.js",
 | 
					    "prod": "npm run client && npm install && node prod.js",
 | 
				
			||||||
    "build-win": "npm run build-prep && pkg -t node12-win-x64 -o ./dist/win/audiobookshelf .",
 | 
					    "build-win": "pkg -t node12-win-x64 -o ./dist/win/audiobookshelf .",
 | 
				
			||||||
    "build-linux": "build/linuxpackager"
 | 
					    "build-linux": "build/linuxpackager"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "bin": "prod.js",
 | 
					  "bin": "prod.js",
 | 
				
			||||||
@ -26,7 +26,6 @@
 | 
				
			|||||||
    "axios": "^0.21.1",
 | 
					    "axios": "^0.21.1",
 | 
				
			||||||
    "bcryptjs": "^2.4.3",
 | 
					    "bcryptjs": "^2.4.3",
 | 
				
			||||||
    "command-line-args": "^5.2.0",
 | 
					    "command-line-args": "^5.2.0",
 | 
				
			||||||
    "cookie-parser": "^1.4.5",
 | 
					 | 
				
			||||||
    "date-and-time": "^2.0.1",
 | 
					    "date-and-time": "^2.0.1",
 | 
				
			||||||
    "epub": "^1.2.1",
 | 
					    "epub": "^1.2.1",
 | 
				
			||||||
    "express": "^4.17.1",
 | 
					    "express": "^4.17.1",
 | 
				
			||||||
 | 
				
			|||||||
@ -24,6 +24,9 @@ class BackupManager {
 | 
				
			|||||||
    this.scheduleTask = null
 | 
					    this.scheduleTask = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.backups = []
 | 
					    this.backups = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // If backup exceeds this value it will be aborted
 | 
				
			||||||
 | 
					    this.MaxBytesBeforeAbort = 1000000000 // ~ 1GB
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get serverSettings() {
 | 
					  get serverSettings() {
 | 
				
			||||||
@ -191,6 +194,7 @@ class BackupManager {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async runBackup() {
 | 
					  async runBackup() {
 | 
				
			||||||
 | 
					    // Check if Metadata Path is inside Config Path (otherwise there will be an infinite loop as the archiver tries to zip itself)
 | 
				
			||||||
    Logger.info(`[BackupManager] Running Backup`)
 | 
					    Logger.info(`[BackupManager] Running Backup`)
 | 
				
			||||||
    var metadataBooksPath = this.serverSettings.backupMetadataCovers ? Path.join(this.MetadataPath, 'books') : null
 | 
					    var metadataBooksPath = this.serverSettings.backupMetadataCovers ? Path.join(this.MetadataPath, 'books') : null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -233,6 +237,7 @@ class BackupManager {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  async removeBackup(backup) {
 | 
					  async removeBackup(backup) {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
 | 
					      Logger.debug(`[BackupManager] Removing Backup "${backup.fullPath}"`)
 | 
				
			||||||
      await fs.remove(backup.fullPath)
 | 
					      await fs.remove(backup.fullPath)
 | 
				
			||||||
      this.backups = this.backups.filter(b => b.id !== backup.id)
 | 
					      this.backups = this.backups.filter(b => b.id !== backup.id)
 | 
				
			||||||
      Logger.info(`[BackupManager] Backup "${backup.id}" Removed`)
 | 
					      Logger.info(`[BackupManager] Backup "${backup.id}" Removed`)
 | 
				
			||||||
@ -263,6 +268,15 @@ class BackupManager {
 | 
				
			|||||||
        Logger.debug('Data has been drained')
 | 
					        Logger.debug('Data has been drained')
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      output.on('finish', () => {
 | 
				
			||||||
 | 
					        Logger.debug('Write Stream Finished')
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      output.on('error', (err) => {
 | 
				
			||||||
 | 
					        Logger.debug('Write Stream Error', err)
 | 
				
			||||||
 | 
					        reject(err)
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // good practice to catch warnings (ie stat failures and other non-blocking errors)
 | 
					      // good practice to catch warnings (ie stat failures and other non-blocking errors)
 | 
				
			||||||
      archive.on('warning', function (err) {
 | 
					      archive.on('warning', function (err) {
 | 
				
			||||||
        if (err.code === 'ENOENT') {
 | 
					        if (err.code === 'ENOENT') {
 | 
				
			||||||
@ -279,6 +293,16 @@ class BackupManager {
 | 
				
			|||||||
        Logger.error(`[BackupManager] Archiver error: ${err.message}`)
 | 
					        Logger.error(`[BackupManager] Archiver error: ${err.message}`)
 | 
				
			||||||
        reject(err)
 | 
					        reject(err)
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
 | 
					      archive.on('progress', ({ fs: fsobj }) => {
 | 
				
			||||||
 | 
					        if (fsobj.processedBytes > this.MaxBytesBeforeAbort) {
 | 
				
			||||||
 | 
					          Logger.error(`[BackupManager] Archiver is too large - aborting to prevent endless loop, Bytes Processed: ${fsobj.processedBytes}`)
 | 
				
			||||||
 | 
					          archive.abort()
 | 
				
			||||||
 | 
					          setTimeout(() => {
 | 
				
			||||||
 | 
					            this.removeBackup(backup)
 | 
				
			||||||
 | 
					            output.destroy('Backup too large') // Promise is reject in write stream error evt
 | 
				
			||||||
 | 
					          }, 500)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // pipe archive data to the file
 | 
					      // pipe archive data to the file
 | 
				
			||||||
      archive.pipe(output)
 | 
					      archive.pipe(output)
 | 
				
			||||||
 | 
				
			|||||||
@ -143,18 +143,21 @@ class Scanner {
 | 
				
			|||||||
      forceAudioFileScan = true
 | 
					      forceAudioFileScan = true
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // ino is now set for every file in scandir
 | 
					    // inode is required
 | 
				
			||||||
    audiobookData.audioFiles = audiobookData.audioFiles.filter(af => af.ino)
 | 
					    audiobookData.audioFiles = audiobookData.audioFiles.filter(af => af.ino)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // REMOVE: No valid audio files
 | 
					    // No valid ebook and audio files found, mark as incomplete
 | 
				
			||||||
    // TODO: Label as incomplete, do not actually delete
 | 
					    var ebookFiles = audiobookData.otherFiles.filter(f => f.filetype === 'ebook')
 | 
				
			||||||
    if (!audiobookData.audioFiles.length) {
 | 
					    if (!audiobookData.audioFiles.length && !ebookFiles.length) {
 | 
				
			||||||
      Logger.error(`[Scanner] "${existingAudiobook.title}" no valid audio files found - removing audiobook`)
 | 
					      Logger.error(`[Scanner] "${existingAudiobook.title}" no valid book files found - marking as incomplete`)
 | 
				
			||||||
 | 
					      existingAudiobook.setLastScan(version)
 | 
				
			||||||
      await this.db.removeEntity('audiobook', existingAudiobook.id)
 | 
					      existingAudiobook.isIncomplete = true
 | 
				
			||||||
      this.emitter('audiobook_removed', existingAudiobook.toJSONMinified())
 | 
					      await this.db.updateAudiobook(existingAudiobook)
 | 
				
			||||||
 | 
					      this.emitter('audiobook_updated', existingAudiobook.toJSONMinified())
 | 
				
			||||||
      return ScanResult.REMOVED
 | 
					      return ScanResult.UPDATED
 | 
				
			||||||
 | 
					    } else if (existingAudiobook.isIncomplete) { // Was incomplete but now is not
 | 
				
			||||||
 | 
					      Logger.info(`[Scanner] "${existingAudiobook.title}" was incomplete but now has book files`)
 | 
				
			||||||
 | 
					      existingAudiobook.isIncomplete = false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Check for audio files that were removed
 | 
					    // Check for audio files that were removed
 | 
				
			||||||
@ -219,14 +222,15 @@ class Scanner {
 | 
				
			|||||||
      await audioFileScanner.scanAudioFiles(existingAudiobook, newAudioFiles)
 | 
					      await audioFileScanner.scanAudioFiles(existingAudiobook, newAudioFiles)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // If after a scan no valid audio tracks remain
 | 
					    // After scanning audio files, some may no longer be valid
 | 
				
			||||||
    // TODO: Label as incomplete, do not actually delete
 | 
					    //   so make sure the directory still has valid book files
 | 
				
			||||||
    if (!existingAudiobook.tracks.length) {
 | 
					    if (!existingAudiobook.tracks.length && !ebookFiles.length) {
 | 
				
			||||||
      Logger.error(`[Scanner] "${existingAudiobook.title}" has no valid tracks after update - removing audiobook`)
 | 
					      Logger.error(`[Scanner] "${existingAudiobook.title}" no valid book files found after update - marking as incomplete`)
 | 
				
			||||||
 | 
					      existingAudiobook.setLastScan(version)
 | 
				
			||||||
      await this.db.removeEntity('audiobook', existingAudiobook.id)
 | 
					      existingAudiobook.isIncomplete = true
 | 
				
			||||||
      this.emitter('audiobook_removed', existingAudiobook.toJSONMinified())
 | 
					      await this.db.updateAudiobook(existingAudiobook)
 | 
				
			||||||
      return ScanResult.REMOVED
 | 
					      this.emitter('audiobook_updated', existingAudiobook.toJSONMinified())
 | 
				
			||||||
 | 
					      return ScanResult.UPDATED
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var hasUpdates = hasUpdatedIno || hasUpdatedLibraryOrFolder || removedAudioFiles.length || removedAudioTracks.length || newAudioFiles.length || hasUpdatedAudioFiles
 | 
					    var hasUpdates = hasUpdatedIno || hasUpdatedLibraryOrFolder || removedAudioFiles.length || removedAudioTracks.length || newAudioFiles.length || hasUpdatedAudioFiles
 | 
				
			||||||
@ -269,8 +273,9 @@ class Scanner {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async scanNewAudiobook(audiobookData) {
 | 
					  async scanNewAudiobook(audiobookData) {
 | 
				
			||||||
    if (!audiobookData.audioFiles.length) {
 | 
					    var ebookFiles = audiobookData.otherFiles.map(f => f.filetype === 'ebook')
 | 
				
			||||||
      Logger.error('[Scanner] No valid audio tracks for Audiobook', audiobookData.path)
 | 
					    if (!audiobookData.audioFiles.length && !ebookFiles.length) {
 | 
				
			||||||
 | 
					      Logger.error('[Scanner] No valid audio files and ebooks for Audiobook', audiobookData.path)
 | 
				
			||||||
      return null
 | 
					      return null
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -279,8 +284,9 @@ class Scanner {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // Scan audio files and set tracks, pulls metadata
 | 
					    // Scan audio files and set tracks, pulls metadata
 | 
				
			||||||
    await audioFileScanner.scanAudioFiles(audiobook, audiobookData.audioFiles)
 | 
					    await audioFileScanner.scanAudioFiles(audiobook, audiobookData.audioFiles)
 | 
				
			||||||
    if (!audiobook.tracks.length) {
 | 
					
 | 
				
			||||||
      Logger.warn('[Scanner] Invalid audiobook, no valid tracks', audiobook.title)
 | 
					    if (!audiobook.tracks.length && !audiobook.ebooks.length) {
 | 
				
			||||||
 | 
					      Logger.warn('[Scanner] Invalid audiobook, no valid audio tracks and ebook files', audiobook.title)
 | 
				
			||||||
      return null
 | 
					      return null
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -37,6 +37,8 @@ class Audiobook {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // Audiobook was scanned and not found
 | 
					    // Audiobook was scanned and not found
 | 
				
			||||||
    this.isMissing = false
 | 
					    this.isMissing = false
 | 
				
			||||||
 | 
					    // Audiobook no longer has "book" files
 | 
				
			||||||
 | 
					    this.isInvalid = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (audiobook) {
 | 
					    if (audiobook) {
 | 
				
			||||||
      this.construct(audiobook)
 | 
					      this.construct(audiobook)
 | 
				
			||||||
@ -70,6 +72,7 @@ class Audiobook {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.isMissing = !!audiobook.isMissing
 | 
					    this.isMissing = !!audiobook.isMissing
 | 
				
			||||||
 | 
					    this.isInvalid = !!audiobook.isInvalid
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get title() {
 | 
					  get title() {
 | 
				
			||||||
@ -175,7 +178,8 @@ class Audiobook {
 | 
				
			|||||||
      audioFiles: this._audioFiles.map(audioFile => audioFile.toJSON()),
 | 
					      audioFiles: this._audioFiles.map(audioFile => audioFile.toJSON()),
 | 
				
			||||||
      otherFiles: this._otherFiles.map(otherFile => otherFile.toJSON()),
 | 
					      otherFiles: this._otherFiles.map(otherFile => otherFile.toJSON()),
 | 
				
			||||||
      chapters: this.chapters || [],
 | 
					      chapters: this.chapters || [],
 | 
				
			||||||
      isMissing: !!this.isMissing
 | 
					      isMissing: !!this.isMissing,
 | 
				
			||||||
 | 
					      isInvalid: !!this.isInvalid
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -197,10 +201,12 @@ class Audiobook {
 | 
				
			|||||||
      hasMissingParts: this.missingParts ? this.missingParts.length : 0,
 | 
					      hasMissingParts: this.missingParts ? this.missingParts.length : 0,
 | 
				
			||||||
      hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0,
 | 
					      hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0,
 | 
				
			||||||
      // numEbooks: this.ebooks.length,
 | 
					      // numEbooks: this.ebooks.length,
 | 
				
			||||||
      numEbooks: this.hasEpub ? 1 : 0,
 | 
					      ebooks: this.ebooks.map(ebook => ebook.toJSON()),
 | 
				
			||||||
 | 
					      numEbooks: this.hasEpub ? 1 : 0, // Only supporting epubs in the reader currently
 | 
				
			||||||
      numTracks: this.tracks.length,
 | 
					      numTracks: this.tracks.length,
 | 
				
			||||||
      chapters: this.chapters || [],
 | 
					      chapters: this.chapters || [],
 | 
				
			||||||
      isMissing: !!this.isMissing
 | 
					      isMissing: !!this.isMissing,
 | 
				
			||||||
 | 
					      isInvalid: !!this.isInvalid
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -220,15 +226,16 @@ class Audiobook {
 | 
				
			|||||||
      sizePretty: this.sizePretty,
 | 
					      sizePretty: this.sizePretty,
 | 
				
			||||||
      missingParts: this.missingParts,
 | 
					      missingParts: this.missingParts,
 | 
				
			||||||
      invalidParts: this.invalidParts,
 | 
					      invalidParts: this.invalidParts,
 | 
				
			||||||
      audioFiles: (this.audioFiles || []).map(audioFile => audioFile.toJSON()),
 | 
					      audioFiles: this._audioFiles.map(audioFile => audioFile.toJSON()),
 | 
				
			||||||
      otherFiles: (this.otherFiles || []).map(otherFile => otherFile.toJSON()),
 | 
					      otherFiles: this._otherFiles.map(otherFile => otherFile.toJSON()),
 | 
				
			||||||
      ebooks: (this.ebooks || []).map(ebook => ebook.toJSON()),
 | 
					      ebooks: this.ebooks.map(ebook => ebook.toJSON()),
 | 
				
			||||||
      numEbooks: this.hasEpub ? 1 : 0,
 | 
					      numEbooks: this.hasEpub ? 1 : 0,
 | 
				
			||||||
      tags: this.tags,
 | 
					      tags: this.tags,
 | 
				
			||||||
      book: this.bookToJSON(),
 | 
					      book: this.bookToJSON(),
 | 
				
			||||||
      tracks: this.tracksToJSON(),
 | 
					      tracks: this.tracksToJSON(),
 | 
				
			||||||
      chapters: this.chapters || [],
 | 
					      chapters: this.chapters || [],
 | 
				
			||||||
      isMissing: !!this.isMissing
 | 
					      isMissing: !!this.isMissing,
 | 
				
			||||||
 | 
					      isInvalid: !!this.isInvalid
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -16,11 +16,12 @@ function getPaths(path) {
 | 
				
			|||||||
  })
 | 
					  })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function isAudioFile(path) {
 | 
					function isBookFile(path) {
 | 
				
			||||||
  if (!path) return false
 | 
					  if (!path) return false
 | 
				
			||||||
  var ext = Path.extname(path)
 | 
					  var ext = Path.extname(path)
 | 
				
			||||||
  if (!ext) return false
 | 
					  if (!ext) return false
 | 
				
			||||||
  return globals.SupportedAudioTypes.includes(ext.slice(1).toLowerCase())
 | 
					  var extclean = ext.slice(1).toLowerCase()
 | 
				
			||||||
 | 
					  return globals.SupportedAudioTypes.includes(extclean) || globals.SupportedEbookTypes.includes(extclean)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Input: array of relative file paths
 | 
					// Input: array of relative file paths
 | 
				
			||||||
@ -36,17 +37,18 @@ function groupFilesIntoAudiobookPaths(paths, useAllFileTypes = false) {
 | 
				
			|||||||
    return pathsA - pathsB
 | 
					    return pathsA - pathsB
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Step 2.5: Seperate audio files and other files (optional)
 | 
					  // Step 2.5: Seperate audio/ebook files and other files (optional)
 | 
				
			||||||
  var audioFilePaths = []
 | 
					  //              - Directories without an audio or ebook file will not be included
 | 
				
			||||||
 | 
					  var bookFilePaths = []
 | 
				
			||||||
  var otherFilePaths = []
 | 
					  var otherFilePaths = []
 | 
				
			||||||
  pathsFiltered.forEach(path => {
 | 
					  pathsFiltered.forEach(path => {
 | 
				
			||||||
    if (isAudioFile(path) || useAllFileTypes) audioFilePaths.push(path)
 | 
					    if (isBookFile(path) || useAllFileTypes) bookFilePaths.push(path)
 | 
				
			||||||
    else otherFilePaths.push(path)
 | 
					    else otherFilePaths.push(path)
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Step 3: Group audio files in audiobooks
 | 
					  // Step 3: Group audio files in audiobooks
 | 
				
			||||||
  var audiobookGroup = {}
 | 
					  var audiobookGroup = {}
 | 
				
			||||||
  audioFilePaths.forEach((path) => {
 | 
					  bookFilePaths.forEach((path) => {
 | 
				
			||||||
    var dirparts = Path.dirname(path).split(Path.sep)
 | 
					    var dirparts = Path.dirname(path).split(Path.sep)
 | 
				
			||||||
    var numparts = dirparts.length
 | 
					    var numparts = dirparts.length
 | 
				
			||||||
    var _path = ''
 | 
					    var _path = ''
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user