From aa675422a9a237c9bf7a15ad1df6cb62300dd4d2 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 26 Feb 2022 16:19:22 -0600 Subject: [PATCH] Add:Support multiple book upload #248 --- client/components/cards/BookUploadCard.vue | 120 +++++ .../components/tables/UploadedFilesTable.vue | 60 +++ client/components/widgets/Alert.vue | 33 ++ client/mixins/uploadHelpers.js | 230 ++++++++++ client/pages/upload/index.vue | 420 +++++++++--------- client/plugins/constants.js | 10 + server/Server.js | 14 +- 7 files changed, 670 insertions(+), 217 deletions(-) create mode 100644 client/components/cards/BookUploadCard.vue create mode 100644 client/components/tables/UploadedFilesTable.vue create mode 100644 client/components/widgets/Alert.vue create mode 100644 client/mixins/uploadHelpers.js diff --git a/client/components/cards/BookUploadCard.vue b/client/components/cards/BookUploadCard.vue new file mode 100644 index 00000000..08d66e2f --- /dev/null +++ b/client/components/cards/BookUploadCard.vue @@ -0,0 +1,120 @@ + + + \ No newline at end of file diff --git a/client/components/tables/UploadedFilesTable.vue b/client/components/tables/UploadedFilesTable.vue new file mode 100644 index 00000000..3f5b52c5 --- /dev/null +++ b/client/components/tables/UploadedFilesTable.vue @@ -0,0 +1,60 @@ + + + \ No newline at end of file diff --git a/client/components/widgets/Alert.vue b/client/components/widgets/Alert.vue new file mode 100644 index 00000000..17f3b66c --- /dev/null +++ b/client/components/widgets/Alert.vue @@ -0,0 +1,33 @@ + + + \ No newline at end of file diff --git a/client/mixins/uploadHelpers.js b/client/mixins/uploadHelpers.js new file mode 100644 index 00000000..4223872f --- /dev/null +++ b/client/mixins/uploadHelpers.js @@ -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 + } + }, + } +} \ No newline at end of file diff --git a/client/pages/upload/index.vue b/client/pages/upload/index.vue index dca805af..c97ef200 100644 --- a/client/pages/upload/index.vue +++ b/client/pages/upload/index.vue @@ -1,161 +1,99 @@ \ No newline at end of file diff --git a/client/plugins/constants.js b/client/plugins/constants.js index 3c5e191a..4ac7a662 100644 --- a/client/plugins/constants.js +++ b/client/plugins/constants.js @@ -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 = { PENDING: 0, READY: 1, @@ -21,6 +30,7 @@ const BookshelfView = { } const Constants = { + SupportedFileTypes, DownloadStatus, CoverDestination, BookCoverAspectRatio, diff --git a/server/Server.js b/server/Server.js index 156fbf74..ecf00a96 100644 --- a/server/Server.js +++ b/server/Server.js @@ -408,25 +408,27 @@ class Server { var library = this.db.libraries.find(lib => lib.id === libraryId) 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) 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) { - return res.status(500).error(`Invalid post data`) + if (!files.length || !title) { + return res.status(500).send(`Invalid post data`) } // For setting permissions recursively var firstDirPath = Path.join(folder.fullPath, author) var outputDirectory = '' - if (series && series.length && series !== 'null') { + if (series && author) { outputDirectory = Path.join(folder.fullPath, author, series, title) - } else { + } else if (author) { outputDirectory = Path.join(folder.fullPath, author, title) + } else { + outputDirectory = Path.join(folder.fullPath, title) } var exists = await fs.pathExists(outputDirectory)