mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Adding download zip file, fix local cover art for m4b download
This commit is contained in:
		
							parent
							
								
									a7c538193c
								
							
						
					
					
						commit
						a56b3a8096
					
				@ -1,28 +1,54 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <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>
 | 
			
		||||
    <div class="w-full border border-black-200 p-4 my-4">
 | 
			
		||||
      <!-- <p class="text-center text-lg mb-4 pb-8 border-b border-black-200">
 | 
			
		||||
        <span class="text-error">Experimental Feature!</span> If your audiobook is made up of multiple audio files, this will concatenate them into a single file. The file type will be the same as the first track. Preparing downloads can take anywhere from a few seconds to 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 10 minutes then get deleted.
 | 
			
		||||
      </p> -->
 | 
			
		||||
      <p class="text-center text-lg mb-4 pb-8 border-b border-black-200">
 | 
			
		||||
        <span class="text-error">Experimental Feature!</span> If your audiobook has multiple tracks, this will merge them into a single M4B audiobook file.<br />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.
 | 
			
		||||
      </p>
 | 
			
		||||
 | 
			
		||||
      <div class="flex items-center">
 | 
			
		||||
        <p class="text-lg">{{ isSingleTrack ? 'Single Track' : 'M4B Audiobook File' }}</p>
 | 
			
		||||
        <div>
 | 
			
		||||
          <!-- <p class="text-lg">{{ isSingleTrack ? 'Single Track' : 'M4B Audiobook File' }}</p> -->
 | 
			
		||||
          <p class="text-lg">M4B Audiobook File <span class="text-error">*</span></p>
 | 
			
		||||
          <p class="max-w-xs text-sm pt-2 text-gray-300">Generate a .M4B audiobook file with embedded cover image and chapters.</p>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="flex-grow" />
 | 
			
		||||
        <div>
 | 
			
		||||
          <p v-if="singleAudioDownloadFailed" class="text-error mb-2">Download Failed</p>
 | 
			
		||||
          <p v-if="singleAudioDownloadReady" class="text-success mb-2">Download Ready!</p>
 | 
			
		||||
          <p v-if="singleAudioDownloadExpired" class="text-error mb-2">Download Expired</p>
 | 
			
		||||
          <a v-if="isSingleTrack" :href="`/local/${singleTrackPath}`" class="btn outline-none rounded-md shadow-md relative border border-gray-600 px-4 py-2 bg-primary">Download Track</a>
 | 
			
		||||
          <ui-btn v-else-if="!singleAudioDownloadReady" :loading="singleAudioDownloadPending" :disabled="tempDisable" @click="startSingleAudioDownload">Start Download</ui-btn>
 | 
			
		||||
          <ui-btn v-else @click="downloadWithProgress">Download</ui-btn>
 | 
			
		||||
          <p v-if="singleDownloadStatus === $constants.DownloadStatus.FAILED" class="text-error mb-2">Download Failed</p>
 | 
			
		||||
          <p v-if="singleDownloadStatus === $constants.DownloadStatus.READY" class="text-success mb-2">Download Ready!</p>
 | 
			
		||||
          <p v-if="singleDownloadStatus === $constants.DownloadStatus.EXPIRED" class="text-error mb-2">Download Expired</p>
 | 
			
		||||
 | 
			
		||||
          <!-- <a v-if="isSingleTrack" :href="`/local/${singleTrackPath}`" class="btn outline-none rounded-md shadow-md relative border border-gray-600 px-4 py-2 bg-primary">Download Track</a> -->
 | 
			
		||||
          <ui-btn v-if="singleDownloadStatus !== $constants.DownloadStatus.READY" :loading="singleDownloadStatus === $constants.DownloadStatus.PENDING" :disabled="tempDisable" @click="startSingleAudioDownload">Start Download</ui-btn>
 | 
			
		||||
          <div v-else>
 | 
			
		||||
            <ui-btn @click="downloadWithProgress(singleAudioDownload)">Download</ui-btn>
 | 
			
		||||
            <p class="px-0.5 py-1 text-sm font-mono text-center">Size: {{ $bytesPretty(singleAudioDownload.size) }}</p>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="w-full border border-black-200 p-4 my-4">
 | 
			
		||||
      <div class="flex items-center">
 | 
			
		||||
        <div>
 | 
			
		||||
          <p v-if="totalFiles > 1" class="text-lg">Zip {{ totalFiles }} Files</p>
 | 
			
		||||
          <p v-else>Zip 1 File</p>
 | 
			
		||||
          <p class="max-w-xs text-sm pt-2 text-gray-300">Generate a .ZIP file from the contents of the audiobook directory.</p>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="flex-grow" />
 | 
			
		||||
        <div>
 | 
			
		||||
          <p v-if="zipDownloadStatus === $constants.DownloadStatus.FAILED" class="text-error mb-2">Download Failed</p>
 | 
			
		||||
          <p v-if="zipDownloadStatus === $constants.DownloadStatus.READY" class="text-success mb-2">Download Ready!</p>
 | 
			
		||||
          <p v-if="zipDownloadStatus === $constants.DownloadStatus.EXPIRED" class="text-error mb-2">Download Expired</p>
 | 
			
		||||
 | 
			
		||||
          <ui-btn v-if="zipDownloadStatus !== $constants.DownloadStatus.READY" :loading="zipDownloadStatus === $constants.DownloadStatus.PENDING" :disabled="tempDisable" @click="startZipDownload">Start Download</ui-btn>
 | 
			
		||||
          <div v-else>
 | 
			
		||||
            <ui-btn @click="downloadWithProgress(zipDownload)">Download</ui-btn>
 | 
			
		||||
            <p class="px-0.5 py-1 text-sm font-mono text-center">Size: {{ $bytesPretty(zipDownload.size) }}</p>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div 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>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div v-if="isDownloading" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 z-50 flex items-center justify-center">
 | 
			
		||||
      <div class="w-80 border border-black-400 bg-bg rounded-xl h-20">
 | 
			
		||||
@ -55,7 +81,7 @@ export default {
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    singleAudioDownloadPending(newVal) {
 | 
			
		||||
    singleDownloadStatus(newVal) {
 | 
			
		||||
      if (newVal) {
 | 
			
		||||
        this.tempDisable = false
 | 
			
		||||
      }
 | 
			
		||||
@ -71,20 +97,14 @@ export default {
 | 
			
		||||
    singleAudioDownload() {
 | 
			
		||||
      return this.downloads.find((d) => d.type === 'singleAudio')
 | 
			
		||||
    },
 | 
			
		||||
    singleAudioDownloadPending() {
 | 
			
		||||
      return this.singleAudioDownload && this.singleAudioDownload.isPending
 | 
			
		||||
    singleDownloadStatus() {
 | 
			
		||||
      return this.singleAudioDownload ? this.singleAudioDownload.status : false
 | 
			
		||||
    },
 | 
			
		||||
    singleAudioDownloadFailed() {
 | 
			
		||||
      return this.singleAudioDownload && this.singleAudioDownload.isFailed
 | 
			
		||||
    zipDownload() {
 | 
			
		||||
      return this.downloads.find((d) => d.type === 'zip')
 | 
			
		||||
    },
 | 
			
		||||
    singleAudioDownloadReady() {
 | 
			
		||||
      return this.singleAudioDownload && this.singleAudioDownload.isReady
 | 
			
		||||
    },
 | 
			
		||||
    singleAudioDownloadExpired() {
 | 
			
		||||
      return this.singleAudioDownload && this.singleAudioDownload.isExpired
 | 
			
		||||
    },
 | 
			
		||||
    zipBundleDownload() {
 | 
			
		||||
      return this.downloads.find((d) => d.type === 'zipBundle')
 | 
			
		||||
    zipDownloadStatus() {
 | 
			
		||||
      return this.zipDownload ? this.zipDownload.status : false
 | 
			
		||||
    },
 | 
			
		||||
    isSingleTrack() {
 | 
			
		||||
      if (!this.audiobook.tracks) return false
 | 
			
		||||
@ -93,11 +113,34 @@ export default {
 | 
			
		||||
    singleTrackPath() {
 | 
			
		||||
      if (!this.isSingleTrack) return null
 | 
			
		||||
      return this.audiobook.tracks[0].path
 | 
			
		||||
    },
 | 
			
		||||
    audioFiles() {
 | 
			
		||||
      return this.audiobook ? this.audiobook.audioFiles || [] : []
 | 
			
		||||
    },
 | 
			
		||||
    otherFiles() {
 | 
			
		||||
      return this.audiobook ? this.audiobook.otherFiles || [] : []
 | 
			
		||||
    },
 | 
			
		||||
    totalFiles() {
 | 
			
		||||
      return this.audioFiles.length + this.otherFiles.length
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    startZipDownload() {
 | 
			
		||||
      // console.log('Download request received', this.audiobook)
 | 
			
		||||
 | 
			
		||||
      this.tempDisable = true
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        this.tempDisable = false
 | 
			
		||||
      }, 1000)
 | 
			
		||||
 | 
			
		||||
      var downloadPayload = {
 | 
			
		||||
        audiobookId: this.audiobook.id,
 | 
			
		||||
        type: 'zip'
 | 
			
		||||
      }
 | 
			
		||||
      this.$root.socket.emit('download', downloadPayload)
 | 
			
		||||
    },
 | 
			
		||||
    startSingleAudioDownload() {
 | 
			
		||||
      console.log('Download request received', this.audiobook)
 | 
			
		||||
      // console.log('Download request received', this.audiobook)
 | 
			
		||||
 | 
			
		||||
      this.tempDisable = true
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
@ -112,10 +155,10 @@ export default {
 | 
			
		||||
      }
 | 
			
		||||
      this.$root.socket.emit('download', downloadPayload)
 | 
			
		||||
    },
 | 
			
		||||
    downloadWithProgress() {
 | 
			
		||||
      var downloadId = this.singleAudioDownload.id
 | 
			
		||||
    downloadWithProgress(download) {
 | 
			
		||||
      var downloadId = download.id
 | 
			
		||||
      var downloadUrl = `${process.env.serverUrl}/api/download/${downloadId}`
 | 
			
		||||
      var filename = this.singleAudioDownload.filename
 | 
			
		||||
      var filename = download.filename
 | 
			
		||||
 | 
			
		||||
      this.isDownloading = true
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -127,39 +127,62 @@ export default {
 | 
			
		||||
        this.$store.commit('user/setSettings', user.settings)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    downloadToastClick(download) {
 | 
			
		||||
      console.log('Downlaod ready toast click', download)
 | 
			
		||||
      // if (!download || !download.audiobookId) {
 | 
			
		||||
      //   return console.error('Invalid download object', download)
 | 
			
		||||
      // }
 | 
			
		||||
      // var audiobook = this.$store.getters['audiobooks/getAudiobook'](download.audiobookId)
 | 
			
		||||
      // if (!audiobook) {
 | 
			
		||||
      //   return console.error('Audiobook not found for download', download)
 | 
			
		||||
      // }
 | 
			
		||||
      // this.$store.commit('showEditModalOnTab', { audiobook, tab: 'download' })
 | 
			
		||||
    },
 | 
			
		||||
    downloadStarted(download) {
 | 
			
		||||
      var filename = download.filename
 | 
			
		||||
      this.$toast.success(`Preparing download for "${filename}"`)
 | 
			
		||||
 | 
			
		||||
      download.isPending = true
 | 
			
		||||
      download.status = this.$constants.DownloadStatus.PENDING
 | 
			
		||||
      download.toastId = this.$toast(`Preparing download "${download.filename}"`, { timeout: false, draggable: false, closeOnClick: false, onClick: this.downloadToastClick })
 | 
			
		||||
      this.$store.commit('downloads/addUpdateDownload', download)
 | 
			
		||||
    },
 | 
			
		||||
    downloadReady(download) {
 | 
			
		||||
      var filename = download.filename
 | 
			
		||||
      this.$toast.success(`Download "${filename}" is ready!`)
 | 
			
		||||
      download.status = this.$constants.DownloadStatus.READY
 | 
			
		||||
      var existingDownload = this.$store.getters['downloads/getDownload'](download.id)
 | 
			
		||||
 | 
			
		||||
      download.isPending = false
 | 
			
		||||
      if (existingDownload && existingDownload.toastId !== undefined) {
 | 
			
		||||
        download.toastId = existingDownload.toastId
 | 
			
		||||
        this.$toast.update(existingDownload.toastId, { content: `Download "${download.filename}" is ready!`, options: { timeout: 5000, type: 'success', onClick: this.downloadToastClick } }, true)
 | 
			
		||||
      } else {
 | 
			
		||||
        this.$toast.success(`Download "${download.filename}" is ready!`)
 | 
			
		||||
      }
 | 
			
		||||
      this.$store.commit('downloads/addUpdateDownload', download)
 | 
			
		||||
    },
 | 
			
		||||
    downloadFailed(download) {
 | 
			
		||||
      var filename = download.filename
 | 
			
		||||
      this.$toast.error(`Download "${filename}" is failed`)
 | 
			
		||||
      download.status = this.$constants.DownloadStatus.FAILED
 | 
			
		||||
      var existingDownload = this.$store.getters['downloads/getDownload'](download.id)
 | 
			
		||||
 | 
			
		||||
      download.isFailed = true
 | 
			
		||||
      download.isReady = false
 | 
			
		||||
      download.isPending = false
 | 
			
		||||
      var failedMsg = download.isTimedOut ? 'timed out' : 'failed'
 | 
			
		||||
 | 
			
		||||
      if (existingDownload && existingDownload.toastId !== undefined) {
 | 
			
		||||
        download.toastId = existingDownload.toastId
 | 
			
		||||
        this.$toast.update(existingDownload.toastId, { content: `Download "${download.filename}" ${failedMsg}`, options: { timeout: 5000, type: 'error', onClick: this.downloadToastClick } }, true)
 | 
			
		||||
      } else {
 | 
			
		||||
        console.warn('Download failed no existing download', existingDownload)
 | 
			
		||||
        this.$toast.error(`Download "${download.filename}" ${failedMsg}`)
 | 
			
		||||
      }
 | 
			
		||||
      this.$store.commit('downloads/addUpdateDownload', download)
 | 
			
		||||
    },
 | 
			
		||||
    downloadKilled(download) {
 | 
			
		||||
      var filename = download.filename
 | 
			
		||||
      this.$toast.error(`Download "${filename}" was terminated`)
 | 
			
		||||
 | 
			
		||||
      var existingDownload = this.$store.getters['downloads/getDownload'](download.id)
 | 
			
		||||
      if (existingDownload && existingDownload.toastId !== undefined) {
 | 
			
		||||
        download.toastId = existingDownload.toastId
 | 
			
		||||
        this.$toast.update(existingDownload.toastId, { content: `Download "${download.filename}" was terminated`, options: { timeout: 5000, type: 'error', onClick: this.downloadToastClick } }, true)
 | 
			
		||||
      } else {
 | 
			
		||||
        console.warn('Download killed no existing download found', existingDownload)
 | 
			
		||||
        this.$toast.error(`Download "${download.filename}" was terminated`)
 | 
			
		||||
      }
 | 
			
		||||
      this.$store.commit('downloads/removeDownload', download)
 | 
			
		||||
    },
 | 
			
		||||
    downloadExpired(download) {
 | 
			
		||||
      download.isExpired = true
 | 
			
		||||
      download.isReady = false
 | 
			
		||||
      download.isPending = false
 | 
			
		||||
      download.status = this.$constants.DownloadStatus.EXPIRED
 | 
			
		||||
      this.$store.commit('downloads/addUpdateDownload', download)
 | 
			
		||||
    },
 | 
			
		||||
    initializeSocket() {
 | 
			
		||||
 | 
			
		||||
@ -47,6 +47,7 @@ module.exports = {
 | 
			
		||||
 | 
			
		||||
  // Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
 | 
			
		||||
  plugins: [
 | 
			
		||||
    '@/plugins/constants.js',
 | 
			
		||||
    '@/plugins/init.client.js',
 | 
			
		||||
    '@/plugins/axios.js',
 | 
			
		||||
    '@/plugins/toast.js'
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "audiobookshelf-client",
 | 
			
		||||
  "version": "1.1.7",
 | 
			
		||||
  "version": "1.1.8",
 | 
			
		||||
  "description": "Audiobook manager and player",
 | 
			
		||||
  "main": "index.js",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										14
									
								
								client/plugins/constants.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								client/plugins/constants.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
			
		||||
const DownloadStatus = {
 | 
			
		||||
  PENDING: 0,
 | 
			
		||||
  READY: 1,
 | 
			
		||||
  EXPIRED: 2,
 | 
			
		||||
  FAILED: 3
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Constants = {
 | 
			
		||||
  DownloadStatus
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ({ app }, inject) => {
 | 
			
		||||
  inject('constants', Constants)
 | 
			
		||||
}
 | 
			
		||||
@ -13,6 +13,9 @@ export const state = () => ({
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const getters = {
 | 
			
		||||
  getAudiobook: (state) => id => {
 | 
			
		||||
    return state.audiobooks.find(ab => ab.id === id)
 | 
			
		||||
  },
 | 
			
		||||
  getFiltered: (state, getters, rootState) => () => {
 | 
			
		||||
    var filtered = state.audiobooks
 | 
			
		||||
    var settings = rootState.user.settings || {}
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,9 @@ export const state = () => ({
 | 
			
		||||
export const getters = {
 | 
			
		||||
  getDownloads: (state) => (audiobookId) => {
 | 
			
		||||
    return state.downloads.filter(d => d.audiobookId === audiobookId)
 | 
			
		||||
  },
 | 
			
		||||
  getDownload: (state) => (id) => {
 | 
			
		||||
    return state.downloads.find(d => d.id === id)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										283
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										283
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "audiobookshelf",
 | 
			
		||||
  "version": "1.1.6",
 | 
			
		||||
  "version": "1.1.7",
 | 
			
		||||
  "lockfileVersion": 1,
 | 
			
		||||
  "requires": true,
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
@ -83,6 +83,53 @@
 | 
			
		||||
        "negotiator": "0.6.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "archiver": {
 | 
			
		||||
      "version": "5.3.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.0.tgz",
 | 
			
		||||
      "integrity": "sha512-iUw+oDwK0fgNpvveEsdQ0Ase6IIKztBJU2U0E9MzszMfmVVUyv1QJhS2ITW9ZCqx8dktAxVAjWWkKehuZE8OPg==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "archiver-utils": "^2.1.0",
 | 
			
		||||
        "async": "^3.2.0",
 | 
			
		||||
        "buffer-crc32": "^0.2.1",
 | 
			
		||||
        "readable-stream": "^3.6.0",
 | 
			
		||||
        "readdir-glob": "^1.0.0",
 | 
			
		||||
        "tar-stream": "^2.2.0",
 | 
			
		||||
        "zip-stream": "^4.1.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "archiver-utils": {
 | 
			
		||||
      "version": "2.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "glob": "^7.1.4",
 | 
			
		||||
        "graceful-fs": "^4.2.0",
 | 
			
		||||
        "lazystream": "^1.0.0",
 | 
			
		||||
        "lodash.defaults": "^4.2.0",
 | 
			
		||||
        "lodash.difference": "^4.5.0",
 | 
			
		||||
        "lodash.flatten": "^4.4.0",
 | 
			
		||||
        "lodash.isplainobject": "^4.0.6",
 | 
			
		||||
        "lodash.union": "^4.6.0",
 | 
			
		||||
        "normalize-path": "^3.0.0",
 | 
			
		||||
        "readable-stream": "^2.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "readable-stream": {
 | 
			
		||||
          "version": "2.3.7",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
 | 
			
		||||
          "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
 | 
			
		||||
          "requires": {
 | 
			
		||||
            "core-util-is": "~1.0.0",
 | 
			
		||||
            "inherits": "~2.0.3",
 | 
			
		||||
            "isarray": "~1.0.0",
 | 
			
		||||
            "process-nextick-args": "~2.0.0",
 | 
			
		||||
            "safe-buffer": "~5.1.1",
 | 
			
		||||
            "string_decoder": "~1.1.1",
 | 
			
		||||
            "util-deprecate": "~1.0.1"
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "are-shallow-equal": {
 | 
			
		||||
      "version": "1.1.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/are-shallow-equal/-/are-shallow-equal-1.1.1.tgz",
 | 
			
		||||
@ -124,6 +171,11 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
 | 
			
		||||
      "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI="
 | 
			
		||||
    },
 | 
			
		||||
    "base64-js": {
 | 
			
		||||
      "version": "1.5.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
 | 
			
		||||
      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
 | 
			
		||||
    },
 | 
			
		||||
    "base64id": {
 | 
			
		||||
      "version": "2.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
 | 
			
		||||
@ -134,6 +186,23 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
 | 
			
		||||
      "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms="
 | 
			
		||||
    },
 | 
			
		||||
    "bl": {
 | 
			
		||||
      "version": "4.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "buffer": "^5.5.0",
 | 
			
		||||
        "inherits": "^2.0.4",
 | 
			
		||||
        "readable-stream": "^3.4.0"
 | 
			
		||||
      },
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "inherits": {
 | 
			
		||||
          "version": "2.0.4",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
 | 
			
		||||
          "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "body-parser": {
 | 
			
		||||
      "version": "1.19.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
 | 
			
		||||
@ -160,6 +229,20 @@
 | 
			
		||||
        "concat-map": "0.0.1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "buffer": {
 | 
			
		||||
      "version": "5.7.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
 | 
			
		||||
      "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "base64-js": "^1.3.1",
 | 
			
		||||
        "ieee754": "^1.1.13"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "buffer-crc32": {
 | 
			
		||||
      "version": "0.2.13",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
 | 
			
		||||
      "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI="
 | 
			
		||||
    },
 | 
			
		||||
    "buffer-equal-constant-time": {
 | 
			
		||||
      "version": "1.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
 | 
			
		||||
@ -210,6 +293,17 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
 | 
			
		||||
      "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
 | 
			
		||||
    },
 | 
			
		||||
    "compress-commons": {
 | 
			
		||||
      "version": "4.1.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz",
 | 
			
		||||
      "integrity": "sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "buffer-crc32": "^0.2.13",
 | 
			
		||||
        "crc32-stream": "^4.0.2",
 | 
			
		||||
        "normalize-path": "^3.0.0",
 | 
			
		||||
        "readable-stream": "^3.6.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "concat-map": {
 | 
			
		||||
      "version": "0.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
 | 
			
		||||
@ -247,6 +341,11 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
 | 
			
		||||
      "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
 | 
			
		||||
    },
 | 
			
		||||
    "core-util-is": {
 | 
			
		||||
      "version": "1.0.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
 | 
			
		||||
      "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "cors": {
 | 
			
		||||
      "version": "2.8.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
 | 
			
		||||
@ -256,6 +355,24 @@
 | 
			
		||||
        "vary": "^1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "crc-32": {
 | 
			
		||||
      "version": "1.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "exit-on-epipe": "~1.0.1",
 | 
			
		||||
        "printj": "~1.1.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "crc32-stream": {
 | 
			
		||||
      "version": "4.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "crc-32": "^1.2.0",
 | 
			
		||||
        "readable-stream": "^3.4.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "debounce": {
 | 
			
		||||
      "version": "1.2.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
 | 
			
		||||
@ -385,6 +502,11 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
 | 
			
		||||
      "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
 | 
			
		||||
    },
 | 
			
		||||
    "exit-on-epipe": {
 | 
			
		||||
      "version": "1.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw=="
 | 
			
		||||
    },
 | 
			
		||||
    "express": {
 | 
			
		||||
      "version": "4.17.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
 | 
			
		||||
@ -468,6 +590,11 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
 | 
			
		||||
      "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
 | 
			
		||||
    },
 | 
			
		||||
    "fs-constants": {
 | 
			
		||||
      "version": "1.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
 | 
			
		||||
    },
 | 
			
		||||
    "fs-extra": {
 | 
			
		||||
      "version": "10.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz",
 | 
			
		||||
@ -478,6 +605,11 @@
 | 
			
		||||
        "universalify": "^2.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "fs.realpath": {
 | 
			
		||||
      "version": "1.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
 | 
			
		||||
      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
 | 
			
		||||
    },
 | 
			
		||||
    "get-stream": {
 | 
			
		||||
      "version": "5.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
 | 
			
		||||
@ -486,6 +618,19 @@
 | 
			
		||||
        "pump": "^3.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "glob": {
 | 
			
		||||
      "version": "7.1.7",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
 | 
			
		||||
      "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "fs.realpath": "^1.0.0",
 | 
			
		||||
        "inflight": "^1.0.4",
 | 
			
		||||
        "inherits": "2",
 | 
			
		||||
        "minimatch": "^3.0.4",
 | 
			
		||||
        "once": "^1.3.0",
 | 
			
		||||
        "path-is-absolute": "^1.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "got": {
 | 
			
		||||
      "version": "11.3.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/got/-/got-11.3.0.tgz",
 | 
			
		||||
@ -544,6 +689,20 @@
 | 
			
		||||
        "safer-buffer": ">= 2.1.2 < 3"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "ieee754": {
 | 
			
		||||
      "version": "1.2.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
 | 
			
		||||
      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
 | 
			
		||||
    },
 | 
			
		||||
    "inflight": {
 | 
			
		||||
      "version": "1.0.6",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
 | 
			
		||||
      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "once": "^1.3.0",
 | 
			
		||||
        "wrappy": "1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "inherits": {
 | 
			
		||||
      "version": "2.0.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
 | 
			
		||||
@ -564,6 +723,11 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w=="
 | 
			
		||||
    },
 | 
			
		||||
    "isarray": {
 | 
			
		||||
      "version": "1.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
 | 
			
		||||
      "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
 | 
			
		||||
    },
 | 
			
		||||
    "isexe": {
 | 
			
		||||
      "version": "2.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
 | 
			
		||||
@ -634,6 +798,30 @@
 | 
			
		||||
        "json-buffer": "3.0.1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "lazystream": {
 | 
			
		||||
      "version": "1.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz",
 | 
			
		||||
      "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "readable-stream": "^2.0.5"
 | 
			
		||||
      },
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "readable-stream": {
 | 
			
		||||
          "version": "2.3.7",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
 | 
			
		||||
          "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
 | 
			
		||||
          "requires": {
 | 
			
		||||
            "core-util-is": "~1.0.0",
 | 
			
		||||
            "inherits": "~2.0.3",
 | 
			
		||||
            "isarray": "~1.0.0",
 | 
			
		||||
            "process-nextick-args": "~2.0.0",
 | 
			
		||||
            "safe-buffer": "~5.1.1",
 | 
			
		||||
            "string_decoder": "~1.1.1",
 | 
			
		||||
            "util-deprecate": "~1.0.1"
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "libgen": {
 | 
			
		||||
      "version": "2.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/libgen/-/libgen-2.1.0.tgz",
 | 
			
		||||
@ -642,6 +830,21 @@
 | 
			
		||||
        "got": "11.3.x"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "lodash.defaults": {
 | 
			
		||||
      "version": "4.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
 | 
			
		||||
      "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw="
 | 
			
		||||
    },
 | 
			
		||||
    "lodash.difference": {
 | 
			
		||||
      "version": "4.5.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
 | 
			
		||||
      "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw="
 | 
			
		||||
    },
 | 
			
		||||
    "lodash.flatten": {
 | 
			
		||||
      "version": "4.4.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
 | 
			
		||||
      "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
 | 
			
		||||
    },
 | 
			
		||||
    "lodash.includes": {
 | 
			
		||||
      "version": "4.3.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
 | 
			
		||||
@ -677,6 +880,11 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
 | 
			
		||||
      "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
 | 
			
		||||
    },
 | 
			
		||||
    "lodash.union": {
 | 
			
		||||
      "version": "4.6.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
 | 
			
		||||
      "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg="
 | 
			
		||||
    },
 | 
			
		||||
    "lowercase-keys": {
 | 
			
		||||
      "version": "2.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
 | 
			
		||||
@ -754,6 +962,11 @@
 | 
			
		||||
        "minimatch": "^3.0.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "normalize-path": {
 | 
			
		||||
      "version": "3.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
 | 
			
		||||
    },
 | 
			
		||||
    "normalize-url": {
 | 
			
		||||
      "version": "6.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
 | 
			
		||||
@ -790,6 +1003,11 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
 | 
			
		||||
      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "path-is-absolute": {
 | 
			
		||||
      "version": "1.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
 | 
			
		||||
      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
 | 
			
		||||
    },
 | 
			
		||||
    "path-to-regexp": {
 | 
			
		||||
      "version": "0.1.7",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
 | 
			
		||||
@ -803,6 +1021,16 @@
 | 
			
		||||
        "rss": "^1.2.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "printj": {
 | 
			
		||||
      "version": "1.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz",
 | 
			
		||||
      "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "process-nextick-args": {
 | 
			
		||||
      "version": "2.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
 | 
			
		||||
    },
 | 
			
		||||
    "promise-concurrency-limiter": {
 | 
			
		||||
      "version": "1.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/promise-concurrency-limiter/-/promise-concurrency-limiter-1.0.0.tgz",
 | 
			
		||||
@ -862,6 +1090,24 @@
 | 
			
		||||
        "unpipe": "1.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "readable-stream": {
 | 
			
		||||
      "version": "3.6.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
 | 
			
		||||
      "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "inherits": "^2.0.3",
 | 
			
		||||
        "string_decoder": "^1.1.1",
 | 
			
		||||
        "util-deprecate": "^1.0.1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "readdir-glob": {
 | 
			
		||||
      "version": "1.1.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.1.tgz",
 | 
			
		||||
      "integrity": "sha512-91/k1EzZwDx6HbERR+zucygRFfiPl2zkIYZtv3Jjr6Mn7SkKcVct8aVO+sSRiGMc6fLf72du3d92/uY63YPdEA==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "minimatch": "^3.0.4"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "resolve-alpn": {
 | 
			
		||||
      "version": "1.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.0.tgz",
 | 
			
		||||
@ -1051,6 +1297,26 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/string-indexes/-/string-indexes-1.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-RUlx+2YydZJNlRAvoh1siPYWj/Xfk6t1sQLkA5n1tMGRCKkRLzkRtJhHk4qRmKergEBh8R3pWhsUsDqia/bolw=="
 | 
			
		||||
    },
 | 
			
		||||
    "string_decoder": {
 | 
			
		||||
      "version": "1.1.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
 | 
			
		||||
      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "safe-buffer": "~5.1.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "tar-stream": {
 | 
			
		||||
      "version": "2.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "bl": "^4.0.3",
 | 
			
		||||
        "end-of-stream": "^1.4.1",
 | 
			
		||||
        "fs-constants": "^1.0.0",
 | 
			
		||||
        "inherits": "^2.0.3",
 | 
			
		||||
        "readable-stream": "^3.1.1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "tiny-readdir": {
 | 
			
		||||
      "version": "1.5.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tiny-readdir/-/tiny-readdir-1.5.0.tgz",
 | 
			
		||||
@ -1083,6 +1349,11 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
 | 
			
		||||
      "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
 | 
			
		||||
    },
 | 
			
		||||
    "util-deprecate": {
 | 
			
		||||
      "version": "1.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
 | 
			
		||||
      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
 | 
			
		||||
    },
 | 
			
		||||
    "utils-merge": {
 | 
			
		||||
      "version": "1.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
 | 
			
		||||
@ -1128,6 +1399,16 @@
 | 
			
		||||
      "version": "1.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
 | 
			
		||||
      "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU="
 | 
			
		||||
    },
 | 
			
		||||
    "zip-stream": {
 | 
			
		||||
      "version": "4.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "archiver-utils": "^2.1.0",
 | 
			
		||||
        "compress-commons": "^4.1.0",
 | 
			
		||||
        "readable-stream": "^3.6.0"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "audiobookshelf",
 | 
			
		||||
  "version": "1.1.7",
 | 
			
		||||
  "version": "1.1.8",
 | 
			
		||||
  "description": "Self-hosted audiobook server for managing and playing audiobooks.",
 | 
			
		||||
  "main": "index.js",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
@ -10,6 +10,7 @@
 | 
			
		||||
  "author": "advplyr",
 | 
			
		||||
  "license": "ISC",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "archiver": "^5.3.0",
 | 
			
		||||
    "axios": "^0.21.1",
 | 
			
		||||
    "bcryptjs": "^2.4.3",
 | 
			
		||||
    "cookie-parser": "^1.4.5",
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
const Path = require('path')
 | 
			
		||||
const fs = require('fs-extra')
 | 
			
		||||
const archiver = require('archiver')
 | 
			
		||||
 | 
			
		||||
const workerThreads = require('worker_threads')
 | 
			
		||||
const Logger = require('./Logger')
 | 
			
		||||
@ -8,9 +9,10 @@ const { writeConcatFile, writeMetadataFile } = require('./utils/ffmpegHelpers')
 | 
			
		||||
const { getFileSize } = require('./utils/fileUtils')
 | 
			
		||||
 | 
			
		||||
class DownloadManager {
 | 
			
		||||
  constructor(db, MetadataPath, emitter) {
 | 
			
		||||
  constructor(db, MetadataPath, AudiobookPath, emitter) {
 | 
			
		||||
    this.db = db
 | 
			
		||||
    this.MetadataPath = MetadataPath
 | 
			
		||||
    this.AudiobookPath = AudiobookPath
 | 
			
		||||
    this.emitter = emitter
 | 
			
		||||
 | 
			
		||||
    this.downloadDirPath = Path.join(this.MetadataPath, 'downloads')
 | 
			
		||||
@ -68,8 +70,7 @@ class DownloadManager {
 | 
			
		||||
    var downloadType = options.type || 'singleAudio'
 | 
			
		||||
    delete options.type
 | 
			
		||||
 | 
			
		||||
    var filepath = null
 | 
			
		||||
    var filename = null
 | 
			
		||||
 | 
			
		||||
    var fileext = null
 | 
			
		||||
    var audiobookDirname = Path.basename(audiobook.path)
 | 
			
		||||
 | 
			
		||||
@ -80,18 +81,18 @@ class DownloadManager {
 | 
			
		||||
        var firstTrack = audiobook.tracks[0]
 | 
			
		||||
        audioFileType = firstTrack.ext
 | 
			
		||||
      }
 | 
			
		||||
      filename = audiobookDirname + audioFileType
 | 
			
		||||
      fileext = audioFileType
 | 
			
		||||
      filepath = Path.join(dlpath, filename)
 | 
			
		||||
    } else if (downloadType === 'zip') {
 | 
			
		||||
      fileext = '.zip'
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var filename = audiobookDirname + fileext
 | 
			
		||||
    var downloadData = {
 | 
			
		||||
      id: downloadId,
 | 
			
		||||
      audiobookId: audiobook.id,
 | 
			
		||||
      type: downloadType,
 | 
			
		||||
      options: options,
 | 
			
		||||
      dirpath: dlpath,
 | 
			
		||||
      fullPath: filepath,
 | 
			
		||||
      fullPath: Path.join(dlpath, filename),
 | 
			
		||||
      filename,
 | 
			
		||||
      ext: fileext,
 | 
			
		||||
      userId: (client && client.user) ? client.user.id : null,
 | 
			
		||||
@ -99,6 +100,7 @@ class DownloadManager {
 | 
			
		||||
    }
 | 
			
		||||
    var download = new Download()
 | 
			
		||||
    download.setData(downloadData)
 | 
			
		||||
    download.setTimeoutTimer(this.downloadTimedOut.bind(this))
 | 
			
		||||
 | 
			
		||||
    if (downloadData.socket) {
 | 
			
		||||
      downloadData.socket.emit('download_started', download.toJSON())
 | 
			
		||||
@ -106,29 +108,105 @@ class DownloadManager {
 | 
			
		||||
 | 
			
		||||
    if (download.type === 'singleAudio') {
 | 
			
		||||
      this.processSingleAudioDownload(audiobook, download)
 | 
			
		||||
    } else if (download.type === 'zip') {
 | 
			
		||||
      this.processZipDownload(audiobook, download)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async processZipDownload(audiobook, download) {
 | 
			
		||||
    this.pendingDownloads.push({
 | 
			
		||||
      id: download.id,
 | 
			
		||||
      download
 | 
			
		||||
    })
 | 
			
		||||
    Logger.info(`[DownloadManager] Processing Zip download ${download.fullPath}`)
 | 
			
		||||
    var success = await this.zipAudiobookDir(audiobook.fullPath, download.fullPath).then(() => {
 | 
			
		||||
      return true
 | 
			
		||||
    }).catch((error) => {
 | 
			
		||||
      Logger.error('[DownloadManager] Process Zip Failed', error)
 | 
			
		||||
      return false
 | 
			
		||||
    })
 | 
			
		||||
    this.sendResult(download, { success })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  zipAudiobookDir(audiobookPath, downloadPath) {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      // create a file to stream archive data to
 | 
			
		||||
      const output = fs.createWriteStream(downloadPath)
 | 
			
		||||
      const archive = archiver('zip', {
 | 
			
		||||
        zlib: { level: 9 } // Sets the compression level.
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      // listen for all archive data to be written
 | 
			
		||||
      // 'close' event is fired only when a file descriptor is involved
 | 
			
		||||
      output.on('close', () => {
 | 
			
		||||
        Logger.info(archive.pointer() + ' total bytes')
 | 
			
		||||
        Logger.debug('archiver has been finalized and the output file descriptor has closed.')
 | 
			
		||||
        resolve()
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      // This event is fired when the data source is drained no matter what was the data source.
 | 
			
		||||
      // It is not part of this library but rather from the NodeJS Stream API.
 | 
			
		||||
      // @see: https://nodejs.org/api/stream.html#stream_event_end
 | 
			
		||||
      output.on('end', () => {
 | 
			
		||||
        Logger.debug('Data has been drained')
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      // good practice to catch warnings (ie stat failures and other non-blocking errors)
 | 
			
		||||
      archive.on('warning', function (err) {
 | 
			
		||||
        if (err.code === 'ENOENT') {
 | 
			
		||||
          // log warning
 | 
			
		||||
          Logger.warn(`[DownloadManager] Archiver warning: ${err.message}`)
 | 
			
		||||
        } else {
 | 
			
		||||
          // throw error
 | 
			
		||||
          Logger.error(`[DownloadManager] Archiver error: ${err.message}`)
 | 
			
		||||
          // throw err
 | 
			
		||||
          reject(err)
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      archive.on('error', function (err) {
 | 
			
		||||
        Logger.error(`[DownloadManager] Archiver error: ${err.message}`)
 | 
			
		||||
        reject(err)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      // pipe archive data to the file
 | 
			
		||||
      archive.pipe(output)
 | 
			
		||||
 | 
			
		||||
      archive.directory(audiobookPath, false)
 | 
			
		||||
 | 
			
		||||
      archive.finalize()
 | 
			
		||||
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async processSingleAudioDownload(audiobook, download) {
 | 
			
		||||
 | 
			
		||||
    // If changing audio file type then encoding is needed
 | 
			
		||||
    var requiresEncode = audiobook.tracks[0].ext !== download.ext || download.includeCover || download.includeMetadata
 | 
			
		||||
    var audioRequiresEncode = audiobook.tracks[0].ext !== download.ext
 | 
			
		||||
    var shouldIncludeCover = download.includeCover && audiobook.book.cover
 | 
			
		||||
    var firstTrackIsM4b = audiobook.tracks[0].ext.toLowerCase() === '.m4b'
 | 
			
		||||
    var isOneTrack = audiobook.tracks.length === 1
 | 
			
		||||
 | 
			
		||||
    var concatFilePath = Path.join(download.dirpath, 'files.txt')
 | 
			
		||||
    await writeConcatFile(audiobook.tracks, concatFilePath)
 | 
			
		||||
    const ffmpegInputs = []
 | 
			
		||||
 | 
			
		||||
    const ffmpegInputs = [
 | 
			
		||||
      {
 | 
			
		||||
    if (!isOneTrack) {
 | 
			
		||||
      var concatFilePath = Path.join(download.dirpath, 'files.txt')
 | 
			
		||||
      await writeConcatFile(audiobook.tracks, concatFilePath)
 | 
			
		||||
      ffmpegInputs.push({
 | 
			
		||||
        input: concatFilePath,
 | 
			
		||||
        options: ['-safe 0', '-f concat']
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
      })
 | 
			
		||||
    } else {
 | 
			
		||||
      ffmpegInputs.push({
 | 
			
		||||
        input: audiobook.tracks[0].fullPath,
 | 
			
		||||
        options: firstTrackIsM4b ? ['-f mp4'] : []
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const logLevel = process.env.NODE_ENV === 'production' ? 'error' : 'warning'
 | 
			
		||||
    var ffmpegOptions = [`-loglevel ${logLevel}`]
 | 
			
		||||
    var ffmpegOutputOptions = []
 | 
			
		||||
 | 
			
		||||
    if (requiresEncode) {
 | 
			
		||||
    if (audioRequiresEncode) {
 | 
			
		||||
      ffmpegOptions = ffmpegOptions.concat([
 | 
			
		||||
        '-map 0:a',
 | 
			
		||||
        '-acodec aac',
 | 
			
		||||
@ -137,12 +215,18 @@ class DownloadManager {
 | 
			
		||||
        '-id3v2_version 3'
 | 
			
		||||
      ])
 | 
			
		||||
    } else {
 | 
			
		||||
      ffmpegOptions.push('-c copy')
 | 
			
		||||
      if (download.ext === '.m4b') {
 | 
			
		||||
        Logger.info('Concat m4b\'s use -f mp4')
 | 
			
		||||
        ffmpegOutputOptions.push('-f mp4')
 | 
			
		||||
      ffmpegOptions.push('-max_muxing_queue_size 1000')
 | 
			
		||||
 | 
			
		||||
      if (isOneTrack && firstTrackIsM4b && !shouldIncludeCover) {
 | 
			
		||||
        ffmpegOptions.push('-c copy')
 | 
			
		||||
      } else {
 | 
			
		||||
        ffmpegOptions.push('-c:a copy')
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (download.ext === '.m4b') {
 | 
			
		||||
      Logger.info('Concat m4b\'s use -f mp4')
 | 
			
		||||
      ffmpegOutputOptions.push('-f mp4')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (download.includeMetadata) {
 | 
			
		||||
      var metadataFilePath = Path.join(download.dirpath, 'metadata.txt')
 | 
			
		||||
@ -155,9 +239,14 @@ class DownloadManager {
 | 
			
		||||
      ffmpegOptions.push('-map_metadata 1')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (download.includeCover && audiobook.book.cover) {
 | 
			
		||||
    if (shouldIncludeCover) {
 | 
			
		||||
      var _cover = audiobook.book.cover
 | 
			
		||||
      if (_cover.startsWith(Path.sep + 'local')) {
 | 
			
		||||
        _cover = Path.join(this.AudiobookPath, _cover.replace(Path.sep + 'local', ''))
 | 
			
		||||
        Logger.debug('Local cover url', _cover)
 | 
			
		||||
      }
 | 
			
		||||
      ffmpegInputs.push({
 | 
			
		||||
        input: audiobook.book.cover,
 | 
			
		||||
        input: _cover,
 | 
			
		||||
        options: ['-f image2pipe']
 | 
			
		||||
      })
 | 
			
		||||
      ffmpegOptions.push('-vf [2:v]crop=trunc(iw/2)*2:trunc(ih/2)*2')
 | 
			
		||||
@ -175,7 +264,9 @@ class DownloadManager {
 | 
			
		||||
    worker.on('message', (message) => {
 | 
			
		||||
      if (message != null && typeof message === 'object') {
 | 
			
		||||
        if (message.type === 'RESULT') {
 | 
			
		||||
          this.sendResult(download, message)
 | 
			
		||||
          if (!download.isTimedOut) {
 | 
			
		||||
            this.sendResult(download, message)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        Logger.error('Invalid worker message', message)
 | 
			
		||||
@ -188,6 +279,17 @@ class DownloadManager {
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async downloadTimedOut(download) {
 | 
			
		||||
    Logger.info(`[DownloadManager] Download ${download.id} timed out (${download.timeoutTimeMs}ms)`)
 | 
			
		||||
 | 
			
		||||
    if (download.socket) {
 | 
			
		||||
      var downloadJson = download.toJSON()
 | 
			
		||||
      downloadJson.isTimedOut = true
 | 
			
		||||
      download.socket.emit('download_failed', downloadJson)
 | 
			
		||||
    }
 | 
			
		||||
    this.removeDownload(download)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async downloadExpired(download) {
 | 
			
		||||
    Logger.info(`[DownloadManager] Download ${download.id} expired`)
 | 
			
		||||
 | 
			
		||||
@ -198,6 +300,8 @@ class DownloadManager {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async sendResult(download, result) {
 | 
			
		||||
    download.clearTimeoutTimer()
 | 
			
		||||
 | 
			
		||||
    // Remove pending download
 | 
			
		||||
    this.pendingDownloads = this.pendingDownloads.filter(d => d.id !== download.id)
 | 
			
		||||
 | 
			
		||||
@ -216,18 +320,8 @@ class DownloadManager {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Remove files.txt if it was used
 | 
			
		||||
    // if (download.type === 'singleAudio') {
 | 
			
		||||
    //   var concatFilePath = Path.join(download.dirpath, 'files.txt')
 | 
			
		||||
    //   try {
 | 
			
		||||
    //     await fs.remove(concatFilePath)
 | 
			
		||||
    //   } catch (error) {
 | 
			
		||||
    //     Logger.error('[DownloadManager] Failed to remove files.txt')
 | 
			
		||||
    //   }
 | 
			
		||||
    // }
 | 
			
		||||
 | 
			
		||||
    result.size = await getFileSize(download.fullPath)
 | 
			
		||||
    download.setComplete(result)
 | 
			
		||||
    var filesize = await getFileSize(download.fullPath)
 | 
			
		||||
    download.setComplete(filesize)
 | 
			
		||||
    if (download.socket) {
 | 
			
		||||
      download.socket.emit('download_ready', download.toJSON())
 | 
			
		||||
    }
 | 
			
		||||
@ -240,15 +334,20 @@ class DownloadManager {
 | 
			
		||||
  async removeDownload(download) {
 | 
			
		||||
    Logger.info('[DownloadManager] Removing download ' + download.id)
 | 
			
		||||
 | 
			
		||||
    download.clearTimeoutTimer()
 | 
			
		||||
    download.clearExpirationTimer()
 | 
			
		||||
 | 
			
		||||
    var pendingDl = this.pendingDownloads.find(d => d.id === download.id)
 | 
			
		||||
 | 
			
		||||
    if (pendingDl) {
 | 
			
		||||
      this.pendingDownloads = this.pendingDownloads.filter(d => d.id !== download.id)
 | 
			
		||||
      Logger.warn(`[DownloadManager] Removing download in progress - stopping worker`)
 | 
			
		||||
      try {
 | 
			
		||||
        pendingDl.worker.postMessage('STOP')
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        Logger.error('[DownloadManager] Error posting stop message to worker', error)
 | 
			
		||||
      if (pendingDl.worker) {
 | 
			
		||||
        try {
 | 
			
		||||
          pendingDl.worker.postMessage('STOP')
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
          Logger.error('[DownloadManager] Error posting stop message to worker', error)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -34,7 +34,7 @@ class Server {
 | 
			
		||||
    this.scanner = new Scanner(this.AudiobookPath, this.MetadataPath, this.db, this.emitter.bind(this))
 | 
			
		||||
    this.streamManager = new StreamManager(this.db, this.MetadataPath)
 | 
			
		||||
    this.rssFeeds = new RssFeeds(this.Port, this.db)
 | 
			
		||||
    this.downloadManager = new DownloadManager(this.db, this.MetadataPath, this.emitter.bind(this))
 | 
			
		||||
    this.downloadManager = new DownloadManager(this.db, this.MetadataPath, this.AudiobookPath, this.emitter.bind(this))
 | 
			
		||||
    this.apiController = new ApiController(this.db, this.scanner, this.auth, this.streamManager, this.rssFeeds, this.downloadManager, this.emitter.bind(this), this.clientEmitter.bind(this))
 | 
			
		||||
    this.hlsController = new HlsController(this.db, this.scanner, this.auth, this.streamManager, this.emitter.bind(this), this.MetadataPath)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -411,28 +411,48 @@ class Audiobook {
 | 
			
		||||
    return this.audioFiles.find(af => af.ino === ino)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setChaptersFromAudioFile(audioFile) {
 | 
			
		||||
    if (!audioFile.chapters) return []
 | 
			
		||||
    return audioFile.chapters.map(c => ({ ...c }))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setChapters() {
 | 
			
		||||
    if (this.audioFiles.length === 1) {
 | 
			
		||||
      if (this.audioFiles[0].chapters) {
 | 
			
		||||
        this.chapters = this.audioFiles[0].chapters.map(c => ({ ...c }))
 | 
			
		||||
    // If 1 audio file without chapters, then no chapters will be set
 | 
			
		||||
 | 
			
		||||
    var includedAudioFiles = this.audioFiles.filter(af => !af.exclude)
 | 
			
		||||
    if (includedAudioFiles.length === 1) {
 | 
			
		||||
      // 1 audio file with chapters
 | 
			
		||||
      if (includedAudioFiles[0].chapters) {
 | 
			
		||||
        this.chapters = includedAudioFiles[0].chapters.map(c => ({ ...c }))
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      this.chapters = []
 | 
			
		||||
      var currTrackId = 0
 | 
			
		||||
      var currChapterId = 0
 | 
			
		||||
      var currStartTime = 0
 | 
			
		||||
      this.tracks.forEach((track) => {
 | 
			
		||||
        this.chapters.push({
 | 
			
		||||
          id: currTrackId++,
 | 
			
		||||
          start: currStartTime,
 | 
			
		||||
          end: currStartTime + track.duration,
 | 
			
		||||
          title: `Chapter ${currTrackId}`
 | 
			
		||||
        })
 | 
			
		||||
        currStartTime += track.duration
 | 
			
		||||
      includedAudioFiles.forEach((file) => {
 | 
			
		||||
        // If audio file has chapters use chapters
 | 
			
		||||
        if (file.chapters && file.chapters.length) {
 | 
			
		||||
          file.chapters.forEach((chapter) => {
 | 
			
		||||
            var chapterDuration = chapter.end - chapter.start
 | 
			
		||||
            if (chapterDuration > 0) {
 | 
			
		||||
              var title = `Chapter ${currChapterId}`
 | 
			
		||||
              if (chapter.title) {
 | 
			
		||||
                title += ` (${chapter.title})`
 | 
			
		||||
              }
 | 
			
		||||
              this.chapters.push({
 | 
			
		||||
                id: currChapterId++,
 | 
			
		||||
                start: currStartTime,
 | 
			
		||||
                end: currStartTime + chapterDuration,
 | 
			
		||||
                title
 | 
			
		||||
              })
 | 
			
		||||
              currStartTime += chapterDuration
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        } else if (file.duration) {
 | 
			
		||||
          // Otherwise just use track has chapter
 | 
			
		||||
          this.chapters.push({
 | 
			
		||||
            id: currChapterId++,
 | 
			
		||||
            start: currStartTime,
 | 
			
		||||
            end: currStartTime + file.duration,
 | 
			
		||||
            title: `Chapter ${currChapterId}`
 | 
			
		||||
          })
 | 
			
		||||
          currStartTime += file.duration
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
const DEFAULT_EXPIRATION = 1000 * 60 * 60 // 60 minutes
 | 
			
		||||
 | 
			
		||||
const DEFAULT_TIMEOUT = 1000 * 60 * 15 // 15 minutes
 | 
			
		||||
class Download {
 | 
			
		||||
  constructor(download) {
 | 
			
		||||
    this.id = null
 | 
			
		||||
@ -16,12 +16,17 @@ class Download {
 | 
			
		||||
    this.userId = null
 | 
			
		||||
    this.socket = null // Socket to notify when complete
 | 
			
		||||
    this.isReady = false
 | 
			
		||||
    this.isTimedOut = false
 | 
			
		||||
 | 
			
		||||
    this.startedAt = null
 | 
			
		||||
    this.finishedAt = null
 | 
			
		||||
    this.expiresAt = null
 | 
			
		||||
 | 
			
		||||
    this.expirationTimeMs = 0
 | 
			
		||||
    this.timeoutTimeMs = 0
 | 
			
		||||
 | 
			
		||||
    this.timeoutTimer = null
 | 
			
		||||
    this.expirationTimer = null
 | 
			
		||||
 | 
			
		||||
    if (download) {
 | 
			
		||||
      this.construct(download)
 | 
			
		||||
@ -88,6 +93,8 @@ class Download {
 | 
			
		||||
    this.finishedAt = download.finishedAt || null
 | 
			
		||||
 | 
			
		||||
    this.expirationTimeMs = download.expirationTimeMs || DEFAULT_EXPIRATION
 | 
			
		||||
    this.timeoutTimeMs = download.timeoutTimeMs || DEFAULT_TIMEOUT
 | 
			
		||||
 | 
			
		||||
    this.expiresAt = download.expiresAt || null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -105,11 +112,28 @@ class Download {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setExpirationTimer(callback) {
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
    this.expirationTimer = setTimeout(() => {
 | 
			
		||||
      if (callback) {
 | 
			
		||||
        callback(this)
 | 
			
		||||
      }
 | 
			
		||||
    }, this.expirationTimeMs)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setTimeoutTimer(callback) {
 | 
			
		||||
    this.timeoutTimer = setTimeout(() => {
 | 
			
		||||
      if (callback) {
 | 
			
		||||
        this.isTimedOut = true
 | 
			
		||||
        callback(this)
 | 
			
		||||
      }
 | 
			
		||||
    }, this.timeoutTimeMs)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  clearTimeoutTimer() {
 | 
			
		||||
    clearTimeout(this.timeoutTimer)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  clearExpirationTimer() {
 | 
			
		||||
    clearTimeout(this.expirationTimer)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
module.exports = Download
 | 
			
		||||
@ -113,7 +113,7 @@ function parseMediaStreamInfo(stream, all_streams, total_bit_rate) {
 | 
			
		||||
function parseChapters(chapters) {
 | 
			
		||||
  if (!chapters) return []
 | 
			
		||||
  return chapters.map(chap => {
 | 
			
		||||
    var title = chap['TAG:title'] || chap.title
 | 
			
		||||
    var title = chap['TAG:title'] || chap.title || ''
 | 
			
		||||
    var timebase = chap.time_base && chap.time_base.includes('/') ? Number(chap.time_base.split('/')[1]) : 1
 | 
			
		||||
    return {
 | 
			
		||||
      id: chap.id,
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user