mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-31 02:17:01 -04: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> | <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> | ||||||
|     <div class="w-full border border-black-200 p-4 my-4"> |     <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"> |       <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 class="flex-grow" /> | ||||||
|         <div> |         <div> | ||||||
|           <p v-if="singleAudioDownloadFailed" class="text-error mb-2">Download Failed</p> |           <p v-if="singleDownloadStatus === $constants.DownloadStatus.FAILED" class="text-error mb-2">Download Failed</p> | ||||||
|           <p v-if="singleAudioDownloadReady" class="text-success mb-2">Download Ready!</p> |           <p v-if="singleDownloadStatus === $constants.DownloadStatus.READY" class="text-success mb-2">Download Ready!</p> | ||||||
|           <p v-if="singleAudioDownloadExpired" class="text-error mb-2">Download Expired</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-else-if="!singleAudioDownloadReady" :loading="singleAudioDownloadPending" :disabled="tempDisable" @click="startSingleAudioDownload">Start Download</ui-btn> |           <!-- <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 @click="downloadWithProgress">Download</ui-btn> |           <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> | ||||||
|     </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 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"> |       <div class="w-80 border border-black-400 bg-bg rounded-xl h-20"> | ||||||
| @ -55,7 +81,7 @@ export default { | |||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   watch: { |   watch: { | ||||||
|     singleAudioDownloadPending(newVal) { |     singleDownloadStatus(newVal) { | ||||||
|       if (newVal) { |       if (newVal) { | ||||||
|         this.tempDisable = false |         this.tempDisable = false | ||||||
|       } |       } | ||||||
| @ -71,20 +97,14 @@ export default { | |||||||
|     singleAudioDownload() { |     singleAudioDownload() { | ||||||
|       return this.downloads.find((d) => d.type === 'singleAudio') |       return this.downloads.find((d) => d.type === 'singleAudio') | ||||||
|     }, |     }, | ||||||
|     singleAudioDownloadPending() { |     singleDownloadStatus() { | ||||||
|       return this.singleAudioDownload && this.singleAudioDownload.isPending |       return this.singleAudioDownload ? this.singleAudioDownload.status : false | ||||||
|     }, |     }, | ||||||
|     singleAudioDownloadFailed() { |     zipDownload() { | ||||||
|       return this.singleAudioDownload && this.singleAudioDownload.isFailed |       return this.downloads.find((d) => d.type === 'zip') | ||||||
|     }, |     }, | ||||||
|     singleAudioDownloadReady() { |     zipDownloadStatus() { | ||||||
|       return this.singleAudioDownload && this.singleAudioDownload.isReady |       return this.zipDownload ? this.zipDownload.status : false | ||||||
|     }, |  | ||||||
|     singleAudioDownloadExpired() { |  | ||||||
|       return this.singleAudioDownload && this.singleAudioDownload.isExpired |  | ||||||
|     }, |  | ||||||
|     zipBundleDownload() { |  | ||||||
|       return this.downloads.find((d) => d.type === 'zipBundle') |  | ||||||
|     }, |     }, | ||||||
|     isSingleTrack() { |     isSingleTrack() { | ||||||
|       if (!this.audiobook.tracks) return false |       if (!this.audiobook.tracks) return false | ||||||
| @ -93,11 +113,34 @@ export default { | |||||||
|     singleTrackPath() { |     singleTrackPath() { | ||||||
|       if (!this.isSingleTrack) return null |       if (!this.isSingleTrack) return null | ||||||
|       return this.audiobook.tracks[0].path |       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: { |   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() { |     startSingleAudioDownload() { | ||||||
|       console.log('Download request received', this.audiobook) |       // console.log('Download request received', this.audiobook) | ||||||
| 
 | 
 | ||||||
|       this.tempDisable = true |       this.tempDisable = true | ||||||
|       setTimeout(() => { |       setTimeout(() => { | ||||||
| @ -112,10 +155,10 @@ export default { | |||||||
|       } |       } | ||||||
|       this.$root.socket.emit('download', downloadPayload) |       this.$root.socket.emit('download', downloadPayload) | ||||||
|     }, |     }, | ||||||
|     downloadWithProgress() { |     downloadWithProgress(download) { | ||||||
|       var downloadId = this.singleAudioDownload.id |       var downloadId = download.id | ||||||
|       var downloadUrl = `${process.env.serverUrl}/api/download/${downloadId}` |       var downloadUrl = `${process.env.serverUrl}/api/download/${downloadId}` | ||||||
|       var filename = this.singleAudioDownload.filename |       var filename = download.filename | ||||||
| 
 | 
 | ||||||
|       this.isDownloading = true |       this.isDownloading = true | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -127,39 +127,62 @@ export default { | |||||||
|         this.$store.commit('user/setSettings', user.settings) |         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) { |     downloadStarted(download) { | ||||||
|       var filename = download.filename |       download.status = this.$constants.DownloadStatus.PENDING | ||||||
|       this.$toast.success(`Preparing download for "${filename}"`) |       download.toastId = this.$toast(`Preparing download "${download.filename}"`, { timeout: false, draggable: false, closeOnClick: false, onClick: this.downloadToastClick }) | ||||||
| 
 |  | ||||||
|       download.isPending = true |  | ||||||
|       this.$store.commit('downloads/addUpdateDownload', download) |       this.$store.commit('downloads/addUpdateDownload', download) | ||||||
|     }, |     }, | ||||||
|     downloadReady(download) { |     downloadReady(download) { | ||||||
|       var filename = download.filename |       download.status = this.$constants.DownloadStatus.READY | ||||||
|       this.$toast.success(`Download "${filename}" is 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) |       this.$store.commit('downloads/addUpdateDownload', download) | ||||||
|     }, |     }, | ||||||
|     downloadFailed(download) { |     downloadFailed(download) { | ||||||
|       var filename = download.filename |       download.status = this.$constants.DownloadStatus.FAILED | ||||||
|       this.$toast.error(`Download "${filename}" is failed`) |       var existingDownload = this.$store.getters['downloads/getDownload'](download.id) | ||||||
| 
 | 
 | ||||||
|       download.isFailed = true |       var failedMsg = download.isTimedOut ? 'timed out' : 'failed' | ||||||
|       download.isReady = false | 
 | ||||||
|       download.isPending = false |       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) |       this.$store.commit('downloads/addUpdateDownload', download) | ||||||
|     }, |     }, | ||||||
|     downloadKilled(download) { |     downloadKilled(download) { | ||||||
|       var filename = download.filename |       var existingDownload = this.$store.getters['downloads/getDownload'](download.id) | ||||||
|       this.$toast.error(`Download "${filename}" was terminated`) |       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) |       this.$store.commit('downloads/removeDownload', download) | ||||||
|     }, |     }, | ||||||
|     downloadExpired(download) { |     downloadExpired(download) { | ||||||
|       download.isExpired = true |       download.status = this.$constants.DownloadStatus.EXPIRED | ||||||
|       download.isReady = false |  | ||||||
|       download.isPending = false |  | ||||||
|       this.$store.commit('downloads/addUpdateDownload', download) |       this.$store.commit('downloads/addUpdateDownload', download) | ||||||
|     }, |     }, | ||||||
|     initializeSocket() { |     initializeSocket() { | ||||||
|  | |||||||
| @ -47,6 +47,7 @@ module.exports = { | |||||||
| 
 | 
 | ||||||
|   // Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
 |   // Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
 | ||||||
|   plugins: [ |   plugins: [ | ||||||
|  |     '@/plugins/constants.js', | ||||||
|     '@/plugins/init.client.js', |     '@/plugins/init.client.js', | ||||||
|     '@/plugins/axios.js', |     '@/plugins/axios.js', | ||||||
|     '@/plugins/toast.js' |     '@/plugins/toast.js' | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "audiobookshelf-client", |   "name": "audiobookshelf-client", | ||||||
|   "version": "1.1.7", |   "version": "1.1.8", | ||||||
|   "description": "Audiobook manager and player", |   "description": "Audiobook manager and player", | ||||||
|   "main": "index.js", |   "main": "index.js", | ||||||
|   "scripts": { |   "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 = { | export const getters = { | ||||||
|  |   getAudiobook: (state) => id => { | ||||||
|  |     return state.audiobooks.find(ab => ab.id === id) | ||||||
|  |   }, | ||||||
|   getFiltered: (state, getters, rootState) => () => { |   getFiltered: (state, getters, rootState) => () => { | ||||||
|     var filtered = state.audiobooks |     var filtered = state.audiobooks | ||||||
|     var settings = rootState.user.settings || {} |     var settings = rootState.user.settings || {} | ||||||
|  | |||||||
| @ -6,6 +6,9 @@ export const state = () => ({ | |||||||
| export const getters = { | export const getters = { | ||||||
|   getDownloads: (state) => (audiobookId) => { |   getDownloads: (state) => (audiobookId) => { | ||||||
|     return state.downloads.filter(d => d.audiobookId === 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", |   "name": "audiobookshelf", | ||||||
|   "version": "1.1.6", |   "version": "1.1.7", | ||||||
|   "lockfileVersion": 1, |   "lockfileVersion": 1, | ||||||
|   "requires": true, |   "requires": true, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
| @ -83,6 +83,53 @@ | |||||||
|         "negotiator": "0.6.2" |         "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": { |     "are-shallow-equal": { | ||||||
|       "version": "1.1.1", |       "version": "1.1.1", | ||||||
|       "resolved": "https://registry.npmjs.org/are-shallow-equal/-/are-shallow-equal-1.1.1.tgz", |       "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", |       "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", | ||||||
|       "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=" |       "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": { |     "base64id": { | ||||||
|       "version": "2.0.0", |       "version": "2.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", |       "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", |       "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", | ||||||
|       "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" |       "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": { |     "body-parser": { | ||||||
|       "version": "1.19.0", |       "version": "1.19.0", | ||||||
|       "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", |       "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", | ||||||
| @ -160,6 +229,20 @@ | |||||||
|         "concat-map": "0.0.1" |         "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": { |     "buffer-equal-constant-time": { | ||||||
|       "version": "1.0.1", |       "version": "1.0.1", | ||||||
|       "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", |       "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", |       "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", | ||||||
|       "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" |       "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": { |     "concat-map": { | ||||||
|       "version": "0.0.1", |       "version": "0.0.1", | ||||||
|       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", |       "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", |       "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", | ||||||
|       "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" |       "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": { |     "cors": { | ||||||
|       "version": "2.8.5", |       "version": "2.8.5", | ||||||
|       "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", |       "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", | ||||||
| @ -256,6 +355,24 @@ | |||||||
|         "vary": "^1" |         "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": { |     "debounce": { | ||||||
|       "version": "1.2.1", |       "version": "1.2.1", | ||||||
|       "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", |       "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", |       "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", | ||||||
|       "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" |       "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": { |     "express": { | ||||||
|       "version": "4.17.1", |       "version": "4.17.1", | ||||||
|       "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", |       "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", |       "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", | ||||||
|       "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" |       "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": { |     "fs-extra": { | ||||||
|       "version": "10.0.0", |       "version": "10.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz", |       "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz", | ||||||
| @ -478,6 +605,11 @@ | |||||||
|         "universalify": "^2.0.0" |         "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": { |     "get-stream": { | ||||||
|       "version": "5.2.0", |       "version": "5.2.0", | ||||||
|       "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", |       "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", | ||||||
| @ -486,6 +618,19 @@ | |||||||
|         "pump": "^3.0.0" |         "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": { |     "got": { | ||||||
|       "version": "11.3.0", |       "version": "11.3.0", | ||||||
|       "resolved": "https://registry.npmjs.org/got/-/got-11.3.0.tgz", |       "resolved": "https://registry.npmjs.org/got/-/got-11.3.0.tgz", | ||||||
| @ -544,6 +689,20 @@ | |||||||
|         "safer-buffer": ">= 2.1.2 < 3" |         "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": { |     "inherits": { | ||||||
|       "version": "2.0.3", |       "version": "2.0.3", | ||||||
|       "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", |       "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", |       "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz", | ||||||
|       "integrity": "sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w==" |       "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": { |     "isexe": { | ||||||
|       "version": "2.0.0", |       "version": "2.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", |       "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", | ||||||
| @ -634,6 +798,30 @@ | |||||||
|         "json-buffer": "3.0.1" |         "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": { |     "libgen": { | ||||||
|       "version": "2.1.0", |       "version": "2.1.0", | ||||||
|       "resolved": "https://registry.npmjs.org/libgen/-/libgen-2.1.0.tgz", |       "resolved": "https://registry.npmjs.org/libgen/-/libgen-2.1.0.tgz", | ||||||
| @ -642,6 +830,21 @@ | |||||||
|         "got": "11.3.x" |         "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": { |     "lodash.includes": { | ||||||
|       "version": "4.3.0", |       "version": "4.3.0", | ||||||
|       "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", |       "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", |       "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", | ||||||
|       "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" |       "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": { |     "lowercase-keys": { | ||||||
|       "version": "2.0.0", |       "version": "2.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", |       "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", | ||||||
| @ -754,6 +962,11 @@ | |||||||
|         "minimatch": "^3.0.2" |         "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": { |     "normalize-url": { | ||||||
|       "version": "6.1.0", |       "version": "6.1.0", | ||||||
|       "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", |       "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", |       "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", | ||||||
|       "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" |       "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": { |     "path-to-regexp": { | ||||||
|       "version": "0.1.7", |       "version": "0.1.7", | ||||||
|       "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", |       "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", | ||||||
| @ -803,6 +1021,16 @@ | |||||||
|         "rss": "^1.2.2" |         "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": { |     "promise-concurrency-limiter": { | ||||||
|       "version": "1.0.0", |       "version": "1.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/promise-concurrency-limiter/-/promise-concurrency-limiter-1.0.0.tgz", |       "resolved": "https://registry.npmjs.org/promise-concurrency-limiter/-/promise-concurrency-limiter-1.0.0.tgz", | ||||||
| @ -862,6 +1090,24 @@ | |||||||
|         "unpipe": "1.0.0" |         "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": { |     "resolve-alpn": { | ||||||
|       "version": "1.2.0", |       "version": "1.2.0", | ||||||
|       "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.0.tgz", |       "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", |       "resolved": "https://registry.npmjs.org/string-indexes/-/string-indexes-1.0.0.tgz", | ||||||
|       "integrity": "sha512-RUlx+2YydZJNlRAvoh1siPYWj/Xfk6t1sQLkA5n1tMGRCKkRLzkRtJhHk4qRmKergEBh8R3pWhsUsDqia/bolw==" |       "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": { |     "tiny-readdir": { | ||||||
|       "version": "1.5.0", |       "version": "1.5.0", | ||||||
|       "resolved": "https://registry.npmjs.org/tiny-readdir/-/tiny-readdir-1.5.0.tgz", |       "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", |       "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", | ||||||
|       "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" |       "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": { |     "utils-merge": { | ||||||
|       "version": "1.0.1", |       "version": "1.0.1", | ||||||
|       "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", |       "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", | ||||||
| @ -1128,6 +1399,16 @@ | |||||||
|       "version": "1.0.1", |       "version": "1.0.1", | ||||||
|       "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", |       "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", | ||||||
|       "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=" |       "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", |   "name": "audiobookshelf", | ||||||
|   "version": "1.1.7", |   "version": "1.1.8", | ||||||
|   "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": { | ||||||
| @ -10,6 +10,7 @@ | |||||||
|   "author": "advplyr", |   "author": "advplyr", | ||||||
|   "license": "ISC", |   "license": "ISC", | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|  |     "archiver": "^5.3.0", | ||||||
|     "axios": "^0.21.1", |     "axios": "^0.21.1", | ||||||
|     "bcryptjs": "^2.4.3", |     "bcryptjs": "^2.4.3", | ||||||
|     "cookie-parser": "^1.4.5", |     "cookie-parser": "^1.4.5", | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| const Path = require('path') | const Path = require('path') | ||||||
| const fs = require('fs-extra') | const fs = require('fs-extra') | ||||||
|  | const archiver = require('archiver') | ||||||
| 
 | 
 | ||||||
| const workerThreads = require('worker_threads') | const workerThreads = require('worker_threads') | ||||||
| const Logger = require('./Logger') | const Logger = require('./Logger') | ||||||
| @ -8,9 +9,10 @@ const { writeConcatFile, writeMetadataFile } = require('./utils/ffmpegHelpers') | |||||||
| const { getFileSize } = require('./utils/fileUtils') | const { getFileSize } = require('./utils/fileUtils') | ||||||
| 
 | 
 | ||||||
| class DownloadManager { | class DownloadManager { | ||||||
|   constructor(db, MetadataPath, emitter) { |   constructor(db, MetadataPath, AudiobookPath, emitter) { | ||||||
|     this.db = db |     this.db = db | ||||||
|     this.MetadataPath = MetadataPath |     this.MetadataPath = MetadataPath | ||||||
|  |     this.AudiobookPath = AudiobookPath | ||||||
|     this.emitter = emitter |     this.emitter = emitter | ||||||
| 
 | 
 | ||||||
|     this.downloadDirPath = Path.join(this.MetadataPath, 'downloads') |     this.downloadDirPath = Path.join(this.MetadataPath, 'downloads') | ||||||
| @ -68,8 +70,7 @@ class DownloadManager { | |||||||
|     var downloadType = options.type || 'singleAudio' |     var downloadType = options.type || 'singleAudio' | ||||||
|     delete options.type |     delete options.type | ||||||
| 
 | 
 | ||||||
|     var filepath = null | 
 | ||||||
|     var filename = null |  | ||||||
|     var fileext = null |     var fileext = null | ||||||
|     var audiobookDirname = Path.basename(audiobook.path) |     var audiobookDirname = Path.basename(audiobook.path) | ||||||
| 
 | 
 | ||||||
| @ -80,18 +81,18 @@ class DownloadManager { | |||||||
|         var firstTrack = audiobook.tracks[0] |         var firstTrack = audiobook.tracks[0] | ||||||
|         audioFileType = firstTrack.ext |         audioFileType = firstTrack.ext | ||||||
|       } |       } | ||||||
|       filename = audiobookDirname + audioFileType |  | ||||||
|       fileext = audioFileType |       fileext = audioFileType | ||||||
|       filepath = Path.join(dlpath, filename) |     } else if (downloadType === 'zip') { | ||||||
|  |       fileext = '.zip' | ||||||
|     } |     } | ||||||
| 
 |     var filename = audiobookDirname + fileext | ||||||
|     var downloadData = { |     var downloadData = { | ||||||
|       id: downloadId, |       id: downloadId, | ||||||
|       audiobookId: audiobook.id, |       audiobookId: audiobook.id, | ||||||
|       type: downloadType, |       type: downloadType, | ||||||
|       options: options, |       options: options, | ||||||
|       dirpath: dlpath, |       dirpath: dlpath, | ||||||
|       fullPath: filepath, |       fullPath: Path.join(dlpath, filename), | ||||||
|       filename, |       filename, | ||||||
|       ext: fileext, |       ext: fileext, | ||||||
|       userId: (client && client.user) ? client.user.id : null, |       userId: (client && client.user) ? client.user.id : null, | ||||||
| @ -99,6 +100,7 @@ class DownloadManager { | |||||||
|     } |     } | ||||||
|     var download = new Download() |     var download = new Download() | ||||||
|     download.setData(downloadData) |     download.setData(downloadData) | ||||||
|  |     download.setTimeoutTimer(this.downloadTimedOut.bind(this)) | ||||||
| 
 | 
 | ||||||
|     if (downloadData.socket) { |     if (downloadData.socket) { | ||||||
|       downloadData.socket.emit('download_started', download.toJSON()) |       downloadData.socket.emit('download_started', download.toJSON()) | ||||||
| @ -106,29 +108,105 @@ class DownloadManager { | |||||||
| 
 | 
 | ||||||
|     if (download.type === 'singleAudio') { |     if (download.type === 'singleAudio') { | ||||||
|       this.processSingleAudioDownload(audiobook, download) |       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) { |   async processSingleAudioDownload(audiobook, download) { | ||||||
| 
 | 
 | ||||||
|     // If changing audio file type then encoding is needed
 |     // 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') |     const ffmpegInputs = [] | ||||||
|     await writeConcatFile(audiobook.tracks, concatFilePath) |  | ||||||
| 
 | 
 | ||||||
|     const ffmpegInputs = [ |     if (!isOneTrack) { | ||||||
|       { |       var concatFilePath = Path.join(download.dirpath, 'files.txt') | ||||||
|  |       await writeConcatFile(audiobook.tracks, concatFilePath) | ||||||
|  |       ffmpegInputs.push({ | ||||||
|         input: concatFilePath, |         input: concatFilePath, | ||||||
|         options: ['-safe 0', '-f concat'] |         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' |     const logLevel = process.env.NODE_ENV === 'production' ? 'error' : 'warning' | ||||||
|     var ffmpegOptions = [`-loglevel ${logLevel}`] |     var ffmpegOptions = [`-loglevel ${logLevel}`] | ||||||
|     var ffmpegOutputOptions = [] |     var ffmpegOutputOptions = [] | ||||||
| 
 | 
 | ||||||
|     if (requiresEncode) { |     if (audioRequiresEncode) { | ||||||
|       ffmpegOptions = ffmpegOptions.concat([ |       ffmpegOptions = ffmpegOptions.concat([ | ||||||
|         '-map 0:a', |         '-map 0:a', | ||||||
|         '-acodec aac', |         '-acodec aac', | ||||||
| @ -137,12 +215,18 @@ class DownloadManager { | |||||||
|         '-id3v2_version 3' |         '-id3v2_version 3' | ||||||
|       ]) |       ]) | ||||||
|     } else { |     } else { | ||||||
|       ffmpegOptions.push('-c copy') |       ffmpegOptions.push('-max_muxing_queue_size 1000') | ||||||
|       if (download.ext === '.m4b') { | 
 | ||||||
|         Logger.info('Concat m4b\'s use -f mp4') |       if (isOneTrack && firstTrackIsM4b && !shouldIncludeCover) { | ||||||
|         ffmpegOutputOptions.push('-f mp4') |         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) { |     if (download.includeMetadata) { | ||||||
|       var metadataFilePath = Path.join(download.dirpath, 'metadata.txt') |       var metadataFilePath = Path.join(download.dirpath, 'metadata.txt') | ||||||
| @ -155,9 +239,14 @@ class DownloadManager { | |||||||
|       ffmpegOptions.push('-map_metadata 1') |       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({ |       ffmpegInputs.push({ | ||||||
|         input: audiobook.book.cover, |         input: _cover, | ||||||
|         options: ['-f image2pipe'] |         options: ['-f image2pipe'] | ||||||
|       }) |       }) | ||||||
|       ffmpegOptions.push('-vf [2:v]crop=trunc(iw/2)*2:trunc(ih/2)*2') |       ffmpegOptions.push('-vf [2:v]crop=trunc(iw/2)*2:trunc(ih/2)*2') | ||||||
| @ -175,7 +264,9 @@ class DownloadManager { | |||||||
|     worker.on('message', (message) => { |     worker.on('message', (message) => { | ||||||
|       if (message != null && typeof message === 'object') { |       if (message != null && typeof message === 'object') { | ||||||
|         if (message.type === 'RESULT') { |         if (message.type === 'RESULT') { | ||||||
|           this.sendResult(download, message) |           if (!download.isTimedOut) { | ||||||
|  |             this.sendResult(download, message) | ||||||
|  |           } | ||||||
|         } |         } | ||||||
|       } else { |       } else { | ||||||
|         Logger.error('Invalid worker message', message) |         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) { |   async downloadExpired(download) { | ||||||
|     Logger.info(`[DownloadManager] Download ${download.id} expired`) |     Logger.info(`[DownloadManager] Download ${download.id} expired`) | ||||||
| 
 | 
 | ||||||
| @ -198,6 +300,8 @@ class DownloadManager { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async sendResult(download, result) { |   async sendResult(download, result) { | ||||||
|  |     download.clearTimeoutTimer() | ||||||
|  | 
 | ||||||
|     // Remove pending download
 |     // Remove pending download
 | ||||||
|     this.pendingDownloads = this.pendingDownloads.filter(d => d.id !== download.id) |     this.pendingDownloads = this.pendingDownloads.filter(d => d.id !== download.id) | ||||||
| 
 | 
 | ||||||
| @ -216,18 +320,8 @@ class DownloadManager { | |||||||
|       return |       return | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Remove files.txt if it was used
 |     var filesize = await getFileSize(download.fullPath) | ||||||
|     // if (download.type === 'singleAudio') {
 |     download.setComplete(filesize) | ||||||
|     //   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) |  | ||||||
|     if (download.socket) { |     if (download.socket) { | ||||||
|       download.socket.emit('download_ready', download.toJSON()) |       download.socket.emit('download_ready', download.toJSON()) | ||||||
|     } |     } | ||||||
| @ -240,15 +334,20 @@ class DownloadManager { | |||||||
|   async removeDownload(download) { |   async removeDownload(download) { | ||||||
|     Logger.info('[DownloadManager] Removing download ' + download.id) |     Logger.info('[DownloadManager] Removing download ' + download.id) | ||||||
| 
 | 
 | ||||||
|  |     download.clearTimeoutTimer() | ||||||
|  |     download.clearExpirationTimer() | ||||||
|  | 
 | ||||||
|     var pendingDl = this.pendingDownloads.find(d => d.id === download.id) |     var pendingDl = this.pendingDownloads.find(d => d.id === download.id) | ||||||
| 
 | 
 | ||||||
|     if (pendingDl) { |     if (pendingDl) { | ||||||
|       this.pendingDownloads = this.pendingDownloads.filter(d => d.id !== download.id) |       this.pendingDownloads = this.pendingDownloads.filter(d => d.id !== download.id) | ||||||
|       Logger.warn(`[DownloadManager] Removing download in progress - stopping worker`) |       Logger.warn(`[DownloadManager] Removing download in progress - stopping worker`) | ||||||
|       try { |       if (pendingDl.worker) { | ||||||
|         pendingDl.worker.postMessage('STOP') |         try { | ||||||
|       } catch (error) { |           pendingDl.worker.postMessage('STOP') | ||||||
|         Logger.error('[DownloadManager] Error posting stop message to worker', error) |         } 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.scanner = new Scanner(this.AudiobookPath, this.MetadataPath, this.db, this.emitter.bind(this)) | ||||||
|     this.streamManager = new StreamManager(this.db, this.MetadataPath) |     this.streamManager = new StreamManager(this.db, this.MetadataPath) | ||||||
|     this.rssFeeds = new RssFeeds(this.Port, this.db) |     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.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) |     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) |     return this.audioFiles.find(af => af.ino === ino) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   setChaptersFromAudioFile(audioFile) { |  | ||||||
|     if (!audioFile.chapters) return [] |  | ||||||
|     return audioFile.chapters.map(c => ({ ...c })) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   setChapters() { |   setChapters() { | ||||||
|     if (this.audioFiles.length === 1) { |     // If 1 audio file without chapters, then no chapters will be set
 | ||||||
|       if (this.audioFiles[0].chapters) { | 
 | ||||||
|         this.chapters = this.audioFiles[0].chapters.map(c => ({ ...c })) |     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 { |     } else { | ||||||
|       this.chapters = [] |       this.chapters = [] | ||||||
|       var currTrackId = 0 |       var currChapterId = 0 | ||||||
|       var currStartTime = 0 |       var currStartTime = 0 | ||||||
|       this.tracks.forEach((track) => { |       includedAudioFiles.forEach((file) => { | ||||||
|         this.chapters.push({ |         // If audio file has chapters use chapters
 | ||||||
|           id: currTrackId++, |         if (file.chapters && file.chapters.length) { | ||||||
|           start: currStartTime, |           file.chapters.forEach((chapter) => { | ||||||
|           end: currStartTime + track.duration, |             var chapterDuration = chapter.end - chapter.start | ||||||
|           title: `Chapter ${currTrackId}` |             if (chapterDuration > 0) { | ||||||
|         }) |               var title = `Chapter ${currChapterId}` | ||||||
|         currStartTime += track.duration |               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_EXPIRATION = 1000 * 60 * 60 // 60 minutes
 | ||||||
| 
 | const DEFAULT_TIMEOUT = 1000 * 60 * 15 // 15 minutes
 | ||||||
| class Download { | class Download { | ||||||
|   constructor(download) { |   constructor(download) { | ||||||
|     this.id = null |     this.id = null | ||||||
| @ -16,12 +16,17 @@ class Download { | |||||||
|     this.userId = null |     this.userId = null | ||||||
|     this.socket = null // Socket to notify when complete
 |     this.socket = null // Socket to notify when complete
 | ||||||
|     this.isReady = false |     this.isReady = false | ||||||
|  |     this.isTimedOut = false | ||||||
| 
 | 
 | ||||||
|     this.startedAt = null |     this.startedAt = null | ||||||
|     this.finishedAt = null |     this.finishedAt = null | ||||||
|     this.expiresAt = null |     this.expiresAt = null | ||||||
| 
 | 
 | ||||||
|     this.expirationTimeMs = 0 |     this.expirationTimeMs = 0 | ||||||
|  |     this.timeoutTimeMs = 0 | ||||||
|  | 
 | ||||||
|  |     this.timeoutTimer = null | ||||||
|  |     this.expirationTimer = null | ||||||
| 
 | 
 | ||||||
|     if (download) { |     if (download) { | ||||||
|       this.construct(download) |       this.construct(download) | ||||||
| @ -88,6 +93,8 @@ class Download { | |||||||
|     this.finishedAt = download.finishedAt || null |     this.finishedAt = download.finishedAt || null | ||||||
| 
 | 
 | ||||||
|     this.expirationTimeMs = download.expirationTimeMs || DEFAULT_EXPIRATION |     this.expirationTimeMs = download.expirationTimeMs || DEFAULT_EXPIRATION | ||||||
|  |     this.timeoutTimeMs = download.timeoutTimeMs || DEFAULT_TIMEOUT | ||||||
|  | 
 | ||||||
|     this.expiresAt = download.expiresAt || null |     this.expiresAt = download.expiresAt || null | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -105,11 +112,28 @@ class Download { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   setExpirationTimer(callback) { |   setExpirationTimer(callback) { | ||||||
|     setTimeout(() => { |     this.expirationTimer = setTimeout(() => { | ||||||
|       if (callback) { |       if (callback) { | ||||||
|         callback(this) |         callback(this) | ||||||
|       } |       } | ||||||
|     }, this.expirationTimeMs) |     }, 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 | module.exports = Download | ||||||
| @ -113,7 +113,7 @@ function parseMediaStreamInfo(stream, all_streams, total_bit_rate) { | |||||||
| function parseChapters(chapters) { | function parseChapters(chapters) { | ||||||
|   if (!chapters) return [] |   if (!chapters) return [] | ||||||
|   return chapters.map(chap => { |   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 |     var timebase = chap.time_base && chap.time_base.includes('/') ? Number(chap.time_base.split('/')[1]) : 1 | ||||||
|     return { |     return { | ||||||
|       id: chap.id, |       id: chap.id, | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user