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