Update uploader to support podcast folder structure

This commit is contained in:
advplyr 2022-04-14 18:24:24 -05:00
parent 5a26b01ffb
commit a62f7a4861
6 changed files with 186 additions and 138 deletions

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="relative w-full py-4 px-6 border border-white border-opacity-10 shadow-lg rounded-md my-6"> <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"> <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> <p class="text-base text-white text-opacity-80 font-mono">#{{ item.index }}</p>
</div> </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')"> <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')">
@ -15,15 +15,19 @@
<div class="flex my-2 -mx-2"> <div class="flex my-2 -mx-2">
<div class="w-1/2 px-2"> <div class="w-1/2 px-2">
<ui-text-input-with-label v-model="bookData.title" :disabled="processing" label="Title" @input="titleUpdated" /> <ui-text-input-with-label v-model="itemData.title" :disabled="processing" label="Title" @input="titleUpdated" />
</div> </div>
<div class="w-1/2 px-2"> <div class="w-1/2 px-2">
<ui-text-input-with-label v-model="bookData.author" :disabled="processing" label="Author" /> <ui-text-input-with-label v-if="!isPodcast" v-model="itemData.author" :disabled="processing" label="Author" />
<div v-else 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>
</div> </div>
<div class="flex my-2 -mx-2"> <div v-if="!isPodcast" class="flex my-2 -mx-2">
<div class="w-1/2 px-2"> <div class="w-1/2 px-2">
<ui-text-input-with-label v-model="bookData.series" :disabled="processing" label="Series" note="(optional)" /> <ui-text-input-with-label v-model="itemData.series" :disabled="processing" label="Series" note="(optional)" />
</div> </div>
<div class="w-1/2 px-2"> <div class="w-1/2 px-2">
<div class="w-full"> <div class="w-full">
@ -33,9 +37,9 @@
</div> </div>
</div> </div>
<tables-uploaded-files-table :files="book.bookFiles" title="Book Files" class="mt-8" /> <tables-uploaded-files-table :files="item.itemFiles" title="Item 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="item.otherFiles.length" title="Other Files" :files="item.otherFiles" />
<tables-uploaded-files-table v-if="book.ignoredFiles.length" title="Ignored Files" :files="book.ignoredFiles" /> <tables-uploaded-files-table v-if="item.ignoredFiles.length" title="Ignored Files" :files="item.ignoredFiles" />
</template> </template>
<widgets-alert v-if="uploadSuccess" type="success"> <widgets-alert v-if="uploadSuccess" type="success">
<p class="text-base">Successfully Uploaded!</p> <p class="text-base">Successfully Uploaded!</p>
@ -55,15 +59,16 @@ import Path from 'path'
export default { export default {
props: { props: {
book: { item: {
type: Object, type: Object,
default: () => {} default: () => {}
}, },
mediaType: String,
processing: Boolean processing: Boolean
}, },
data() { data() {
return { return {
bookData: { itemData: {
title: '', title: '',
author: '', author: '',
series: '' series: ''
@ -75,14 +80,19 @@ export default {
} }
}, },
computed: { computed: {
isPodcast() {
return this.mediaType === 'podcast'
},
directory() { directory() {
if (!this.bookData.title) return '' if (!this.itemData.title) return ''
if (this.bookData.series && this.bookData.author) { if (this.isPodcast) return this.itemData.title
return Path.join(this.bookData.author, this.bookData.series, this.bookData.title)
} else if (this.bookData.author) { if (this.itemData.series && this.itemData.author) {
return Path.join(this.bookData.author, this.bookData.title) return Path.join(this.itemData.author, this.itemData.series, this.itemData.title)
} else if (this.itemData.author) {
return Path.join(this.itemData.author, this.itemData.title)
} else { } else {
return this.bookData.title return this.itemData.title
} }
} }
}, },
@ -96,24 +106,24 @@ export default {
this.error = '' this.error = ''
}, },
getData() { getData() {
if (!this.bookData.title) { if (!this.itemData.title) {
this.error = 'Must have a title' this.error = 'Must have a title'
return null return null
} }
this.error = '' this.error = ''
var files = this.book.bookFiles.concat(this.book.otherFiles) var files = this.item.itemFiles.concat(this.item.otherFiles)
return { return {
index: this.book.index, index: this.item.index,
...this.bookData, ...this.itemData,
files files
} }
} }
}, },
mounted() { mounted() {
if (this.book) { if (this.item) {
this.bookData.title = this.book.title this.itemData.title = this.item.title
this.bookData.author = this.book.author this.itemData.author = this.item.author
this.bookData.series = this.book.series this.itemData.series = this.item.series
} }
} }
} }

View File

@ -4,8 +4,8 @@ export default {
data() { data() {
return { return {
uploadHelpers: { uploadHelpers: {
getBooksFromDrop: this.getBooksFromDataTransferItems, getItemsFromDrop: this.getItemsFromDataTransferItems,
getBooksFromPicker: this.getBooksFromFileList getItemsFromPicker: this.getItemsFromFilelist
} }
} }
}, },
@ -23,8 +23,8 @@ export default {
} }
return false return false
}, },
filterAudiobookFiles(files) { filterItemFiles(files, mediaType) {
var validBookFiles = [] var validItemFiles = []
var validOtherFiles = [] var validOtherFiles = []
var ignoredFiles = [] var ignoredFiles = []
files.forEach((file) => { files.forEach((file) => {
@ -32,60 +32,60 @@ export default {
if (!filetype) ignoredFiles.push(file) if (!filetype) ignoredFiles.push(file)
else { else {
file.filetype = filetype file.filetype = filetype
if (filetype === 'audio' || filetype === 'ebook') validBookFiles.push(file) if (filetype === 'audio' || (filetype === 'ebook' && mediaType === 'book')) validItemFiles.push(file)
else validOtherFiles.push(file) else validOtherFiles.push(file)
} }
}) })
return { return {
bookFiles: validBookFiles, itemFiles: validItemFiles,
otherFiles: validOtherFiles, otherFiles: validOtherFiles,
ignoredFiles ignoredFiles
} }
}, },
audiobookFromItems(items) { itemFromTreeItems(items, mediaType) {
var { bookFiles, otherFiles, ignoredFiles } = this.filterAudiobookFiles(items) var { itemFiles, otherFiles, ignoredFiles } = this.filterItemFiles(items, mediaType)
if (!bookFiles.length) { if (!itemFiles.length) {
ignoredFiles = ignoredFiles.concat(otherFiles) ignoredFiles = ignoredFiles.concat(otherFiles)
otherFiles = [] otherFiles = []
} }
return [ return [
{ {
bookFiles, itemFiles,
otherFiles, otherFiles,
ignoredFiles ignoredFiles
} }
] ]
}, },
traverseForAudiobook(folder, depth = 1) { traverseForItem(folder, mediaType, depth = 1) {
if (folder.items.some((f) => f.isDirectory)) { if (folder.items.some((f) => f.isDirectory)) {
var audiobooks = [] var items = []
folder.items.forEach((file) => { folder.items.forEach((file) => {
if (file.isDirectory) { if (file.isDirectory) {
var audiobookResults = this.traverseForAudiobook(file, ++depth) var itemResults = this.traverseForItem(file, mediaType, ++depth)
audiobooks = audiobooks.concat(audiobookResults) items = items.concat(itemResults)
} }
}) })
return audiobooks return items
} else { } else {
return this.audiobookFromItems(folder.items) return this.itemFromTreeItems(folder.items, mediaType)
} }
}, },
fileTreeToAudiobooks(filetree) { fileTreeToItems(filetree, mediaType) {
// Has directores - Is Multi Book Drop // Has directores - Is Multi Book Drop
if (filetree.some((f) => f.isDirectory)) { if (filetree.some((f) => f.isDirectory)) {
var ignoredFilesInRoot = filetree.filter((f) => !f.isDirectory) var ignoredFilesInRoot = filetree.filter((f) => !f.isDirectory)
if (ignoredFilesInRoot.length) filetree = filetree.filter((f) => f.isDirectory) if (ignoredFilesInRoot.length) filetree = filetree.filter((f) => f.isDirectory)
var audiobookResults = this.traverseForAudiobook({ items: filetree }) var itemResults = this.traverseForItem({ items: filetree }, mediaType)
return { return {
audiobooks: audiobookResults, items: itemResults,
ignoredFiles: ignoredFilesInRoot ignoredFiles: ignoredFilesInRoot
} }
} else { } else {
// Single Book drop // Single Book drop
return { return {
audiobooks: this.audiobookFromItems(filetree), items: this.itemFromTreeItems(filetree, mediaType),
ignoredFiles: [] ignoredFiles: []
} }
} }
@ -140,7 +140,7 @@ export default {
series: '', series: '',
...book ...book
} }
var firstBookFile = book.bookFiles[0] var firstBookFile = book.itemFiles[0]
if (!firstBookFile.filepath) return audiobook // No path if (!firstBookFile.filepath) return audiobook // No path
var firstBookPath = Path.dirname(firstBookFile.filepath) var firstBookPath = Path.dirname(firstBookFile.filepath)
@ -157,32 +157,49 @@ export default {
} }
return audiobook return audiobook
}, },
async getBooksFromDataTransferItems(items) { cleanPodcast(item, index) {
var podcast = {
index,
title: '',
...item
}
var firstAudioFile = podcast.itemFiles[0]
if (!firstAudioFile.filepath) return podcast // No path
var firstPath = Path.dirname(firstAudioFile.filepath)
var dirs = firstPath.split('/').filter(d => !!d && d !== '.')
podcast.title = dirs.length > 1 ? dirs[1] : dirs[0]
return podcast
},
cleanItem(item, mediaType, index) {
if (mediaType === 'podcast') return this.cleanPodcast(item, index)
return this.cleanBook(item, index)
},
async getItemsFromDataTransferItems(items, mediaType) {
var files = await this.getFilesDropped(items) var files = await this.getFilesDropped(items)
if (!files || !files.length) return { error: 'No files found ' } if (!files || !files.length) return { error: 'No files found ' }
var audiobooksData = this.fileTreeToAudiobooks(files) var itemData = this.fileTreeToItems(files, mediaType)
if (!audiobooksData.audiobooks.length && !audiobooksData.ignoredFiles.length) { if (!itemData.items.length && !itemData.ignoredFiles.length) {
return { error: 'Invalid file drop' } return { error: 'Invalid file drop' }
} }
var ignoredFiles = audiobooksData.ignoredFiles var ignoredFiles = itemData.ignoredFiles
var index = 1 var index = 1
var books = audiobooksData.audiobooks.filter((ab) => { var items = itemData.items.filter((ab) => {
if (!ab.bookFiles.length) { if (!ab.itemFiles.length) {
if (ab.otherFiles.length) ignoredFiles = ignoredFiles.concat(ab.otherFiles) if (ab.otherFiles.length) ignoredFiles = ignoredFiles.concat(ab.otherFiles)
if (ab.ignoredFiles.length) ignoredFiles = ignoredFiles.concat(ab.ignoredFiles) if (ab.ignoredFiles.length) ignoredFiles = ignoredFiles.concat(ab.ignoredFiles)
} }
return ab.bookFiles.length return ab.itemFiles.length
}).map(ab => this.cleanBook(ab, index++)) }).map(ab => this.cleanItem(ab, index++))
return { return {
books, items,
ignoredFiles ignoredFiles
} }
}, },
getBooksFromFileList(filelist) { getItemsFromFilelist(filelist, mediaType) {
var ignoredFiles = [] var ignoredFiles = []
var otherFiles = [] var otherFiles = []
var bookMap = {} var itemMap = {}
filelist.forEach((file) => { filelist.forEach((file) => {
var filetype = this.checkFileType(file.name) var filetype = this.checkFileType(file.name)
@ -191,17 +208,17 @@ export default {
file.filetype = filetype file.filetype = filetype
if (file.webkitRelativePath) file.filepath = file.webkitRelativePath if (file.webkitRelativePath) file.filepath = file.webkitRelativePath
if (filetype === 'audio' || filetype === 'ebook') { if (filetype === 'audio' || (filetype === 'ebook' && mediaType === 'book')) {
var dir = file.filepath ? Path.dirname(file.filepath) : '' var dir = file.filepath ? Path.dirname(file.filepath) : ''
if (!bookMap[dir]) { if (!itemMap[dir]) {
bookMap[dir] = { itemMap[dir] = {
path: dir, path: dir,
ignoredFiles: [], ignoredFiles: [],
bookFiles: [], itemFiles: [],
otherFiles: [] otherFiles: []
} }
} }
bookMap[dir].bookFiles.push(file) itemMap[dir].itemFiles.push(file)
} else { } else {
otherFiles.push(file) otherFiles.push(file)
} }
@ -210,18 +227,18 @@ export default {
otherFiles.forEach((file) => { otherFiles.forEach((file) => {
var dir = Path.dirname(file.filepath) var dir = Path.dirname(file.filepath)
var findBook = Object.values(bookMap).find(b => dir.startsWith(b.path)) var findItem = Object.values(itemMap).find(b => dir.startsWith(b.path))
if (findBook) { if (findItem) {
bookMap[dir].otherFiles.push(file) findItem.otherFiles.push(file)
} else { } else {
ignoredFiles.push(file) ignoredFiles.push(file)
} }
}) })
var index = 1 var index = 1
var books = Object.values(bookMap).map(ab => this.cleanBook(ab, index++)) var items = Object.values(itemMap).map(i => this.cleanItem(i, mediaType, index++))
return { return {
books, items,
ignoredFiles: ignoredFiles ignoredFiles: ignoredFiles
} }
}, },

View File

@ -3,11 +3,14 @@
<div class="w-full max-w-6xl mx-auto"> <div class="w-full max-w-6xl mx-auto">
<!-- Library & folder picker --> <!-- Library & folder picker -->
<div class="flex my-6 -mx-2"> <div class="flex my-6 -mx-2">
<div class="w-1/3 px-2"> <div class="w-1/5 px-2">
<ui-dropdown v-model="selectedLibraryId" :items="libraryItems" label="Library" :disabled="processing" @input="libraryChanged" /> <ui-dropdown v-model="selectedLibraryId" :items="libraryItems" label="Library" :disabled="!!items.length" @input="libraryChanged" />
</div> </div>
<div class="w-2/3 px-2"> <div class="w-3/5 px-2">
<ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="!selectedLibraryId || processing" label="Folder" /> <ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="!selectedLibraryId || !!items.length" label="Folder" />
</div>
<div class="w-1/5 px-2">
<ui-text-input-with-label :value="selectedLibraryMediaType" readonly label="Media Type" />
</div> </div>
</div> </div>
@ -16,7 +19,7 @@
</widgets-alert> </widgets-alert>
<!-- Picker display --> <!-- 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'"> <div v-if="!items.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-2xl text-center">{{ isDragging ? 'Drop files' : "Drag n' drop files or folders" }}</p>
<p class="text-center text-sm my-5">or</p> <p class="text-center text-sm my-5">or</p>
<div class="w-full max-w-xl mx-auto"> <div class="w-full max-w-xl mx-auto">
@ -29,33 +32,33 @@
<p class="text-xs text-white text-opacity-50 font-mono"><strong>Supported File Types: </strong>{{ inputAccept.join(', ') }}</p> <p class="text-xs text-white text-opacity-50 font-mono"><strong>Supported File Types: </strong>{{ inputAccept.join(', ') }}</p>
</div> </div>
</div> </div>
<!-- Book list header --> <!-- Item list header -->
<div v-else class="w-full flex items-center pb-4 border-b border-white border-opacity-10"> <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 class="text-lg">{{ items.length }} item{{ items.length === 1 ? '' : 's' }}</p>
<p v-if="ignoredFiles.length" class="text-lg">&nbsp;|&nbsp;{{ ignoredFiles.length }} file{{ ignoredFiles.length === 1 ? '' : 's' }} ignored</p> <p v-if="ignoredFiles.length" class="text-lg">&nbsp;|&nbsp;{{ ignoredFiles.length }} file{{ ignoredFiles.length === 1 ? '' : 's' }} ignored</p>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn :disabled="processing" small @click="reset">Reset</ui-btn> <ui-btn :disabled="processing" small @click="reset">Reset</ui-btn>
</div> </div>
<!-- Alerts --> <!-- Alerts -->
<widgets-alert v-if="!books.length && !uploadReady" type="error" class="my-4"> <widgets-alert v-if="!items.length && !uploadReady" type="error" class="my-4">
<p class="text-lg">No books found</p> <p class="text-lg">No items found</p>
</widgets-alert> </widgets-alert>
<widgets-alert v-if="ignoredFiles.length" type="warning" class="my-4"> <widgets-alert v-if="ignoredFiles.length" type="warning" class="my-4">
<div class="w-full pr-12"> <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> <p class="text-base mb-1">Unsupported files are ignored. When choosing or dropping a folder, other files that are not in an item folder are ignored.</p>
<tables-uploaded-files-table :files="ignoredFiles" title="Ignored Files" class="text-white" /> <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> <p class="text-xs text-white text-opacity-50 font-mono pt-1"><strong>Supported File Types: </strong>{{ inputAccept.join(', ') }}</p>
</div> </div>
</widgets-alert> </widgets-alert>
<!-- Book Upload cards --> <!-- Item Upload cards -->
<template v-for="(book, index) in books"> <template v-for="(item, index) in items">
<cards-book-upload-card :ref="`bookCard-${book.index}`" :key="index" :book="book" :processing="processing" @remove="removeBook(book)" /> <cards-item-upload-card :ref="`itemCard-${item.index}`" :key="index" :media-type="selectedLibraryMediaType" :item="item" :processing="processing" @remove="removeItem(item)" />
</template> </template>
<!-- Upload/Reset btns --> <!-- Upload/Reset btns -->
<div v-show="books.length" class="flex justify-end pb-8 pt-4"> <div v-show="items.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-if="!uploadFinished" color="success" :loading="processing" @click="submit">Upload</ui-btn>
<ui-btn v-else @click="reset">Reset</ui-btn> <ui-btn v-else @click="reset">Reset</ui-btn>
</div> </div>
@ -75,7 +78,7 @@ export default {
return { return {
isDragging: false, isDragging: false,
error: '', error: '',
books: [], items: [],
ignoredFiles: [], ignoredFiles: [],
selectedLibraryId: null, selectedLibraryId: null,
selectedFolderId: null, selectedFolderId: null,
@ -108,6 +111,12 @@ export default {
selectedLibrary() { selectedLibrary() {
return this.libraries.find((lib) => lib.id === this.selectedLibraryId) return this.libraries.find((lib) => lib.id === this.selectedLibraryId)
}, },
selectedLibraryMediaType() {
return this.selectedLibrary ? this.selectedLibrary.mediaType : null
},
selectedLibraryIsPodcast() {
return this.selectedLibraryMediaType === 'podcast'
},
selectedFolder() { selectedFolder() {
if (!this.selectedLibrary) return null if (!this.selectedLibrary) return null
return this.selectedLibrary.folders.find((fold) => fold.id === this.selectedFolderId) return this.selectedLibrary.folders.find((fold) => fold.id === this.selectedFolderId)
@ -122,7 +131,7 @@ export default {
}) })
}, },
uploadReady() { uploadReady() {
return !this.books.length && !this.ignoredFiles.length && !this.uploadFinished return !this.items.length && !this.ignoredFiles.length && !this.uploadFinished
} }
}, },
methods: { methods: {
@ -141,15 +150,15 @@ export default {
this.selectedFolderId = this.selectedLibrary.folders[0].id this.selectedFolderId = this.selectedLibrary.folders[0].id
} }
}, },
removeBook(book) { removeItem(item) {
this.books = this.books.filter((b) => b.index !== book.index) this.items = this.items.filter((b) => b.index !== item.index)
if (!this.books.length) { if (!this.items.length) {
this.reset() this.reset()
} }
}, },
reset() { reset() {
this.error = '' this.error = ''
this.books = [] this.items = []
this.ignoredFiles = [] this.ignoredFiles = []
this.uploadFinished = false this.uploadFinished = false
if (this.$refs.fileInput) this.$refs.fileInput.value = '' if (this.$refs.fileInput) this.$refs.fileInput.value = ''
@ -186,31 +195,31 @@ export default {
e.preventDefault() e.preventDefault()
this.isDragging = false this.isDragging = false
var items = e.dataTransfer.items || [] var items = e.dataTransfer.items || []
var bookResults = await this.uploadHelpers.getBooksFromDrop(items) var itemResults = await this.uploadHelpers.getItemsFromDrop(items)
this.setResults(bookResults) this.setResults(itemResults)
}, },
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) {
var bookResults = this.uploadHelpers.getBooksFromPicker(_files) var itemResults = this.uploadHelpers.getItemsFromPicker(_files, this.selectedLibraryMediaType)
this.setResults(bookResults) this.setResults(itemResults)
} }
}, },
setResults(bookResults) { setResults(itemResults) {
if (bookResults.error) { if (itemResults.error) {
this.error = bookResults.error this.error = itemResults.error
this.books = [] this.items = []
this.ignoredFiles = [] this.ignoredFiles = []
} else { } else {
this.error = '' this.error = ''
this.books = bookResults.books this.items = itemResults.items
this.ignoredFiles = bookResults.ignoredFiles this.ignoredFiles = itemResults.ignoredFiles
} }
console.log('Upload results', bookResults) console.log('Upload results', itemResults)
}, },
updateBookCardStatus(index, status) { updateItemCardStatus(index, status) {
var ref = this.$refs[`bookCard-${index}`] var ref = this.$refs[`itemCard-${index}`]
if (ref && ref.length) ref = ref[0] if (ref && ref.length) ref = ref[0]
if (!ref) { if (!ref) {
console.error('Book card ref not found', index, this.$refs) console.error('Book card ref not found', index, this.$refs)
@ -218,16 +227,18 @@ export default {
ref.setUploadStatus(status) ref.setUploadStatus(status)
} }
}, },
uploadBook(book) { uploadItem(item) {
var form = new FormData() var form = new FormData()
form.set('title', book.title) form.set('title', item.title)
form.set('author', book.author) if (!this.selectedLibraryIsPodcast) {
form.set('series', book.series) form.set('author', item.author)
form.set('series', item.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
book.files.forEach((file) => { item.files.forEach((file) => {
form.set(`${index++}`, file) form.set(`${index++}`, file)
}) })
@ -241,24 +252,24 @@ export default {
return false return false
}) })
}, },
validateBooks() { validateItems() {
var bookData = [] var itemData = []
for (var book of this.books) { for (var item of this.items) {
var bookref = this.$refs[`bookCard-${book.index}`] var itemref = this.$refs[`itemCard-${item.index}`]
if (bookref && bookref.length) bookref = bookref[0] if (itemref && itemref.length) itemref = itemref[0]
if (!bookref) { if (!itemref) {
console.error('Invalid book index no ref', book.index, this.$refs.bookCard) console.error('Invalid item index no ref', item.index, this.$refs.itemCard)
return false return false
} else { } else {
var data = bookref.getData() var data = itemref.getData()
if (!data) { if (!data) {
return false return false
} }
bookData.push(data) itemData.push(data)
} }
} }
return bookData return itemData
}, },
async submit() { async submit() {
if (!this.selectedFolderId || !this.selectedLibraryId) { if (!this.selectedFolderId || !this.selectedLibraryId) {
@ -267,27 +278,27 @@ export default {
return return
} }
var books = this.validateBooks() var items = this.validateItems()
if (!books) { if (!items) {
this.$toast.error('Some invalid books') this.$toast.error('Some invalid items')
return return
} }
this.processing = true this.processing = true
var booksUploaded = 0 var itemsUploaded = 0
var booksFailed = 0 var itemsFailed = 0
for (let i = 0; i < books.length; i++) { for (let i = 0; i < items.length; i++) {
var book = books[i] var item = items[i]
this.updateBookCardStatus(book.index, 'uploading') this.updateItemCardStatus(item.index, 'uploading')
var result = await this.uploadBook(book) var result = await this.uploadItem(item)
if (result) booksUploaded++ if (result) itemsUploaded++
else booksFailed++ else itemsFailed++
this.updateBookCardStatus(book.index, result ? 'success' : 'failed') this.updateItemCardStatus(item.index, result ? 'success' : 'failed')
} }
if (booksUploaded) { if (itemsUploaded) {
this.$toast.success(`Successfully uploaded ${booksUploaded} book${booksUploaded > 1 ? 's' : ''}`) this.$toast.success(`Successfully uploaded ${itemsUploaded} item${itemsUploaded > 1 ? 's' : ''}`)
} }
if (booksFailed) { if (itemsFailed) {
this.$toast.success(`Failed to upload ${booksFailed} book${booksFailed > 1 ? 's' : ''}`) this.$toast.success(`Failed to upload ${itemsFailed} item${itemsFailed > 1 ? 's' : ''}`)
} }
this.processing = false this.processing = false
this.uploadFinished = true this.uploadFinished = true

View File

@ -4,7 +4,7 @@ const SupportedFileTypes = {
ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'], ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
info: ['nfo'], info: ['nfo'],
text: ['txt'], text: ['txt'],
opf: ['opf'] metadata: ['opf', 'abs']
} }
const DownloadStatus = { const DownloadStatus = {

View File

@ -1,6 +1,7 @@
const Path = require('path') const Path = require('path')
const fs = require('fs-extra') const fs = require('fs-extra')
const Logger = require('../Logger') const Logger = require('../Logger')
const filePerms = require('../utils/filePerms')
const { isObject } = require('../utils/index') const { isObject } = require('../utils/index')
@ -37,15 +38,21 @@ class MiscController {
} }
// For setting permissions recursively // For setting permissions recursively
var firstDirPath = Path.join(folder.fullPath, author)
var outputDirectory = '' var outputDirectory = ''
if (series && author) { var firstDirPath = ''
outputDirectory = Path.join(folder.fullPath, author, series, title)
} else if (author) { if (library.isPodcast) { // Podcasts only in 1 folder
outputDirectory = Path.join(folder.fullPath, author, title)
} else {
outputDirectory = Path.join(folder.fullPath, title) outputDirectory = Path.join(folder.fullPath, title)
firstDirPath = outputDirectory
} else {
firstDirPath = Path.join(folder.fullPath, author)
if (series && author) {
outputDirectory = Path.join(folder.fullPath, author, series, title)
} else if (author) {
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)

View File

@ -26,6 +26,9 @@ class Library {
get folderPaths() { get folderPaths() {
return this.folders.map(f => f.fullPath) return this.folders.map(f => f.fullPath)
} }
get isPodcast() {
return this.mediaType === 'podcast'
}
construct(library) { construct(library) {
this.id = library.id this.id = library.id