mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-31 02:17:01 -04:00 
			
		
		
		
	Add:Support multiple book upload #248
This commit is contained in:
		
							parent
							
								
									907790fe92
								
							
						
					
					
						commit
						aa675422a9
					
				
							
								
								
									
										120
									
								
								client/components/cards/BookUploadCard.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								client/components/cards/BookUploadCard.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,120 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="relative w-full py-4 px-6 border border-white border-opacity-10 shadow-lg rounded-md my-6"> | ||||||
|  |     <div class="absolute -top-3 -left-3 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full"> | ||||||
|  |       <p class="text-base text-white text-opacity-80 font-mono">#{{ book.index }}</p> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <div v-if="!processing && !uploadFailed && !uploadSuccess" class="absolute -top-3 -right-3 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full hover:bg-error cursor-pointer" @click="$emit('remove')"> | ||||||
|  |       <span class="text-base text-white text-opacity-80 font-mono material-icons">close</span> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <template v-if="!uploadSuccess && !uploadFailed"> | ||||||
|  |       <widgets-alert v-if="error" type="error"> | ||||||
|  |         <p class="text-base">{{ error }}</p> | ||||||
|  |       </widgets-alert> | ||||||
|  | 
 | ||||||
|  |       <div class="flex my-2 -mx-2"> | ||||||
|  |         <div class="w-1/2 px-2"> | ||||||
|  |           <ui-text-input-with-label v-model="bookData.title" :disabled="processing" label="Title" @input="titleUpdated" /> | ||||||
|  |         </div> | ||||||
|  |         <div class="w-1/2 px-2"> | ||||||
|  |           <ui-text-input-with-label v-model="bookData.author" :disabled="processing" label="Author" /> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |       <div class="flex my-2 -mx-2"> | ||||||
|  |         <div class="w-1/2 px-2"> | ||||||
|  |           <ui-text-input-with-label v-model="bookData.series" :disabled="processing" label="Series" note="(optional)" /> | ||||||
|  |         </div> | ||||||
|  |         <div class="w-1/2 px-2"> | ||||||
|  |           <div class="w-full"> | ||||||
|  |             <p class="px-1 text-sm font-semibold">Directory <em class="font-normal text-xs pl-2">(auto)</em></p> | ||||||
|  |             <ui-text-input :value="directory" disabled class="w-full font-mono text-xs" style="height: 38px" /> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <tables-uploaded-files-table :files="book.bookFiles" title="Book Files" class="mt-8" /> | ||||||
|  |       <tables-uploaded-files-table v-if="book.otherFiles.length" title="Other Files" :files="book.otherFiles" /> | ||||||
|  |       <tables-uploaded-files-table v-if="book.ignoredFiles.length" title="Ignored Files" :files="book.ignoredFiles" /> | ||||||
|  |     </template> | ||||||
|  |     <widgets-alert v-if="uploadSuccess" type="success"> | ||||||
|  |       <p class="text-base">Successfully Uploaded!</p> | ||||||
|  |     </widgets-alert> | ||||||
|  |     <widgets-alert v-if="uploadFailed" type="error"> | ||||||
|  |       <p class="text-base">Failed to upload</p> | ||||||
|  |     </widgets-alert> | ||||||
|  | 
 | ||||||
|  |     <div v-if="isUploading" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 flex items-center justify-center z-20"> | ||||||
|  |       <ui-loading-indicator text="Uploading..." /> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import Path from 'path' | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   props: { | ||||||
|  |     book: { | ||||||
|  |       type: Object, | ||||||
|  |       default: () => {} | ||||||
|  |     }, | ||||||
|  |     processing: Boolean | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       bookData: { | ||||||
|  |         title: '', | ||||||
|  |         author: '', | ||||||
|  |         series: '' | ||||||
|  |       }, | ||||||
|  |       error: '', | ||||||
|  |       isUploading: false, | ||||||
|  |       uploadFailed: false, | ||||||
|  |       uploadSuccess: false | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     directory() { | ||||||
|  |       if (!this.bookData.title) return '' | ||||||
|  |       if (this.bookData.series && this.bookData.author) { | ||||||
|  |         return Path.join(this.bookData.author, this.bookData.series, this.bookData.title) | ||||||
|  |       } else if (this.bookData.author) { | ||||||
|  |         return Path.join(this.bookData.author, this.bookData.title) | ||||||
|  |       } else { | ||||||
|  |         return this.bookData.title | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     setUploadStatus(status) { | ||||||
|  |       this.isUploading = status === 'uploading' | ||||||
|  |       this.uploadFailed = status === 'failed' | ||||||
|  |       this.uploadSuccess = status === 'success' | ||||||
|  |     }, | ||||||
|  |     titleUpdated() { | ||||||
|  |       this.error = '' | ||||||
|  |     }, | ||||||
|  |     getData() { | ||||||
|  |       if (!this.bookData.title) { | ||||||
|  |         this.error = 'Must have a title' | ||||||
|  |         return null | ||||||
|  |       } | ||||||
|  |       this.error = '' | ||||||
|  |       var files = this.book.bookFiles.concat(this.book.otherFiles) | ||||||
|  |       return { | ||||||
|  |         index: this.book.index, | ||||||
|  |         ...this.bookData, | ||||||
|  |         files | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   mounted() { | ||||||
|  |     if (this.book) { | ||||||
|  |       this.bookData.title = this.book.title | ||||||
|  |       this.bookData.author = this.book.author | ||||||
|  |       this.bookData.series = this.book.series | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
							
								
								
									
										60
									
								
								client/components/tables/UploadedFilesTable.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								client/components/tables/UploadedFilesTable.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,60 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="w-full my-4" @mousedown.prevent @mouseup.prevent> | ||||||
|  |     <div class="w-full bg-primary px-6 py-1 flex items-center cursor-pointer" @click.stop="clickBar"> | ||||||
|  |       <p class="pr-4">{{ title }}</p> | ||||||
|  |       <span class="bg-black-400 rounded-xl py-0.5 px-2 text-sm font-mono">{{ files.length }}</span> | ||||||
|  |       <div class="flex-grow" /> | ||||||
|  |       <div class="cursor-pointer h-9 w-9 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="expand ? 'transform rotate-180' : ''"> | ||||||
|  |         <span class="material-icons text-3xl">expand_more</span> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |     <transition name="slide"> | ||||||
|  |       <div class="w-full" v-show="expand"> | ||||||
|  |         <table class="text-sm tracksTable"> | ||||||
|  |           <tr class="font-book"> | ||||||
|  |             <th class="text-left">Filename</th> | ||||||
|  |             <th class="text-left">Size</th> | ||||||
|  |             <th class="text-left">Type</th> | ||||||
|  |           </tr> | ||||||
|  |           <template v-for="file in files"> | ||||||
|  |             <tr :key="file.path"> | ||||||
|  |               <td class="font-book pl-2"> | ||||||
|  |                 {{ file.name }} | ||||||
|  |               </td> | ||||||
|  |               <td class="font-mono"> | ||||||
|  |                 {{ $bytesPretty(file.size) }} | ||||||
|  |               </td> | ||||||
|  |               <td class="font-book"> | ||||||
|  |                 {{ file.filetype }} | ||||||
|  |               </td> | ||||||
|  |             </tr> | ||||||
|  |           </template> | ||||||
|  |         </table> | ||||||
|  |       </div> | ||||||
|  |     </transition> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   props: { | ||||||
|  |     title: String, | ||||||
|  |     files: { | ||||||
|  |       type: Array, | ||||||
|  |       default: () => [] | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       expand: false | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: {}, | ||||||
|  |   methods: { | ||||||
|  |     clickBar() { | ||||||
|  |       this.expand = !this.expand | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   mounted() {} | ||||||
|  | } | ||||||
|  | </script> | ||||||
							
								
								
									
										33
									
								
								client/components/widgets/Alert.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								client/components/widgets/Alert.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="w-full bg-opacity-5 border border-opacity-60 rounded-lg flex items-center relative py-4 pl-16" :class="wrapperClass"> | ||||||
|  |     <div class="absolute top-0 left-4 h-full flex items-center"> | ||||||
|  |       <span class="material-icons-outlined text-2xl">{{ icon }}</span> | ||||||
|  |     </div> | ||||||
|  |     <slot /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   props: { | ||||||
|  |     type: { | ||||||
|  |       type: String, | ||||||
|  |       default: 'error' | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return {} | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     icon() { | ||||||
|  |       if (this.type === 'error' || this.type === 'warning') return 'report' | ||||||
|  |       return 'info' | ||||||
|  |     }, | ||||||
|  |     wrapperClass() { | ||||||
|  |       return `bg-${this.type} border-${this.type} text-${this.type}` | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: {}, | ||||||
|  |   mounted() {} | ||||||
|  | } | ||||||
|  | </script> | ||||||
							
								
								
									
										230
									
								
								client/mixins/uploadHelpers.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										230
									
								
								client/mixins/uploadHelpers.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,230 @@ | |||||||
|  | import Path from 'path' | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       uploadHelpers: { | ||||||
|  |         getBooksFromDrop: this.getBooksFromDataTransferItems, | ||||||
|  |         getBooksFromPicker: this.getBooksFromFileList | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     checkFileType(filename) { | ||||||
|  |       var ext = Path.extname(filename) | ||||||
|  |       if (!ext) return false | ||||||
|  |       if (ext.startsWith('.')) ext = ext.slice(1) | ||||||
|  |       ext = ext.toLowerCase() | ||||||
|  | 
 | ||||||
|  |       for (const filetype in this.$constants.SupportedFileTypes) { | ||||||
|  |         if (this.$constants.SupportedFileTypes[filetype].includes(ext)) { | ||||||
|  |           return filetype | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       return false | ||||||
|  |     }, | ||||||
|  |     filterAudiobookFiles(files) { | ||||||
|  |       var validBookFiles = [] | ||||||
|  |       var validOtherFiles = [] | ||||||
|  |       var ignoredFiles = [] | ||||||
|  |       files.forEach((file) => { | ||||||
|  |         var filetype = this.checkFileType(file.name) | ||||||
|  |         if (!filetype) ignoredFiles.push(file) | ||||||
|  |         else { | ||||||
|  |           file.filetype = filetype | ||||||
|  |           if (filetype === 'audio' || filetype === 'ebook') validBookFiles.push(file) | ||||||
|  |           else validOtherFiles.push(file) | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  | 
 | ||||||
|  |       return { | ||||||
|  |         bookFiles: validBookFiles, | ||||||
|  |         otherFiles: validOtherFiles, | ||||||
|  |         ignoredFiles | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     audiobookFromItems(items) { | ||||||
|  |       var { bookFiles, otherFiles, ignoredFiles } = this.filterAudiobookFiles(items) | ||||||
|  |       if (!bookFiles.length) { | ||||||
|  |         ignoredFiles = ignoredFiles.concat(otherFiles) | ||||||
|  |         otherFiles = [] | ||||||
|  |       } | ||||||
|  |       return [ | ||||||
|  |         { | ||||||
|  |           bookFiles, | ||||||
|  |           otherFiles, | ||||||
|  |           ignoredFiles | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|  |     }, | ||||||
|  |     traverseForAudiobook(folder, depth = 1) { | ||||||
|  |       if (folder.items.some((f) => f.isDirectory)) { | ||||||
|  |         var audiobooks = [] | ||||||
|  |         folder.items.forEach((file) => { | ||||||
|  |           if (file.isDirectory) { | ||||||
|  |             var audiobookResults = this.traverseForAudiobook(file, ++depth) | ||||||
|  |             audiobooks = audiobooks.concat(audiobookResults) | ||||||
|  |           } | ||||||
|  |         }) | ||||||
|  |         return audiobooks | ||||||
|  |       } else { | ||||||
|  |         return this.audiobookFromItems(folder.items) | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     fileTreeToAudiobooks(filetree) { | ||||||
|  |       // Has directores - Is Multi Book Drop
 | ||||||
|  |       if (filetree.some((f) => f.isDirectory)) { | ||||||
|  |         var ignoredFilesInRoot = filetree.filter((f) => !f.isDirectory) | ||||||
|  |         if (ignoredFilesInRoot.length) filetree = filetree.filter((f) => f.isDirectory) | ||||||
|  | 
 | ||||||
|  |         var audiobookResults = this.traverseForAudiobook({ items: filetree }) | ||||||
|  |         return { | ||||||
|  |           audiobooks: audiobookResults, | ||||||
|  |           ignoredFiles: ignoredFilesInRoot | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         // Single Book drop
 | ||||||
|  |         return { | ||||||
|  |           audiobooks: this.audiobookFromItems(filetree), | ||||||
|  |           ignoredFiles: [] | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     getFilesDropped(dataTransferItems) { | ||||||
|  |       var treemap = { | ||||||
|  |         path: '/', | ||||||
|  |         items: [] | ||||||
|  |       } | ||||||
|  |       function traverseFileTreePromise(item, currtreemap) { | ||||||
|  |         return new Promise((resolve) => { | ||||||
|  |           if (item.isFile) { | ||||||
|  |             item.file((file) => { | ||||||
|  |               file.filepath = currtreemap.path + file.name //save full path
 | ||||||
|  |               currtreemap.items.push(file) | ||||||
|  |               resolve(file) | ||||||
|  |             }) | ||||||
|  |           } else if (item.isDirectory) { | ||||||
|  |             let dirReader = item.createReader() | ||||||
|  |             currtreemap.items.push({ | ||||||
|  |               isDirectory: true, | ||||||
|  |               dirname: item.name, | ||||||
|  |               path: currtreemap.path + item.name + '/', | ||||||
|  |               items: [] | ||||||
|  |             }) | ||||||
|  |             var newtreemap = currtreemap.items[currtreemap.items.length - 1] | ||||||
|  |             dirReader.readEntries((entries) => { | ||||||
|  |               let entriesPromises = [] | ||||||
|  |               for (let entr of entries) entriesPromises.push(traverseFileTreePromise(entr, newtreemap)) | ||||||
|  |               resolve(Promise.all(entriesPromises)) | ||||||
|  |             }) | ||||||
|  |           } | ||||||
|  |         }) | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return new Promise((resolve, reject) => { | ||||||
|  |         let entriesPromises = [] | ||||||
|  |         for (let it of dataTransferItems) { | ||||||
|  |           var filetree = traverseFileTreePromise(it.webkitGetAsEntry(), treemap) | ||||||
|  |           entriesPromises.push(filetree) | ||||||
|  |         } | ||||||
|  |         Promise.all(entriesPromises).then(() => { | ||||||
|  |           resolve(treemap.items) | ||||||
|  |         }) | ||||||
|  |       }) | ||||||
|  |     }, | ||||||
|  |     cleanBook(book, index) { | ||||||
|  |       var audiobook = { | ||||||
|  |         index, | ||||||
|  |         title: '', | ||||||
|  |         author: '', | ||||||
|  |         series: '', | ||||||
|  |         ...book | ||||||
|  |       } | ||||||
|  |       var firstBookFile = book.bookFiles[0] | ||||||
|  |       if (!firstBookFile.filepath) return audiobook // No path
 | ||||||
|  | 
 | ||||||
|  |       var firstBookPath = Path.dirname(firstBookFile.filepath) | ||||||
|  | 
 | ||||||
|  |       var dirs = firstBookPath.split('/').filter(d => !!d && d !== '.') | ||||||
|  |       if (dirs.length) { | ||||||
|  |         audiobook.title = dirs.pop() | ||||||
|  |         if (dirs.length > 1) { | ||||||
|  |           audiobook.series = dirs.pop() | ||||||
|  |         } | ||||||
|  |         if (dirs.length) { | ||||||
|  |           audiobook.author = dirs.pop() | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       return audiobook | ||||||
|  |     }, | ||||||
|  |     async getBooksFromDataTransferItems(items) { | ||||||
|  |       var files = await this.getFilesDropped(items) | ||||||
|  |       if (!files || !files.length) return { error: 'No files found ' } | ||||||
|  |       var audiobooksData = this.fileTreeToAudiobooks(files) | ||||||
|  |       if (!audiobooksData.audiobooks.length && !audiobooksData.ignoredFiles.length) { | ||||||
|  |         return { error: 'Invalid file drop' } | ||||||
|  |       } | ||||||
|  |       var ignoredFiles = audiobooksData.ignoredFiles | ||||||
|  |       var index = 1 | ||||||
|  |       var books = audiobooksData.audiobooks.filter((ab) => { | ||||||
|  |         if (!ab.bookFiles.length) { | ||||||
|  |           if (ab.otherFiles.length) ignoredFiles = ignoredFiles.concat(ab.otherFiles) | ||||||
|  |           if (ab.ignoredFiles.length) ignoredFiles = ignoredFiles.concat(ab.ignoredFiles) | ||||||
|  |         } | ||||||
|  |         return ab.bookFiles.length | ||||||
|  |       }).map(ab => this.cleanBook(ab, index++)) | ||||||
|  |       return { | ||||||
|  |         books, | ||||||
|  |         invalidBooks, | ||||||
|  |         ignoredFiles | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     getBooksFromFileList(filelist) { | ||||||
|  |       var ignoredFiles = [] | ||||||
|  |       var otherFiles = [] | ||||||
|  | 
 | ||||||
|  |       var bookMap = {} | ||||||
|  | 
 | ||||||
|  |       filelist.forEach((file) => { | ||||||
|  |         var filetype = this.checkFileType(file.name) | ||||||
|  |         if (!filetype) ignoredFiles.push(file) | ||||||
|  |         else { | ||||||
|  |           file.filetype = filetype | ||||||
|  |           if (file.webkitRelativePath) file.filepath = file.webkitRelativePath | ||||||
|  | 
 | ||||||
|  |           if (filetype === 'audio' || filetype === 'ebook') { | ||||||
|  |             var dir = file.filepath ? Path.dirname(file.filepath) : '' | ||||||
|  |             if (!bookMap[dir]) { | ||||||
|  |               bookMap[dir] = { | ||||||
|  |                 path: dir, | ||||||
|  |                 ignoredFiles: [], | ||||||
|  |                 bookFiles: [], | ||||||
|  |                 otherFiles: [] | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |             bookMap[dir].bookFiles.push(file) | ||||||
|  |           } else { | ||||||
|  |             otherFiles.push(file) | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  | 
 | ||||||
|  |       otherFiles.forEach((file) => { | ||||||
|  |         var dir = Path.dirname(file.filepath) | ||||||
|  |         var findBook = Object.values(bookMap).find(b => dir.startsWith(b.path)) | ||||||
|  |         if (findBook) { | ||||||
|  |           bookMap[dir].otherFiles.push(file) | ||||||
|  |         } else { | ||||||
|  |           ignoredFiles.push(file) | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  | 
 | ||||||
|  |       var index = 1 | ||||||
|  |       var books = Object.values(bookMap).map(ab => this.cleanBook(ab, index++)) | ||||||
|  |       return { | ||||||
|  |         books, | ||||||
|  |         ignoredFiles: ignoredFiles | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -1,161 +1,99 @@ | |||||||
| <template> | <template> | ||||||
|   <div id="page-wrapper" class="page p-0 sm:p-6 overflow-y-auto" :class="streamAudiobook ? 'streaming' : ''"> |   <div id="page-wrapper" class="page p-0 sm:p-6 overflow-y-auto" :class="streamAudiobook ? 'streaming' : ''"> | ||||||
|     <main class="md:container mx-auto md:h-full max-w-screen-lg p-0 md:p-6"> |     <div class="w-full max-w-6xl mx-auto"> | ||||||
|       <article class="max-h-full md:overflow-y-auto relative flex flex-col rounded-md" @drop="drop" @dragover="dragover" @dragleave="dragleave" @dragenter="dragenter"> |       <!-- Library & folder picker --> | ||||||
|         <h1 class="text-xl font-book px-8 pt-4 pb-2">Audiobook Uploader</h1> |       <div class="flex my-6 -mx-2"> | ||||||
| 
 |  | ||||||
|         <div class="flex my-2 px-6"> |  | ||||||
|         <div class="w-1/3 px-2"> |         <div class="w-1/3 px-2"> | ||||||
|             <!-- <ui-text-input-with-label v-model="title" label="Title" /> --> |           <ui-dropdown v-model="selectedLibraryId" :items="libraryItems" label="Library" :disabled="processing" @input="libraryChanged" /> | ||||||
|             <ui-dropdown v-model="selectedLibraryId" :items="libraryItems" label="Library" @input="libraryChanged" /> |  | ||||||
|         </div> |         </div> | ||||||
|         <div class="w-2/3 px-2"> |         <div class="w-2/3 px-2"> | ||||||
|             <ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="!selectedLibraryId" label="Folder" /> |           <ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="!selectedLibraryId || processing" label="Folder" /> | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|         <div class="flex my-2 px-6"> |  | ||||||
|           <div class="w-1/2 px-2"> |  | ||||||
|             <ui-text-input-with-label v-model="title" label="Title" /> |  | ||||||
|           </div> |  | ||||||
|           <div class="w-1/2 px-2"> |  | ||||||
|             <ui-text-input-with-label v-model="author" label="Author" /> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|         <div class="flex my-2 px-6"> |  | ||||||
|           <div class="w-1/2 px-2"> |  | ||||||
|             <ui-text-input-with-label v-model="series" label="Series" note="(optional)" /> |  | ||||||
|           </div> |  | ||||||
|           <div class="w-1/2 px-2"> |  | ||||||
|             <div class="w-full"> |  | ||||||
|               <p class="px-1 text-sm font-semibold">Directory <em class="font-normal text-xs pl-2">(auto)</em></p> |  | ||||||
|               <ui-text-input :value="directory" disabled class="w-full font-mono text-xs" style="height: 42px" /> |  | ||||||
|             </div> |  | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|         <section v-if="showUploader" class="h-full overflow-auto p-8 w-full flex flex-col"> |       <widgets-alert v-if="error" type="error"> | ||||||
|           <header class="border-dashed border-2 border-gray-400 py-12 flex flex-col justify-center items-center relative h-40" :class="isDragOver ? 'bg-white bg-opacity-10' : ''"> |         <p class="text-lg">{{ error }}</p> | ||||||
|             <p v-show="isDragOver" class="mb-3 font-semibold text-gray-200 flex flex-wrap justify-center">Drop em'</p> |       </widgets-alert> | ||||||
|             <p v-show="!isDragOver" class="mb-3 font-semibold text-gray-200 flex flex-wrap justify-center">Drop your audio and image files or</p> | 
 | ||||||
|  |       <!-- Picker display --> | ||||||
|  |       <div v-if="!books.length && !ignoredFiles.length" class="w-full mx-auto border border-white border-opacity-20 px-12 pt-12 pb-4 my-12 relative" :class="isDragging ? 'bg-primary bg-opacity-40' : 'border-dashed'"> | ||||||
|  |         <p class="text-2xl text-center">{{ isDragging ? 'Drop files' : "Drag n' drop files or folders" }}</p> | ||||||
|  |         <p class="text-center text-sm my-5">or</p> | ||||||
|  |         <div class="w-full max-w-xl mx-auto"> | ||||||
|  |           <div class="flex"> | ||||||
|  |             <ui-btn class="w-full mx-1" @click="openFilePicker">Choose files</ui-btn> | ||||||
|  |             <ui-btn class="w-full mx-1" @click="openFolderPicker">Choose a folder</ui-btn> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |         <div class="pt-8 text-center"> | ||||||
|  |           <p class="text-xs text-white text-opacity-50 font-mono"><strong>Supported File Types: </strong>{{ inputAccept.join(', ') }}</p> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |       <!-- Book list header --> | ||||||
|  |       <div v-else class="w-full flex items-center pb-4 border-b border-white border-opacity-10"> | ||||||
|  |         <p class="text-lg">{{ books.length }} book{{ books.length === 1 ? '' : 's' }}</p> | ||||||
|  |         <p v-if="ignoredFiles.length" class="text-lg"> | {{ ignoredFiles.length }} file{{ ignoredFiles.length === 1 ? '' : 's' }} ignored</p> | ||||||
|  |         <div class="flex-grow" /> | ||||||
|  |         <ui-btn :disabled="processing" small @click="reset">Reset</ui-btn> | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <!-- Alerts --> | ||||||
|  |       <widgets-alert v-if="!books.length && !uploadReady" type="error" class="my-4"> | ||||||
|  |         <p class="text-lg">No books found</p> | ||||||
|  |       </widgets-alert> | ||||||
|  |       <widgets-alert v-if="ignoredFiles.length" type="warning" class="my-4"> | ||||||
|  |         <div class="w-full pr-12"> | ||||||
|  |           <p class="text-base mb-1">Unsupported files are ignored. When choosing or dropping a folder, other files that are not in a book folder are ignored.</p> | ||||||
|  |           <tables-uploaded-files-table :files="ignoredFiles" title="Ignored Files" class="text-white" /> | ||||||
|  |           <p class="text-xs text-white text-opacity-50 font-mono pt-1"><strong>Supported File Types: </strong>{{ inputAccept.join(', ') }}</p> | ||||||
|  |         </div> | ||||||
|  |       </widgets-alert> | ||||||
|  | 
 | ||||||
|  |       <!-- Book Upload cards --> | ||||||
|  |       <template v-for="(book, index) in books"> | ||||||
|  |         <cards-book-upload-card :ref="`bookCard-${book.index}`" :key="index" :book="book" :processing="processing" @remove="removeBook(book)" /> | ||||||
|  |       </template> | ||||||
|  | 
 | ||||||
|  |       <!-- Upload/Reset btns --> | ||||||
|  |       <div v-show="books.length" class="flex justify-end pb-8 pt-4"> | ||||||
|  |         <ui-btn v-if="!uploadFinished" color="success" :loading="processing" @click="submit">Upload</ui-btn> | ||||||
|  |         <ui-btn v-else @click="reset">Reset</ui-btn> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
| 
 | 
 | ||||||
|     <input ref="fileInput" id="hidden-input" type="file" multiple :accept="inputAccept" class="hidden" @change="inputChanged" /> |     <input ref="fileInput" id="hidden-input" type="file" multiple :accept="inputAccept" class="hidden" @change="inputChanged" /> | ||||||
|             <ui-btn @click="clickSelectAudioFiles">Select files</ui-btn> |     <input ref="fileFolderInput" id="hidden-input" type="file" webkitdirectory multiple :accept="inputAccept" class="hidden" @change="inputChanged" /> | ||||||
|             <p class="text-xs text-gray-300 absolute bottom-3 right-3">{{ inputAccept }}</p> |  | ||||||
|           </header> |  | ||||||
|         </section> |  | ||||||
|         <section v-else class="h-full overflow-auto px-8 pb-8 w-full flex flex-col"> |  | ||||||
|           <p v-if="!hasValidAudioFiles" class="text-error text-lg pt-4">* No valid audio tracks</p> |  | ||||||
| 
 |  | ||||||
|           <div v-if="validImageFiles.length"> |  | ||||||
|             <h1 class="pt-8 pb-3 font-semibold sm:text-lg text-gray-200">Cover Image(s)</h1> |  | ||||||
|             <div class="flex"> |  | ||||||
|               <template v-for="file in validImageFiles"> |  | ||||||
|                 <div :key="file.name" class="h-28 w-20 bg-bg"> |  | ||||||
|                   <img :src="file.src" class="h-full w-full object-contain" /> |  | ||||||
|                 </div> |  | ||||||
|               </template> |  | ||||||
|             </div> |  | ||||||
|           </div> |  | ||||||
| 
 |  | ||||||
|           <div v-if="validAudioFiles.length"> |  | ||||||
|             <h1 class="pt-8 pb-3 font-semibold sm:text-lg text-gray-200">Audio Tracks</h1> |  | ||||||
| 
 |  | ||||||
|             <table class="text-sm tracksTable"> |  | ||||||
|               <tr class="font-book"> |  | ||||||
|                 <th class="text-left">Filename</th> |  | ||||||
|                 <th class="text-left">Type</th> |  | ||||||
|                 <th class="text-left">Size</th> |  | ||||||
|               </tr> |  | ||||||
|               <template v-for="file in validAudioFiles"> |  | ||||||
|                 <tr :key="file.name"> |  | ||||||
|                   <td class="font-book"> |  | ||||||
|                     <p class="truncate">{{ file.name }}</p> |  | ||||||
|                   </td> |  | ||||||
|                   <td class="font-sm"> |  | ||||||
|                     {{ file.type }} |  | ||||||
|                   </td> |  | ||||||
|                   <td class="font-mono"> |  | ||||||
|                     {{ $bytesPretty(file.size) }} |  | ||||||
|                   </td> |  | ||||||
|                 </tr> |  | ||||||
|               </template> |  | ||||||
|             </table> |  | ||||||
|           </div> |  | ||||||
| 
 |  | ||||||
|           <div v-if="invalidFiles.length"> |  | ||||||
|             <h1 class="pt-8 pb-3 font-semibold sm:text-lg text-gray-200">Invalid Files</h1> |  | ||||||
|             <table class="text-sm tracksTable"> |  | ||||||
|               <tr class="font-book"> |  | ||||||
|                 <th class="text-left">Filename</th> |  | ||||||
|                 <th class="text-left">Type</th> |  | ||||||
|                 <th class="text-left">Size</th> |  | ||||||
|               </tr> |  | ||||||
|               <template v-for="file in invalidFiles"> |  | ||||||
|                 <tr :key="file.name"> |  | ||||||
|                   <td class="font-book"> |  | ||||||
|                     <p class="truncate">{{ file.name }}</p> |  | ||||||
|                   </td> |  | ||||||
|                   <td class="font-sm"> |  | ||||||
|                     {{ file.type }} |  | ||||||
|                   </td> |  | ||||||
|                   <td class="font-mono"> |  | ||||||
|                     {{ $bytesPretty(file.size) }} |  | ||||||
|                   </td> |  | ||||||
|                 </tr> |  | ||||||
|               </template> |  | ||||||
|             </table> |  | ||||||
|           </div> |  | ||||||
|         </section> |  | ||||||
|         <footer v-show="!showUploader" class="flex justify-end px-8 pb-8 pt-4"> |  | ||||||
|           <ui-btn :disabled="!hasValidAudioFiles" color="success" @click="submit">Upload Audiobook</ui-btn> |  | ||||||
|           <button id="cancel" class="ml-3 rounded-sm px-3 py-1 hover:bg-white hover:bg-opacity-10 focus:shadow-outline focus:outline-none" @click="cancel">Cancel</button> |  | ||||||
|         </footer> |  | ||||||
| 
 |  | ||||||
|         <div v-if="processing" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 flex items-center justify-center z-20"> |  | ||||||
|           <ui-loading-indicator text="Uploading..." /> |  | ||||||
|         </div> |  | ||||||
|       </article> |  | ||||||
|     </main> |  | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script> | <script> | ||||||
| import Path from 'path' | import uploadHelpers from '@/mixins/uploadHelpers' | ||||||
| 
 | 
 | ||||||
| export default { | export default { | ||||||
|  |   mixins: [uploadHelpers], | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       processing: false, |       isDragging: false, | ||||||
|       title: null, |       error: '', | ||||||
|       author: null, |       books: [], | ||||||
|       series: null, |       ignoredFiles: [], | ||||||
|       acceptedAudioFormats: ['.mp3', '.m4b', '.m4a', '.flac', '.opus', '.mp4', '.aac'], |  | ||||||
|       acceptedImageFormats: ['.png', '.jpg', '.jpeg', '.webp'], |  | ||||||
|       inputAccept: '.png, .jpg, .jpeg, .webp, .mp3, .m4b, .m4a, .flac, .opus, .mp4, .aac', |  | ||||||
|       isDragOver: false, |  | ||||||
|       showUploader: true, |  | ||||||
|       validAudioFiles: [], |  | ||||||
|       validImageFiles: [], |  | ||||||
|       invalidFiles: [], |  | ||||||
|       selectedLibraryId: null, |       selectedLibraryId: null, | ||||||
|       selectedFolderId: null |       selectedFolderId: null, | ||||||
|  |       processing: false, | ||||||
|  |       uploadFinished: false | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   computed: { |   computed: { | ||||||
|  |     inputAccept() { | ||||||
|  |       var extensions = [] | ||||||
|  |       Object.values(this.$constants.SupportedFileTypes).forEach((types) => { | ||||||
|  |         extensions = extensions.concat(types.map((t) => `.${t}`)) | ||||||
|  |       }) | ||||||
|  |       return extensions | ||||||
|  |     }, | ||||||
|     streamAudiobook() { |     streamAudiobook() { | ||||||
|       return this.$store.state.streamAudiobook |       return this.$store.state.streamAudiobook | ||||||
|     }, |     }, | ||||||
|     hasValidAudioFiles() { |  | ||||||
|       return this.validAudioFiles.length |  | ||||||
|     }, |  | ||||||
|     directory() { |  | ||||||
|       if (!this.author || !this.title) return '' |  | ||||||
|       if (this.series) { |  | ||||||
|         return Path.join(this.author, this.series, this.title) |  | ||||||
|       } else { |  | ||||||
|         return Path.join(this.author, this.title) |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     libraries() { |     libraries() { | ||||||
|       return this.$store.state.libraries.libraries |       return this.$store.state.libraries.libraries | ||||||
|     }, |     }, | ||||||
| @ -182,6 +120,9 @@ export default { | |||||||
|           text: fold.fullPath |           text: fold.fullPath | ||||||
|         } |         } | ||||||
|       }) |       }) | ||||||
|  |     }, | ||||||
|  |     uploadReady() { | ||||||
|  |       return !this.books.length && !this.ignoredFiles.length && !this.uploadFinished | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
| @ -200,114 +141,171 @@ export default { | |||||||
|         this.selectedFolderId = this.selectedLibrary.folders[0].id |         this.selectedFolderId = this.selectedLibrary.folders[0].id | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     reset() { |     removeBook(book) { | ||||||
|       this.title = '' |       this.books = this.books.filter((b) => b.index !== book.index) | ||||||
|       this.author = '' |       if (!this.books.length) { | ||||||
|       this.series = '' |         this.reset() | ||||||
|       this.cancel() |  | ||||||
|     }, |  | ||||||
|     cancel() { |  | ||||||
|       this.validAudioFiles = [] |  | ||||||
|       this.validImageFiles = [] |  | ||||||
|       this.invalidFiles = [] |  | ||||||
|       if (this.$refs.fileInput) { |  | ||||||
|         this.$refs.fileInput.value = '' |  | ||||||
|       } |       } | ||||||
|       this.showUploader = true |     }, | ||||||
|  |     reset() { | ||||||
|  |       this.error = '' | ||||||
|  |       this.books = [] | ||||||
|  |       this.ignoredFiles = [] | ||||||
|  |       this.uploadFinished = false | ||||||
|  |       if (this.$refs.fileInput) this.$refs.fileInput.value = '' | ||||||
|  |       if (this.$refs.fileFolderInput) this.$refs.fileFolderInput.value = '' | ||||||
|  |     }, | ||||||
|  |     openFilePicker() { | ||||||
|  |       if (this.$refs.fileInput) this.$refs.fileInput.click() | ||||||
|  |     }, | ||||||
|  |     openFolderPicker() { | ||||||
|  |       if (this.$refs.fileFolderInput) this.$refs.fileFolderInput.click() | ||||||
|  |     }, | ||||||
|  |     isDraggingFile(e) { | ||||||
|  |       // Checks dragging file or folder and not an element on the page | ||||||
|  |       var dt = e.dataTransfer || {} | ||||||
|  |       return dt.types && dt.types.indexOf('Files') >= 0 | ||||||
|  |     }, | ||||||
|  |     dragenter(e) { | ||||||
|  |       e.preventDefault() | ||||||
|  |       if (this.uploadReady && this.isDraggingFile(e) && !this.isDragging) { | ||||||
|  |         this.isDragging = true | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     dragleave(e) { | ||||||
|  |       e.preventDefault() | ||||||
|  |       if (!e.fromElement && this.isDragging) { | ||||||
|  |         this.isDragging = false | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     dragover(e) { | ||||||
|  |       // This is required to catch the drop event | ||||||
|  |       e.preventDefault() | ||||||
|  |     }, | ||||||
|  |     async drop(e) { | ||||||
|  |       e.preventDefault() | ||||||
|  |       this.isDragging = false | ||||||
|  |       var items = e.dataTransfer.items || [] | ||||||
|  |       var bookResults = await this.uploadHelpers.getBooksFromDrop(items) | ||||||
|  |       this.setResults(bookResults) | ||||||
|     }, |     }, | ||||||
|     inputChanged(e) { |     inputChanged(e) { | ||||||
|       if (!e.target || !e.target.files) return |       if (!e.target || !e.target.files) return | ||||||
|       var _files = Array.from(e.target.files) |       var _files = Array.from(e.target.files) | ||||||
|       if (_files && _files.length) { |       if (_files && _files.length) { | ||||||
|         this.filesChanged(_files) |         var bookResults = this.uploadHelpers.getBooksFromPicker(_files) | ||||||
|  |         this.setResults(bookResults) | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     drop(evt) { |     setResults(bookResults) { | ||||||
|       this.isDragOver = false |       if (bookResults.error) { | ||||||
|       this.preventDefaults(evt) |         this.error = bookResults.error | ||||||
|       const files = [...evt.dataTransfer.files] |         this.books = [] | ||||||
|       this.filesChanged(files) |         this.ignoredFiles = [] | ||||||
|     }, |  | ||||||
|     dragover(evt) { |  | ||||||
|       this.isDragOver = true |  | ||||||
|       this.preventDefaults(evt) |  | ||||||
|     }, |  | ||||||
|     dragleave(evt) { |  | ||||||
|       this.isDragOver = false |  | ||||||
|       this.preventDefaults(evt) |  | ||||||
|     }, |  | ||||||
|     dragenter(evt) { |  | ||||||
|       this.isDragOver = true |  | ||||||
|       this.preventDefaults(evt) |  | ||||||
|     }, |  | ||||||
|     preventDefaults(e) { |  | ||||||
|       e.preventDefault() |  | ||||||
|       e.stopPropagation() |  | ||||||
|     }, |  | ||||||
|     filesChanged(files) { |  | ||||||
|       this.showUploader = false |  | ||||||
| 
 |  | ||||||
|       for (let i = 0; i < files.length; i++) { |  | ||||||
|         var file = files[i] |  | ||||||
|         var ext = Path.extname(file.name) |  | ||||||
| 
 |  | ||||||
|         if (this.acceptedAudioFormats.includes(ext)) { |  | ||||||
|           this.validAudioFiles.push(file) |  | ||||||
|         } else if (file.type.startsWith('image/')) { |  | ||||||
|           file.src = URL.createObjectURL(file) |  | ||||||
|           this.validImageFiles.push(file) |  | ||||||
|       } else { |       } else { | ||||||
|           this.invalidFiles.push(file) |         this.error = '' | ||||||
|  |         this.books = bookResults.books | ||||||
|  |         this.ignoredFiles = bookResults.ignoredFiles | ||||||
|       } |       } | ||||||
|  |       console.log('Upload results', bookResults) | ||||||
|  |     }, | ||||||
|  |     updateBookCardStatus(index, status) { | ||||||
|  |       var ref = this.$refs[`bookCard-${index}`] | ||||||
|  |       if (ref && ref.length) ref = ref[0] | ||||||
|  |       if (!ref) { | ||||||
|  |         console.error('Book card ref not found', index, this.$refs) | ||||||
|  |       } else { | ||||||
|  |         ref.setUploadStatus(status) | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     clickSelectAudioFiles() { |     uploadBook(book) { | ||||||
|       if (this.$refs.fileInput) { |  | ||||||
|         this.$refs.fileInput.click() |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     submit() { |  | ||||||
|       if (!this.title || !this.author) { |  | ||||||
|         this.$toast.error('Must enter a title and author') |  | ||||||
|         return |  | ||||||
|       } |  | ||||||
|       if (!this.selectedLibraryId || !this.selectedFolderId) { |  | ||||||
|         this.$toast.error('Must select a library and folder') |  | ||||||
|         return |  | ||||||
|       } |  | ||||||
|       this.processing = true |  | ||||||
| 
 |  | ||||||
|       var form = new FormData() |       var form = new FormData() | ||||||
|       form.set('title', this.title) |       form.set('title', book.title) | ||||||
|       form.set('author', this.author) |       form.set('author', book.author) | ||||||
|       form.set('series', this.series) |       form.set('series', book.series) | ||||||
|       form.set('library', this.selectedLibraryId) |       form.set('library', this.selectedLibraryId) | ||||||
|       form.set('folder', this.selectedFolderId) |       form.set('folder', this.selectedFolderId) | ||||||
| 
 | 
 | ||||||
|       var index = 0 |       var index = 0 | ||||||
|       var files = this.validAudioFiles.concat(this.validImageFiles) |       book.files.forEach((file) => { | ||||||
|       files.forEach((file) => { |  | ||||||
|         form.set(`${index++}`, file) |         form.set(`${index++}`, file) | ||||||
|       }) |       }) | ||||||
| 
 | 
 | ||||||
|       this.$axios |       return this.$axios | ||||||
|         .$post('/upload', form) |         .$post('/upload', form) | ||||||
|         .then((data) => { |         .then(() => true) | ||||||
|           this.$toast.success('Audiobook Uploaded Successfully') |  | ||||||
|           this.reset() |  | ||||||
|           this.processing = false |  | ||||||
|         }) |  | ||||||
|         .catch((error) => { |         .catch((error) => { | ||||||
|           console.error('Failed', error) |           console.error('Failed', error) | ||||||
|           var errorMessage = error.response && error.response.data ? error.response.data : 'Oops, something went wrong...' |           var errorMessage = error.response && error.response.data ? error.response.data : 'Oops, something went wrong...' | ||||||
|           this.$toast.error(errorMessage) |           this.$toast.error(errorMessage) | ||||||
|           this.processing = false |           return false | ||||||
|         }) |         }) | ||||||
|  |     }, | ||||||
|  |     validateBooks() { | ||||||
|  |       var bookData = [] | ||||||
|  |       for (var book of this.books) { | ||||||
|  |         var bookref = this.$refs[`bookCard-${book.index}`] | ||||||
|  |         if (bookref && bookref.length) bookref = bookref[0] | ||||||
|  | 
 | ||||||
|  |         if (!bookref) { | ||||||
|  |           console.error('Invalid book index no ref', book.index, this.$refs.bookCard) | ||||||
|  |           return false | ||||||
|  |         } else { | ||||||
|  |           var data = bookref.getData() | ||||||
|  |           if (!data) { | ||||||
|  |             return false | ||||||
|  |           } | ||||||
|  |           bookData.push(data) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       return bookData | ||||||
|  |     }, | ||||||
|  |     async submit() { | ||||||
|  |       if (!this.selectedFolderId || !this.selectedLibraryId) { | ||||||
|  |         this.$toast.error('Must select library and folder') | ||||||
|  |         document.getElementById('page-wrapper').scroll({ top: 0, left: 0, behavior: 'smooth' }) | ||||||
|  |         return | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       var books = this.validateBooks() | ||||||
|  |       if (!books) { | ||||||
|  |         this.$toast.error('Some invalid books') | ||||||
|  |         return | ||||||
|  |       } | ||||||
|  |       this.processing = true | ||||||
|  |       var booksUploaded = 0 | ||||||
|  |       var booksFailed = 0 | ||||||
|  |       for (let i = 0; i < books.length; i++) { | ||||||
|  |         var book = books[i] | ||||||
|  |         this.updateBookCardStatus(book.index, 'uploading') | ||||||
|  |         var result = await this.uploadBook(book) | ||||||
|  |         if (result) booksUploaded++ | ||||||
|  |         else booksFailed++ | ||||||
|  |         this.updateBookCardStatus(book.index, result ? 'success' : 'failed') | ||||||
|  |       } | ||||||
|  |       if (booksUploaded) { | ||||||
|  |         this.$toast.success(`Successfully uploaded ${booksUploaded} book${booksUploaded > 1 ? 's' : ''}`) | ||||||
|  |       } | ||||||
|  |       if (booksFailed) { | ||||||
|  |         this.$toast.success(`Failed to upload ${booksFailed} book${booksFailed > 1 ? 's' : ''}`) | ||||||
|  |       } | ||||||
|  |       this.processing = false | ||||||
|  |       this.uploadFinished = true | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   mounted() { |   mounted() { | ||||||
|     this.selectedLibraryId = this.$store.state.libraries.currentLibraryId |     this.selectedLibraryId = this.$store.state.libraries.currentLibraryId | ||||||
|     this.setDefaultFolder() |     this.setDefaultFolder() | ||||||
|  |     window.addEventListener('dragenter', this.dragenter) | ||||||
|  |     window.addEventListener('dragleave', this.dragleave) | ||||||
|  |     window.addEventListener('dragover', this.dragover) | ||||||
|  |     window.addEventListener('drop', this.drop) | ||||||
|  |   }, | ||||||
|  |   beforeDestroy() { | ||||||
|  |     window.removeEventListener('dragenter', this.dragenter) | ||||||
|  |     window.removeEventListener('dragleave', this.dragleave) | ||||||
|  |     window.removeEventListener('dragover', this.dragover) | ||||||
|  |     window.removeEventListener('drop', this.drop) | ||||||
|   } |   } | ||||||
| } | } | ||||||
| </script> | </script> | ||||||
| @ -1,3 +1,12 @@ | |||||||
|  | const SupportedFileTypes = { | ||||||
|  |   image: ['png', 'jpg', 'jpeg', 'webp'], | ||||||
|  |   audio: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'mp4', 'aac'], | ||||||
|  |   ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'], | ||||||
|  |   info: ['nfo'], | ||||||
|  |   text: ['txt'], | ||||||
|  |   opf: ['opf'] | ||||||
|  | } | ||||||
|  | 
 | ||||||
| const DownloadStatus = { | const DownloadStatus = { | ||||||
|   PENDING: 0, |   PENDING: 0, | ||||||
|   READY: 1, |   READY: 1, | ||||||
| @ -21,6 +30,7 @@ const BookshelfView = { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const Constants = { | const Constants = { | ||||||
|  |   SupportedFileTypes, | ||||||
|   DownloadStatus, |   DownloadStatus, | ||||||
|   CoverDestination, |   CoverDestination, | ||||||
|   BookCoverAspectRatio, |   BookCoverAspectRatio, | ||||||
|  | |||||||
| @ -408,25 +408,27 @@ class Server { | |||||||
| 
 | 
 | ||||||
|     var library = this.db.libraries.find(lib => lib.id === libraryId) |     var library = this.db.libraries.find(lib => lib.id === libraryId) | ||||||
|     if (!library) { |     if (!library) { | ||||||
|       return res.status(500).error(`Library not found with id ${libraryId}`) |       return res.status(500).send(`Library not found with id ${libraryId}`) | ||||||
|     } |     } | ||||||
|     var folder = library.folders.find(fold => fold.id === folderId) |     var folder = library.folders.find(fold => fold.id === folderId) | ||||||
|     if (!folder) { |     if (!folder) { | ||||||
|       return res.status(500).error(`Folder not found with id ${folderId} in library ${library.name}`) |       return res.status(500).send(`Folder not found with id ${folderId} in library ${library.name}`) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (!files.length || !title || !author) { |     if (!files.length || !title) { | ||||||
|       return res.status(500).error(`Invalid post data`) |       return res.status(500).send(`Invalid post data`) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // For setting permissions recursively
 |     // For setting permissions recursively
 | ||||||
|     var firstDirPath = Path.join(folder.fullPath, author) |     var firstDirPath = Path.join(folder.fullPath, author) | ||||||
| 
 | 
 | ||||||
|     var outputDirectory = '' |     var outputDirectory = '' | ||||||
|     if (series && series.length && series !== 'null') { |     if (series && author) { | ||||||
|       outputDirectory = Path.join(folder.fullPath, author, series, title) |       outputDirectory = Path.join(folder.fullPath, author, series, title) | ||||||
|     } else { |     } else if (author) { | ||||||
|       outputDirectory = Path.join(folder.fullPath, author, title) |       outputDirectory = Path.join(folder.fullPath, author, title) | ||||||
|  |     } else { | ||||||
|  |       outputDirectory = Path.join(folder.fullPath, title) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     var exists = await fs.pathExists(outputDirectory) |     var exists = await fs.pathExists(outputDirectory) | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user