mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-11-07 16:44:16 +01:00
Merge branch 'advplyr:master' into master
This commit is contained in:
commit
cd5e5099f2
@ -1,7 +1,7 @@
|
||||
<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>
|
||||
<p class="text-base text-white text-opacity-80 font-mono">#{{ item.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')">
|
||||
@ -15,15 +15,19 @@
|
||||
|
||||
<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" />
|
||||
<ui-text-input-with-label v-model="itemData.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" />
|
||||
<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 class="flex my-2 -mx-2">
|
||||
<div v-if="!isPodcast" 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)" />
|
||||
<ui-text-input-with-label v-model="itemData.series" :disabled="processing" label="Series" note="(optional)" />
|
||||
</div>
|
||||
<div class="w-1/2 px-2">
|
||||
<div class="w-full">
|
||||
@ -33,9 +37,9 @@
|
||||
</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" />
|
||||
<tables-uploaded-files-table :files="item.itemFiles" title="Item Files" class="mt-8" />
|
||||
<tables-uploaded-files-table v-if="item.otherFiles.length" title="Other Files" :files="item.otherFiles" />
|
||||
<tables-uploaded-files-table v-if="item.ignoredFiles.length" title="Ignored Files" :files="item.ignoredFiles" />
|
||||
</template>
|
||||
<widgets-alert v-if="uploadSuccess" type="success">
|
||||
<p class="text-base">Successfully Uploaded!</p>
|
||||
@ -55,15 +59,16 @@ import Path from 'path'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
book: {
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
mediaType: String,
|
||||
processing: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
bookData: {
|
||||
itemData: {
|
||||
title: '',
|
||||
author: '',
|
||||
series: ''
|
||||
@ -75,14 +80,19 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isPodcast() {
|
||||
return this.mediaType === 'podcast'
|
||||
},
|
||||
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)
|
||||
if (!this.itemData.title) return ''
|
||||
if (this.isPodcast) return this.itemData.title
|
||||
|
||||
if (this.itemData.series && this.itemData.author) {
|
||||
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 {
|
||||
return this.bookData.title
|
||||
return this.itemData.title
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -96,24 +106,24 @@ export default {
|
||||
this.error = ''
|
||||
},
|
||||
getData() {
|
||||
if (!this.bookData.title) {
|
||||
if (!this.itemData.title) {
|
||||
this.error = 'Must have a title'
|
||||
return null
|
||||
}
|
||||
this.error = ''
|
||||
var files = this.book.bookFiles.concat(this.book.otherFiles)
|
||||
var files = this.item.itemFiles.concat(this.item.otherFiles)
|
||||
return {
|
||||
index: this.book.index,
|
||||
...this.bookData,
|
||||
index: this.item.index,
|
||||
...this.itemData,
|
||||
files
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.book) {
|
||||
this.bookData.title = this.book.title
|
||||
this.bookData.author = this.book.author
|
||||
this.bookData.series = this.book.series
|
||||
if (this.item) {
|
||||
this.itemData.title = this.item.title
|
||||
this.itemData.author = this.item.author
|
||||
this.itemData.series = this.item.series
|
||||
}
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
<p class="font-book text-3xl text-white truncate pointer-events-none">{{ title }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<div class="absolute -top-10 left-0 w-full flex">
|
||||
<div class="absolute -top-10 left-0 z-10 w-full flex">
|
||||
<template v-for="tab in availableTabs">
|
||||
<div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-1 cursor-pointer hover:bg-bg font-book border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div>
|
||||
</template>
|
||||
@ -252,7 +252,7 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style scoped>
|
||||
.tab {
|
||||
height: 40px;
|
||||
}
|
||||
|
@ -1,22 +1,18 @@
|
||||
<template>
|
||||
<div class="w-full h-full px-4 py-2 mb-4">
|
||||
<div v-show="showDirectoryPicker" class="flex items-center py-1 mb-2">
|
||||
<span class="material-icons text-3xl cursor-pointer hover:text-gray-300" @click="backArrowPress">arrow_back</span>
|
||||
<p class="px-4 text-xl">{{ title }}</p>
|
||||
</div>
|
||||
<div v-if="!showDirectoryPicker" class="w-full h-full py-4">
|
||||
<div class="flex flex-wrap md:flex-nowrap -mx-1">
|
||||
<div class="w-2/5 md:w-72 px-1 py-1 md:py-0">
|
||||
<ui-dropdown v-model="mediaType" :items="mediaTypes" label="Media Type" :disabled="!!library" small @input="changedMediaType" />
|
||||
<ui-dropdown v-model="mediaType" :items="mediaTypes" label="Media Type" :disabled="!isNew" small @input="changedMediaType" />
|
||||
</div>
|
||||
<div class="w-full md:flex-grow px-1 py-1 md:py-0">
|
||||
<ui-text-input-with-label v-model="name" label="Library Name" />
|
||||
<ui-text-input-with-label v-model="name" label="Library Name" @blur="nameBlurred" />
|
||||
</div>
|
||||
<div class="w-1/5 md:w-18 px-1 py-1 md:py-0">
|
||||
<ui-media-icon-picker v-model="icon" />
|
||||
<ui-media-icon-picker v-model="icon" @input="iconChanged" />
|
||||
</div>
|
||||
<div class="w-2/5 md:w-72 px-1 py-1 md:py-0">
|
||||
<ui-dropdown v-model="provider" :items="providers" label="Metadata Provider" small />
|
||||
<ui-dropdown v-model="provider" :items="providers" label="Metadata Provider" small @input="formUpdated" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -27,39 +23,23 @@
|
||||
<ui-editable-text v-model="folder.fullPath" readonly type="text" class="w-full" />
|
||||
<span v-show="folders.length > 1" class="material-icons ml-2 cursor-pointer hover:text-error" @click="removeFolder(folder)">close</span>
|
||||
</div>
|
||||
<!-- <p v-if="!folders.length" class="text-sm text-gray-300 px-1 py-2">No folders</p> -->
|
||||
|
||||
<div class="flex py-1 px-2 items-center w-full">
|
||||
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<ui-editable-text v-model="newFolderPath" placeholder="New folder path" type="text" class="w-full" />
|
||||
<ui-editable-text v-model="newFolderPath" placeholder="New folder path" type="text" class="w-full" @blur="newFolderInputBlurred" />
|
||||
</div>
|
||||
|
||||
<ui-btn class="w-full mt-2" color="primary" @click="showDirectoryPicker = true">Browse for Folder</ui-btn>
|
||||
</div>
|
||||
<div class="absolute bottom-0 left-0 w-full py-4 px-4">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-grow" />
|
||||
<ui-btn v-show="!disableSubmit" color="success" :disabled="disableSubmit" @click="submit">{{ library ? 'Update Library' : 'Create Library' }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<modals-libraries-folder-chooser v-else :paths="folderPaths" @select="selectFolder" />
|
||||
|
||||
<div v-if="!showDirectoryPicker">
|
||||
<div class="flex items-center pt-2">
|
||||
<ui-toggle-switch v-if="!globalWatcherDisabled" v-model="disableWatcher" />
|
||||
<ui-toggle-switch v-else disabled :value="false" />
|
||||
<p class="pl-4 text-lg">Disable folder watcher for library</p>
|
||||
</div>
|
||||
<p v-if="globalWatcherDisabled" class="text-xs text-warning">*Watcher is disabled globally in server settings</p>
|
||||
</div>
|
||||
<modals-libraries-folder-chooser v-else :paths="folderPaths" @back="showDirectoryPicker = false" @select="selectFolder" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
isNew: Boolean,
|
||||
library: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
@ -73,7 +53,6 @@ export default {
|
||||
icon: '',
|
||||
folders: [],
|
||||
showDirectoryPicker: false,
|
||||
disableWatcher: false,
|
||||
newFolderPath: '',
|
||||
mediaType: null,
|
||||
mediaTypes: [
|
||||
@ -89,36 +68,54 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
if (this.showDirectoryPicker) return 'Choose a Folder'
|
||||
return ''
|
||||
},
|
||||
folderPaths() {
|
||||
return this.folders.map((f) => f.fullPath)
|
||||
},
|
||||
disableSubmit() {
|
||||
if (!this.library) {
|
||||
return false
|
||||
}
|
||||
var newfolderpaths = this.folderPaths.join(',')
|
||||
var origfolderpaths = this.library.folders.map((f) => f.fullPath).join(',')
|
||||
|
||||
return newfolderpaths === origfolderpaths && this.name === this.library.name && this.provider === this.library.provider && this.disableWatcher === this.library.disableWatcher && this.icon === this.library.icon && !this.newFolderPath
|
||||
},
|
||||
providers() {
|
||||
if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
|
||||
return this.$store.state.scanners.providers
|
||||
},
|
||||
globalWatcherDisabled() {
|
||||
return this.$store.getters['getServerSetting']('scannerDisableWatcher')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getLibraryData() {
|
||||
return {
|
||||
name: this.name,
|
||||
provider: this.provider,
|
||||
folders: this.folders,
|
||||
icon: this.icon,
|
||||
mediaType: this.mediaType
|
||||
}
|
||||
},
|
||||
formUpdated() {
|
||||
this.$emit('update', this.getLibraryData())
|
||||
},
|
||||
newFolderInputBlurred() {
|
||||
if (this.newFolderPath) {
|
||||
this.folders.push({ fullPath: this.newFolderPath })
|
||||
this.newFolderPath = ''
|
||||
this.formUpdated()
|
||||
}
|
||||
},
|
||||
iconChanged() {
|
||||
this.formUpdated()
|
||||
},
|
||||
nameBlurred() {
|
||||
if (this.name !== this.library.name) {
|
||||
this.formUpdated()
|
||||
}
|
||||
},
|
||||
changedMediaType() {
|
||||
this.provider = this.providers[0].value
|
||||
this.formUpdated()
|
||||
},
|
||||
selectFolder(fullPath) {
|
||||
this.folders.push({ fullPath })
|
||||
this.showDirectoryPicker = false
|
||||
this.formUpdated()
|
||||
},
|
||||
removeFolder(folder) {
|
||||
this.folders = this.folders.filter((f) => f.fullPath !== folder.fullPath)
|
||||
this.formUpdated()
|
||||
},
|
||||
backArrowPress() {
|
||||
if (this.showDirectoryPicker) {
|
||||
@ -129,95 +126,9 @@ export default {
|
||||
this.name = this.library ? this.library.name : ''
|
||||
this.provider = this.library ? this.library.provider : 'google'
|
||||
this.folders = this.library ? this.library.folders.map((p) => ({ ...p })) : []
|
||||
this.disableWatcher = this.library ? !!this.library.disableWatcher : false
|
||||
this.icon = this.library ? this.library.icon : 'default'
|
||||
this.mediaType = this.library ? this.library.mediaType : 'book'
|
||||
this.showDirectoryPicker = false
|
||||
},
|
||||
selectFolder(fullPath) {
|
||||
this.folders.push({ fullPath })
|
||||
this.showDirectoryPicker = false
|
||||
},
|
||||
submit() {
|
||||
if (this.newFolderPath) {
|
||||
this.folders.push({ fullPath: this.newFolderPath })
|
||||
}
|
||||
|
||||
if (this.library) {
|
||||
this.updateLibrary()
|
||||
} else {
|
||||
this.createLibrary()
|
||||
}
|
||||
},
|
||||
updateLibrary() {
|
||||
if (!this.name) {
|
||||
this.$toast.error('Library must have a name')
|
||||
return
|
||||
}
|
||||
if (!this.folders.length) {
|
||||
this.$toast.error('Library must have at least 1 path')
|
||||
return
|
||||
}
|
||||
var newLibraryPayload = {
|
||||
name: this.name,
|
||||
provider: this.provider,
|
||||
folders: this.folders,
|
||||
icon: this.icon,
|
||||
disableWatcher: this.disableWatcher
|
||||
}
|
||||
|
||||
this.$emit('update:processing', true)
|
||||
this.$axios
|
||||
.$patch(`/api/libraries/${this.library.id}`, newLibraryPayload)
|
||||
.then((res) => {
|
||||
this.$emit('update:processing', false)
|
||||
this.$emit('close')
|
||||
this.$toast.success(`Library "${res.name}" updated successfully`)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
if (error.response && error.response.data) {
|
||||
this.$toast.error(error.response.data)
|
||||
} else {
|
||||
this.$toast.error('Failed to update library')
|
||||
}
|
||||
this.$emit('update:processing', false)
|
||||
})
|
||||
},
|
||||
createLibrary() {
|
||||
if (!this.name) {
|
||||
this.$toast.error('Library must have a name')
|
||||
return
|
||||
}
|
||||
if (!this.folders.length) {
|
||||
this.$toast.error('Library must have at least 1 path')
|
||||
return
|
||||
}
|
||||
var newLibraryPayload = {
|
||||
name: this.name,
|
||||
provider: this.provider,
|
||||
folders: this.folders,
|
||||
icon: this.icon,
|
||||
mediaType: this.mediaType,
|
||||
disableWatcher: this.disableWatcher
|
||||
}
|
||||
this.$emit('update:processing', true)
|
||||
this.$axios
|
||||
.$post('/api/libraries', newLibraryPayload)
|
||||
.then((res) => {
|
||||
this.$emit('update:processing', false)
|
||||
this.$emit('close')
|
||||
this.$toast.success(`Library "${res.name}" created successfully`)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
if (error.response && error.response.data) {
|
||||
this.$toast.error(error.response.data)
|
||||
} else {
|
||||
this.$toast.error('Failed to create library')
|
||||
}
|
||||
this.$emit('update:processing', false)
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
@ -5,8 +5,20 @@
|
||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<div class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
||||
<modals-libraries-edit-library v-if="show" :library="library" :processing.sync="processing" @close="show = false" />
|
||||
<div class="absolute -top-10 left-0 z-10 w-full flex">
|
||||
<template v-for="tab in tabs">
|
||||
<div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-1 cursor-pointer hover:bg-bg font-book border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="px-4 w-full text-sm pt-6 pb-20 rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
||||
<component v-if="libraryCopy && show" :is="tabName" :is-new="!library" :library="libraryCopy" :processing.sync="processing" @update="updateLibrary" @close="show = false" />
|
||||
|
||||
<div class="absolute bottom-0 left-0 w-full px-4 py-4 border-t border-opacity-10">
|
||||
<div class="flex justify-end">
|
||||
<ui-btn @click="submit">{{ buttonText }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
</template>
|
||||
@ -22,7 +34,21 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
processing: false
|
||||
processing: false,
|
||||
selectedTab: 'details',
|
||||
tabs: [
|
||||
{
|
||||
id: 'details',
|
||||
title: 'Details',
|
||||
component: 'modals-libraries-edit-library'
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
title: 'Settings',
|
||||
component: 'modals-libraries-library-settings'
|
||||
}
|
||||
],
|
||||
libraryCopy: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -36,10 +62,157 @@ export default {
|
||||
},
|
||||
title() {
|
||||
return this.library ? 'Update Library' : 'New Library'
|
||||
},
|
||||
buttonText() {
|
||||
return this.library ? 'Update Library' : 'Create New Library'
|
||||
},
|
||||
tabName() {
|
||||
var _tab = this.tabs.find((t) => t.id === this.selectedTab)
|
||||
return _tab ? _tab.component : ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show: {
|
||||
handler(newVal) {
|
||||
if (newVal) this.init()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectTab(tab) {
|
||||
this.selectedTab = tab
|
||||
},
|
||||
updateLibrary(library) {
|
||||
this.mapLibraryToCopy(library)
|
||||
},
|
||||
getNewLibraryData() {
|
||||
return {
|
||||
name: '',
|
||||
provider: 'google',
|
||||
folders: [],
|
||||
icon: 'database',
|
||||
mediaType: 'book',
|
||||
settings: {
|
||||
disableWatcher: false
|
||||
}
|
||||
}
|
||||
},
|
||||
init() {
|
||||
this.selectedTab = 'details'
|
||||
this.libraryCopy = this.getNewLibraryData()
|
||||
if (this.library) {
|
||||
this.mapLibraryToCopy(this.library)
|
||||
}
|
||||
},
|
||||
mapLibraryToCopy(library) {
|
||||
for (const key in this.libraryCopy) {
|
||||
if (library[key] !== undefined) {
|
||||
if (key === 'folders') {
|
||||
this.libraryCopy.folders = library.folders.map((f) => ({ ...f }))
|
||||
} else if (key === 'settings') {
|
||||
this.libraryCopy.settings = { ...library.settings }
|
||||
} else {
|
||||
this.libraryCopy[key] = library[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
validate() {
|
||||
if (!this.libraryCopy.name) {
|
||||
this.$toast.error('Library must have a name')
|
||||
return false
|
||||
}
|
||||
if (!this.libraryCopy.folders.length) {
|
||||
this.$toast.error('Library must have at least 1 path')
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
submit() {
|
||||
if (!this.validate()) return
|
||||
|
||||
if (this.library) {
|
||||
this.submitUpdateLibrary()
|
||||
} else {
|
||||
this.submitCreateLibrary()
|
||||
}
|
||||
},
|
||||
getLibraryUpdatePayload() {
|
||||
var updatePayload = {}
|
||||
for (const key in this.libraryCopy) {
|
||||
if (key === 'folders') {
|
||||
if (this.libraryCopy.folders.join(',') !== this.library.folders.join(',')) {
|
||||
updatePayload.folders = [...this.libraryCopy.folders]
|
||||
}
|
||||
} else if (key === 'settings') {
|
||||
for (const settingsKey in this.libraryCopy.settings) {
|
||||
if (this.libraryCopy.settings[settingsKey] !== this.library.settings[settingsKey]) {
|
||||
if (!updatePayload.settings) updatePayload.settings = {}
|
||||
updatePayload.settings[settingsKey] = this.libraryCopy.settings[settingsKey]
|
||||
}
|
||||
}
|
||||
} else if (key !== 'mediaType' && this.libraryCopy[key] !== this.library[key]) {
|
||||
updatePayload[key] = this.libraryCopy[key]
|
||||
}
|
||||
}
|
||||
return updatePayload
|
||||
},
|
||||
submitUpdateLibrary() {
|
||||
var newLibraryPayload = this.getLibraryUpdatePayload()
|
||||
if (!Object.keys(newLibraryPayload).length) {
|
||||
this.$toast.info('No updates are necessary')
|
||||
return
|
||||
}
|
||||
|
||||
this.processing = true
|
||||
this.$axios
|
||||
.$patch(`/api/libraries/${this.library.id}`, newLibraryPayload)
|
||||
.then((res) => {
|
||||
this.processing = false
|
||||
this.show = false
|
||||
this.$toast.success(`Library "${res.name}" updated successfully`)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
if (error.response && error.response.data) {
|
||||
this.$toast.error(error.response.data)
|
||||
} else {
|
||||
this.$toast.error('Failed to update library')
|
||||
}
|
||||
this.processing = false
|
||||
})
|
||||
},
|
||||
submitCreateLibrary() {
|
||||
this.processing = true
|
||||
this.$axios
|
||||
.$post('/api/libraries', this.libraryCopy)
|
||||
.then((res) => {
|
||||
this.processing = false
|
||||
this.show = false
|
||||
this.$toast.success(`Library "${res.name}" created successfully`)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
if (error.response && error.response.data) {
|
||||
this.$toast.error(error.response.data)
|
||||
} else {
|
||||
this.$toast.error('Failed to create library')
|
||||
}
|
||||
this.processing = false
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
mounted() {},
|
||||
beforeDestroy() {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tab {
|
||||
height: 40px;
|
||||
}
|
||||
.tab.tab-selected {
|
||||
height: 41px;
|
||||
}
|
||||
</style>
|
@ -1,10 +1,14 @@
|
||||
<template>
|
||||
<div class="w-full h-full">
|
||||
<div class="w-full h-full bg-bg absolute top-0 left-0 px-4 py-4 z-10">
|
||||
<div class="flex items-center py-1 mb-2">
|
||||
<span class="material-icons text-3xl cursor-pointer hover:text-gray-300" @click="$emit('back')">arrow_back</span>
|
||||
<p class="px-4 text-xl">Choose a Folder</p>
|
||||
</div>
|
||||
<div v-if="allFolders.length" class="w-full bg-primary bg-opacity-70 py-1 px-4 mb-2">
|
||||
<p class="font-mono truncate">{{ selectedPath || '\\' }}</p>
|
||||
</div>
|
||||
<div v-if="allFolders.length" class="flex bg-primary bg-opacity-50 p-4">
|
||||
<div class="w-1/2 border-r border-bg">
|
||||
<div v-if="allFolders.length" class="flex bg-primary bg-opacity-50 p-4 folder-container">
|
||||
<div class="w-1/2 border-r border-bg h-full overflow-y-auto">
|
||||
<div v-if="level > 0" class="w-full p-1 cursor-pointer flex items-center" @click="goBack">
|
||||
<span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<p class="text-base font-mono px-2">..</p>
|
||||
@ -15,7 +19,7 @@
|
||||
<span v-if="dir.dirs && dir.dirs.length && dir.path === selectedPath" class="material-icons" style="font-size: 1.1rem">arrow_right</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1/2">
|
||||
<div class="w-1/2 h-full overflow-y-auto">
|
||||
<div v-for="dir in _subdirs" :key="dir.path" :class="dir.className" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200" @click="selectSubDir(dir)">
|
||||
<span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p>
|
||||
@ -30,12 +34,8 @@
|
||||
<p class="text-gray-300">Note: folders already mapped will not be shown</p>
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-0 left-0 w-full py-4 px-8">
|
||||
<div class="w-full py-2">
|
||||
<ui-btn :disabled="!selectedPath" color="primary" class="w-full mt-2" @click="selectFolder">Select Folder Path</ui-btn>
|
||||
<!-- <div class="flex items-center">
|
||||
<div class="flex-grow" />
|
||||
<ui-btn color="success" @click="selectFolder">Select</ui-btn>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -161,4 +161,9 @@ export default {
|
||||
.dir-item.dir-used {
|
||||
background-color: rgba(255, 25, 0, 0.1);
|
||||
}
|
||||
.folder-container {
|
||||
max-height: calc(100% - 130px);
|
||||
height: calc(100% - 130px);
|
||||
min-height: calc(100% - 130px);
|
||||
}
|
||||
</style>
|
63
client/components/modals/libraries/LibrarySettings.vue
Normal file
63
client/components/modals/libraries/LibrarySettings.vue
Normal file
@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="w-full h-full px-4 py-1 mb-4">
|
||||
<div class="py-3">
|
||||
<div class="flex items-center">
|
||||
<ui-toggle-switch v-if="!globalWatcherDisabled" v-model="disableWatcher" @input="formUpdated" />
|
||||
<ui-toggle-switch v-else disabled :value="false" />
|
||||
<p class="pl-4 text-lg">Disable folder watcher for library</p>
|
||||
</div>
|
||||
<p v-if="globalWatcherDisabled" class="text-xs text-warning">*Watcher is disabled globally in server settings</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
library: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
processing: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
provider: null,
|
||||
disableWatcher: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
librarySettings() {
|
||||
return this.library.settings || {}
|
||||
},
|
||||
globalWatcherDisabled() {
|
||||
return this.$store.getters['getServerSetting']('scannerDisableWatcher')
|
||||
},
|
||||
mediaType() {
|
||||
return this.library.mediaType
|
||||
},
|
||||
providers() {
|
||||
if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
|
||||
return this.$store.state.scanners.providers
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getLibraryData() {
|
||||
return {
|
||||
settings: {
|
||||
disableWatcher: !!this.disableWatcher
|
||||
}
|
||||
}
|
||||
},
|
||||
formUpdated() {
|
||||
this.$emit('update', this.getLibraryData())
|
||||
},
|
||||
init() {
|
||||
this.disableWatcher = !!this.librarySettings.disableWatcher
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
</script>
|
@ -40,8 +40,8 @@ export default {
|
||||
showMenu: false,
|
||||
types: [
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Default'
|
||||
id: 'database',
|
||||
name: 'Database'
|
||||
},
|
||||
{
|
||||
id: 'audiobook',
|
||||
@ -65,7 +65,7 @@ export default {
|
||||
computed: {
|
||||
selected: {
|
||||
get() {
|
||||
return this.value || 'default'
|
||||
return this.value || 'database'
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('input', val)
|
||||
@ -75,7 +75,7 @@ export default {
|
||||
return this.types.find((t) => t.id === this.selected)
|
||||
},
|
||||
selectedName() {
|
||||
return this.selectedItem ? this.selectedItem.name : 'Default'
|
||||
return this.selectedItem ? this.selectedItem.name : 'Database'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -3,7 +3,7 @@
|
||||
<p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">
|
||||
{{ label }}<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em>
|
||||
</p>
|
||||
<ui-text-input ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" class="w-full" />
|
||||
<ui-text-input ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" class="w-full" @blur="inputBlurred" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -38,6 +38,9 @@ export default {
|
||||
if (this.$refs.input && this.$refs.input.blur) {
|
||||
this.$refs.input.blur()
|
||||
}
|
||||
},
|
||||
inputBlurred() {
|
||||
this.$emit('blur')
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
|
@ -4,8 +4,8 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
uploadHelpers: {
|
||||
getBooksFromDrop: this.getBooksFromDataTransferItems,
|
||||
getBooksFromPicker: this.getBooksFromFileList
|
||||
getItemsFromDrop: this.getItemsFromDataTransferItems,
|
||||
getItemsFromPicker: this.getItemsFromFilelist
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -23,8 +23,8 @@ export default {
|
||||
}
|
||||
return false
|
||||
},
|
||||
filterAudiobookFiles(files) {
|
||||
var validBookFiles = []
|
||||
filterItemFiles(files, mediaType) {
|
||||
var validItemFiles = []
|
||||
var validOtherFiles = []
|
||||
var ignoredFiles = []
|
||||
files.forEach((file) => {
|
||||
@ -32,60 +32,60 @@ export default {
|
||||
if (!filetype) ignoredFiles.push(file)
|
||||
else {
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
bookFiles: validBookFiles,
|
||||
itemFiles: validItemFiles,
|
||||
otherFiles: validOtherFiles,
|
||||
ignoredFiles
|
||||
}
|
||||
},
|
||||
audiobookFromItems(items) {
|
||||
var { bookFiles, otherFiles, ignoredFiles } = this.filterAudiobookFiles(items)
|
||||
if (!bookFiles.length) {
|
||||
itemFromTreeItems(items, mediaType) {
|
||||
var { itemFiles, otherFiles, ignoredFiles } = this.filterItemFiles(items, mediaType)
|
||||
if (!itemFiles.length) {
|
||||
ignoredFiles = ignoredFiles.concat(otherFiles)
|
||||
otherFiles = []
|
||||
}
|
||||
return [
|
||||
{
|
||||
bookFiles,
|
||||
itemFiles,
|
||||
otherFiles,
|
||||
ignoredFiles
|
||||
}
|
||||
]
|
||||
},
|
||||
traverseForAudiobook(folder, depth = 1) {
|
||||
traverseForItem(folder, mediaType, depth = 1) {
|
||||
if (folder.items.some((f) => f.isDirectory)) {
|
||||
var audiobooks = []
|
||||
var items = []
|
||||
folder.items.forEach((file) => {
|
||||
if (file.isDirectory) {
|
||||
var audiobookResults = this.traverseForAudiobook(file, ++depth)
|
||||
audiobooks = audiobooks.concat(audiobookResults)
|
||||
var itemResults = this.traverseForItem(file, mediaType, ++depth)
|
||||
items = items.concat(itemResults)
|
||||
}
|
||||
})
|
||||
return audiobooks
|
||||
return items
|
||||
} else {
|
||||
return this.audiobookFromItems(folder.items)
|
||||
return this.itemFromTreeItems(folder.items, mediaType)
|
||||
}
|
||||
},
|
||||
fileTreeToAudiobooks(filetree) {
|
||||
fileTreeToItems(filetree, mediaType) {
|
||||
// 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 })
|
||||
var itemResults = this.traverseForItem({ items: filetree }, mediaType)
|
||||
return {
|
||||
audiobooks: audiobookResults,
|
||||
items: itemResults,
|
||||
ignoredFiles: ignoredFilesInRoot
|
||||
}
|
||||
} else {
|
||||
// Single Book drop
|
||||
return {
|
||||
audiobooks: this.audiobookFromItems(filetree),
|
||||
items: this.itemFromTreeItems(filetree, mediaType),
|
||||
ignoredFiles: []
|
||||
}
|
||||
}
|
||||
@ -140,7 +140,7 @@ export default {
|
||||
series: '',
|
||||
...book
|
||||
}
|
||||
var firstBookFile = book.bookFiles[0]
|
||||
var firstBookFile = book.itemFiles[0]
|
||||
if (!firstBookFile.filepath) return audiobook // No path
|
||||
|
||||
var firstBookPath = Path.dirname(firstBookFile.filepath)
|
||||
@ -157,32 +157,49 @@ export default {
|
||||
}
|
||||
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)
|
||||
if (!files || !files.length) return { error: 'No files found ' }
|
||||
var audiobooksData = this.fileTreeToAudiobooks(files)
|
||||
if (!audiobooksData.audiobooks.length && !audiobooksData.ignoredFiles.length) {
|
||||
var itemData = this.fileTreeToItems(files, mediaType)
|
||||
if (!itemData.items.length && !itemData.ignoredFiles.length) {
|
||||
return { error: 'Invalid file drop' }
|
||||
}
|
||||
var ignoredFiles = audiobooksData.ignoredFiles
|
||||
var ignoredFiles = itemData.ignoredFiles
|
||||
var index = 1
|
||||
var books = audiobooksData.audiobooks.filter((ab) => {
|
||||
if (!ab.bookFiles.length) {
|
||||
var items = itemData.items.filter((ab) => {
|
||||
if (!ab.itemFiles.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 ab.itemFiles.length
|
||||
}).map(ab => this.cleanItem(ab, index++))
|
||||
return {
|
||||
books,
|
||||
items,
|
||||
ignoredFiles
|
||||
}
|
||||
},
|
||||
getBooksFromFileList(filelist) {
|
||||
getItemsFromFilelist(filelist, mediaType) {
|
||||
var ignoredFiles = []
|
||||
var otherFiles = []
|
||||
|
||||
var bookMap = {}
|
||||
var itemMap = {}
|
||||
|
||||
filelist.forEach((file) => {
|
||||
var filetype = this.checkFileType(file.name)
|
||||
@ -191,17 +208,17 @@ export default {
|
||||
file.filetype = filetype
|
||||
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) : ''
|
||||
if (!bookMap[dir]) {
|
||||
bookMap[dir] = {
|
||||
if (!itemMap[dir]) {
|
||||
itemMap[dir] = {
|
||||
path: dir,
|
||||
ignoredFiles: [],
|
||||
bookFiles: [],
|
||||
itemFiles: [],
|
||||
otherFiles: []
|
||||
}
|
||||
}
|
||||
bookMap[dir].bookFiles.push(file)
|
||||
itemMap[dir].itemFiles.push(file)
|
||||
} else {
|
||||
otherFiles.push(file)
|
||||
}
|
||||
@ -210,18 +227,18 @@ export default {
|
||||
|
||||
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)
|
||||
var findItem = Object.values(itemMap).find(b => dir.startsWith(b.path))
|
||||
if (findItem) {
|
||||
findItem.otherFiles.push(file)
|
||||
} else {
|
||||
ignoredFiles.push(file)
|
||||
}
|
||||
})
|
||||
|
||||
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 {
|
||||
books,
|
||||
items,
|
||||
ignoredFiles: ignoredFiles
|
||||
}
|
||||
},
|
||||
|
@ -51,6 +51,7 @@ export default {
|
||||
if (!collection) {
|
||||
return redirect('/')
|
||||
}
|
||||
|
||||
store.commit('user/addUpdateCollection', collection)
|
||||
return {
|
||||
collectionId: collection.id
|
||||
|
@ -23,11 +23,16 @@
|
||||
export default {
|
||||
async asyncData({ store, params, redirect, query, app }) {
|
||||
var libraryId = params.library
|
||||
var library = await store.dispatch('libraries/fetch', libraryId)
|
||||
if (!library) {
|
||||
var libraryData = await store.dispatch('libraries/fetch', libraryId)
|
||||
if (!libraryData) {
|
||||
return redirect('/oops?message=Library not found')
|
||||
}
|
||||
|
||||
const library = libraryData.library
|
||||
if (library.mediaType === 'podcast') {
|
||||
return redirect(`/library/${libraryId}`)
|
||||
}
|
||||
|
||||
return {
|
||||
libraryId
|
||||
}
|
||||
|
@ -14,8 +14,8 @@
|
||||
export default {
|
||||
async asyncData({ params, query, store, app, redirect }) {
|
||||
var libraryId = params.library
|
||||
var library = await store.dispatch('libraries/fetch', libraryId)
|
||||
if (!library) {
|
||||
var libraryData = await store.dispatch('libraries/fetch', libraryId)
|
||||
if (!libraryData) {
|
||||
return redirect('/oops?message=Library not found')
|
||||
}
|
||||
|
||||
@ -23,6 +23,13 @@ export default {
|
||||
if (query.filter) {
|
||||
store.dispatch('user/updateUserSettings', { filterBy: query.filter })
|
||||
}
|
||||
|
||||
// Redirect podcast libraries
|
||||
const library = libraryData.library
|
||||
if (library.mediaType === 'podcast' && (params.id === 'collections' || params.id === 'series')) {
|
||||
return redirect(`/library/${libraryId}`)
|
||||
}
|
||||
|
||||
return {
|
||||
id: params.id || '',
|
||||
libraryId
|
||||
|
@ -44,10 +44,17 @@
|
||||
export default {
|
||||
async asyncData({ params, query, store, app, redirect }) {
|
||||
var libraryId = params.library
|
||||
var library = await store.dispatch('libraries/fetch', libraryId)
|
||||
if (!library) {
|
||||
var libraryData = await store.dispatch('libraries/fetch', libraryId)
|
||||
if (!libraryData) {
|
||||
return redirect('/oops?message=Library not found')
|
||||
}
|
||||
|
||||
// Redirect book libraries
|
||||
const library = libraryData.library
|
||||
if (library.mediaType === 'book') {
|
||||
return redirect(`/library/${libraryId}`)
|
||||
}
|
||||
|
||||
return {
|
||||
libraryId
|
||||
}
|
||||
|
@ -14,10 +14,16 @@
|
||||
export default {
|
||||
async asyncData({ store, params, redirect, query, app }) {
|
||||
var libraryId = params.library
|
||||
var library = await store.dispatch('libraries/fetch', libraryId)
|
||||
if (!library) {
|
||||
var libraryData = await store.dispatch('libraries/fetch', libraryId)
|
||||
if (!libraryData) {
|
||||
return redirect('/oops?message=Library not found')
|
||||
}
|
||||
|
||||
const library = libraryData.library
|
||||
if (library.mediaType === 'podcast') {
|
||||
return redirect(`/library/${libraryId}`)
|
||||
}
|
||||
|
||||
var series = await app.$axios.$get(`/api/series/${params.id}`).catch((error) => {
|
||||
console.error('Failed', error)
|
||||
return false
|
||||
|
@ -3,11 +3,14 @@
|
||||
<div class="w-full max-w-6xl mx-auto">
|
||||
<!-- Library & folder picker -->
|
||||
<div class="flex my-6 -mx-2">
|
||||
<div class="w-1/3 px-2">
|
||||
<ui-dropdown v-model="selectedLibraryId" :items="libraryItems" label="Library" :disabled="processing" @input="libraryChanged" />
|
||||
<div class="w-1/5 px-2">
|
||||
<ui-dropdown v-model="selectedLibraryId" :items="libraryItems" label="Library" :disabled="!!items.length" @input="libraryChanged" />
|
||||
</div>
|
||||
<div class="w-2/3 px-2">
|
||||
<ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="!selectedLibraryId || processing" label="Folder" />
|
||||
<div class="w-3/5 px-2">
|
||||
<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>
|
||||
|
||||
@ -16,7 +19,7 @@
|
||||
</widgets-alert>
|
||||
|
||||
<!-- 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-center text-sm my-5">or</p>
|
||||
<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>
|
||||
</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">
|
||||
<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"> | {{ 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 v-if="!items.length && !uploadReady" type="error" class="my-4">
|
||||
<p class="text-lg">No items 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>
|
||||
<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" />
|
||||
<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)" />
|
||||
<!-- Item Upload cards -->
|
||||
<template v-for="(item, index) in items">
|
||||
<cards-item-upload-card :ref="`itemCard-${item.index}`" :key="index" :media-type="selectedLibraryMediaType" :item="item" :processing="processing" @remove="removeItem(item)" />
|
||||
</template>
|
||||
|
||||
<!-- 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-else @click="reset">Reset</ui-btn>
|
||||
</div>
|
||||
@ -75,7 +78,7 @@ export default {
|
||||
return {
|
||||
isDragging: false,
|
||||
error: '',
|
||||
books: [],
|
||||
items: [],
|
||||
ignoredFiles: [],
|
||||
selectedLibraryId: null,
|
||||
selectedFolderId: null,
|
||||
@ -108,6 +111,12 @@ export default {
|
||||
selectedLibrary() {
|
||||
return this.libraries.find((lib) => lib.id === this.selectedLibraryId)
|
||||
},
|
||||
selectedLibraryMediaType() {
|
||||
return this.selectedLibrary ? this.selectedLibrary.mediaType : null
|
||||
},
|
||||
selectedLibraryIsPodcast() {
|
||||
return this.selectedLibraryMediaType === 'podcast'
|
||||
},
|
||||
selectedFolder() {
|
||||
if (!this.selectedLibrary) return null
|
||||
return this.selectedLibrary.folders.find((fold) => fold.id === this.selectedFolderId)
|
||||
@ -122,7 +131,7 @@ export default {
|
||||
})
|
||||
},
|
||||
uploadReady() {
|
||||
return !this.books.length && !this.ignoredFiles.length && !this.uploadFinished
|
||||
return !this.items.length && !this.ignoredFiles.length && !this.uploadFinished
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -141,15 +150,15 @@ export default {
|
||||
this.selectedFolderId = this.selectedLibrary.folders[0].id
|
||||
}
|
||||
},
|
||||
removeBook(book) {
|
||||
this.books = this.books.filter((b) => b.index !== book.index)
|
||||
if (!this.books.length) {
|
||||
removeItem(item) {
|
||||
this.items = this.items.filter((b) => b.index !== item.index)
|
||||
if (!this.items.length) {
|
||||
this.reset()
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
this.error = ''
|
||||
this.books = []
|
||||
this.items = []
|
||||
this.ignoredFiles = []
|
||||
this.uploadFinished = false
|
||||
if (this.$refs.fileInput) this.$refs.fileInput.value = ''
|
||||
@ -186,31 +195,31 @@ export default {
|
||||
e.preventDefault()
|
||||
this.isDragging = false
|
||||
var items = e.dataTransfer.items || []
|
||||
var bookResults = await this.uploadHelpers.getBooksFromDrop(items)
|
||||
this.setResults(bookResults)
|
||||
var itemResults = await this.uploadHelpers.getItemsFromDrop(items)
|
||||
this.setResults(itemResults)
|
||||
},
|
||||
inputChanged(e) {
|
||||
if (!e.target || !e.target.files) return
|
||||
var _files = Array.from(e.target.files)
|
||||
if (_files && _files.length) {
|
||||
var bookResults = this.uploadHelpers.getBooksFromPicker(_files)
|
||||
this.setResults(bookResults)
|
||||
var itemResults = this.uploadHelpers.getItemsFromPicker(_files, this.selectedLibraryMediaType)
|
||||
this.setResults(itemResults)
|
||||
}
|
||||
},
|
||||
setResults(bookResults) {
|
||||
if (bookResults.error) {
|
||||
this.error = bookResults.error
|
||||
this.books = []
|
||||
setResults(itemResults) {
|
||||
if (itemResults.error) {
|
||||
this.error = itemResults.error
|
||||
this.items = []
|
||||
this.ignoredFiles = []
|
||||
} else {
|
||||
this.error = ''
|
||||
this.books = bookResults.books
|
||||
this.ignoredFiles = bookResults.ignoredFiles
|
||||
this.items = itemResults.items
|
||||
this.ignoredFiles = itemResults.ignoredFiles
|
||||
}
|
||||
console.log('Upload results', bookResults)
|
||||
console.log('Upload results', itemResults)
|
||||
},
|
||||
updateBookCardStatus(index, status) {
|
||||
var ref = this.$refs[`bookCard-${index}`]
|
||||
updateItemCardStatus(index, status) {
|
||||
var ref = this.$refs[`itemCard-${index}`]
|
||||
if (ref && ref.length) ref = ref[0]
|
||||
if (!ref) {
|
||||
console.error('Book card ref not found', index, this.$refs)
|
||||
@ -218,16 +227,18 @@ export default {
|
||||
ref.setUploadStatus(status)
|
||||
}
|
||||
},
|
||||
uploadBook(book) {
|
||||
uploadItem(item) {
|
||||
var form = new FormData()
|
||||
form.set('title', book.title)
|
||||
form.set('author', book.author)
|
||||
form.set('series', book.series)
|
||||
form.set('title', item.title)
|
||||
if (!this.selectedLibraryIsPodcast) {
|
||||
form.set('author', item.author)
|
||||
form.set('series', item.series)
|
||||
}
|
||||
form.set('library', this.selectedLibraryId)
|
||||
form.set('folder', this.selectedFolderId)
|
||||
|
||||
var index = 0
|
||||
book.files.forEach((file) => {
|
||||
item.files.forEach((file) => {
|
||||
form.set(`${index++}`, file)
|
||||
})
|
||||
|
||||
@ -241,24 +252,24 @@ export default {
|
||||
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]
|
||||
validateItems() {
|
||||
var itemData = []
|
||||
for (var item of this.items) {
|
||||
var itemref = this.$refs[`itemCard-${item.index}`]
|
||||
if (itemref && itemref.length) itemref = itemref[0]
|
||||
|
||||
if (!bookref) {
|
||||
console.error('Invalid book index no ref', book.index, this.$refs.bookCard)
|
||||
if (!itemref) {
|
||||
console.error('Invalid item index no ref', item.index, this.$refs.itemCard)
|
||||
return false
|
||||
} else {
|
||||
var data = bookref.getData()
|
||||
var data = itemref.getData()
|
||||
if (!data) {
|
||||
return false
|
||||
}
|
||||
bookData.push(data)
|
||||
itemData.push(data)
|
||||
}
|
||||
}
|
||||
return bookData
|
||||
return itemData
|
||||
},
|
||||
async submit() {
|
||||
if (!this.selectedFolderId || !this.selectedLibraryId) {
|
||||
@ -267,27 +278,27 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
var books = this.validateBooks()
|
||||
if (!books) {
|
||||
this.$toast.error('Some invalid books')
|
||||
var items = this.validateItems()
|
||||
if (!items) {
|
||||
this.$toast.error('Some invalid items')
|
||||
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')
|
||||
var itemsUploaded = 0
|
||||
var itemsFailed = 0
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
var item = items[i]
|
||||
this.updateItemCardStatus(item.index, 'uploading')
|
||||
var result = await this.uploadItem(item)
|
||||
if (result) itemsUploaded++
|
||||
else itemsFailed++
|
||||
this.updateItemCardStatus(item.index, result ? 'success' : 'failed')
|
||||
}
|
||||
if (booksUploaded) {
|
||||
this.$toast.success(`Successfully uploaded ${booksUploaded} book${booksUploaded > 1 ? 's' : ''}`)
|
||||
if (itemsUploaded) {
|
||||
this.$toast.success(`Successfully uploaded ${itemsUploaded} item${itemsUploaded > 1 ? 's' : ''}`)
|
||||
}
|
||||
if (booksFailed) {
|
||||
this.$toast.success(`Failed to upload ${booksFailed} book${booksFailed > 1 ? 's' : ''}`)
|
||||
if (itemsFailed) {
|
||||
this.$toast.success(`Failed to upload ${itemsFailed} item${itemsFailed > 1 ? 's' : ''}`)
|
||||
}
|
||||
this.processing = false
|
||||
this.uploadFinished = true
|
||||
|
@ -4,7 +4,7 @@ const SupportedFileTypes = {
|
||||
ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
|
||||
info: ['nfo'],
|
||||
text: ['txt'],
|
||||
opf: ['opf']
|
||||
metadata: ['opf', 'abs']
|
||||
}
|
||||
|
||||
const DownloadStatus = {
|
||||
|
@ -9,7 +9,7 @@ const UserCollection = require('./objects/UserCollection')
|
||||
const Library = require('./objects/Library')
|
||||
const Author = require('./objects/entities/Author')
|
||||
const Series = require('./objects/entities/Series')
|
||||
const ServerSettings = require('./objects/ServerSettings')
|
||||
const ServerSettings = require('./objects/settings/ServerSettings')
|
||||
const PlaybackSession = require('./objects/PlaybackSession')
|
||||
|
||||
class Db {
|
||||
|
@ -69,19 +69,19 @@ class FolderWatcher extends EventEmitter {
|
||||
|
||||
initWatcher(libraries) {
|
||||
libraries.forEach((lib) => {
|
||||
if (!lib.disableWatcher) {
|
||||
if (!lib.settings.disableWatcher) {
|
||||
this.buildLibraryWatcher(lib)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
addLibrary(library) {
|
||||
if (this.disabled || library.disableWatcher) return
|
||||
if (this.disabled || library.settings.disableWatcher) return
|
||||
this.buildLibraryWatcher(library)
|
||||
}
|
||||
|
||||
updateLibrary(library) {
|
||||
if (this.disabled || library.disableWatcher) return
|
||||
if (this.disabled || library.settings.disableWatcher) return
|
||||
var libwatcher = this.libraryWatchers.find(lib => lib.id === library.id)
|
||||
if (libwatcher) {
|
||||
libwatcher.name = library.name
|
||||
|
@ -1,6 +1,7 @@
|
||||
const Path = require('path')
|
||||
const fs = require('fs-extra')
|
||||
const Logger = require('../Logger')
|
||||
const filePerms = require('../utils/filePerms')
|
||||
|
||||
const { isObject } = require('../utils/index')
|
||||
|
||||
@ -37,15 +38,21 @@ class MiscController {
|
||||
}
|
||||
|
||||
// For setting permissions recursively
|
||||
var firstDirPath = Path.join(folder.fullPath, author)
|
||||
|
||||
var outputDirectory = ''
|
||||
if (series && author) {
|
||||
outputDirectory = Path.join(folder.fullPath, author, series, title)
|
||||
} else if (author) {
|
||||
outputDirectory = Path.join(folder.fullPath, author, title)
|
||||
} else {
|
||||
var firstDirPath = ''
|
||||
|
||||
if (library.isPodcast) { // Podcasts only in 1 folder
|
||||
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)
|
||||
|
@ -139,31 +139,30 @@ class PodcastManager {
|
||||
}
|
||||
|
||||
async checkForNewEpisodes() {
|
||||
var podcastsWithAutoDownload = this.db.libraryItems.find(li => li.mediaType === 'podcast' && li.media.autoDownloadEpisodes)
|
||||
var podcastsWithAutoDownload = this.db.libraryItems.filter(li => li.mediaType === 'podcast' && li.media.autoDownloadEpisodes)
|
||||
if (!podcastsWithAutoDownload.length) {
|
||||
this.cancelCron()
|
||||
return
|
||||
}
|
||||
|
||||
for (const libraryItem of podcastsWithAutoDownload) {
|
||||
Logger.info(`[PodcastManager] checkForNewEpisodes Cron for "${libraryItem.media.metadata.title}"`)
|
||||
const lastEpisodeCheckDate = new Date(libraryItem.media.lastEpisodeCheck || 0)
|
||||
Logger.info(`[PodcastManager] checkForNewEpisodes Cron for "${libraryItem.media.metadata.title}" - Last episode check: ${lastEpisodeCheckDate}`)
|
||||
var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem)
|
||||
var hasUpdates = false
|
||||
|
||||
if (!newEpisodes) { // Failed
|
||||
libraryItem.media.autoDownloadEpisodes = false
|
||||
hasUpdates = true
|
||||
} else if (newEpisodes.length) {
|
||||
Logger.info(`[PodcastManager] Found ${newEpisodes.length} new episodes for podcast "${libraryItem.media.metadata.title}" - starting download`)
|
||||
this.downloadPodcastEpisodes(libraryItem, newEpisodes)
|
||||
hasUpdates = true
|
||||
} else {
|
||||
Logger.debug(`[PodcastManager] No new episodes for "${libraryItem.media.metadata.title}"`)
|
||||
}
|
||||
|
||||
if (hasUpdates) {
|
||||
libraryItem.media.lastEpisodeCheck = Date.now()
|
||||
libraryItem.updatedAt = Date.now()
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
}
|
||||
libraryItem.media.lastEpisodeCheck = Date.now()
|
||||
libraryItem.updatedAt = Date.now()
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
const Folder = require('./Folder')
|
||||
const LibrarySettings = require('./settings/LibrarySettings')
|
||||
const { getId } = require('../utils/index')
|
||||
|
||||
class Library {
|
||||
@ -10,9 +11,9 @@ class Library {
|
||||
this.icon = 'database' // database, podcast, book, audiobook, comic
|
||||
this.mediaType = 'book' // book, podcast
|
||||
this.provider = 'google'
|
||||
this.disableWatcher = false
|
||||
|
||||
this.lastScan = 0
|
||||
this.settings = null
|
||||
|
||||
this.createdAt = null
|
||||
this.lastUpdate = null
|
||||
@ -25,6 +26,9 @@ class Library {
|
||||
get folderPaths() {
|
||||
return this.folders.map(f => f.fullPath)
|
||||
}
|
||||
get isPodcast() {
|
||||
return this.mediaType === 'podcast'
|
||||
}
|
||||
|
||||
construct(library) {
|
||||
this.id = library.id
|
||||
@ -34,7 +38,11 @@ class Library {
|
||||
this.icon = library.icon || 'database'
|
||||
this.mediaType = library.mediaType
|
||||
this.provider = library.provider || 'google'
|
||||
this.disableWatcher = !!library.disableWatcher
|
||||
|
||||
this.settings = new LibrarySettings(library.settings)
|
||||
if (library.settings === undefined) { // LibrarySettings added in v2, migrate settings
|
||||
this.settings.disableWatcher = !!library.disableWatcher
|
||||
}
|
||||
|
||||
this.createdAt = library.createdAt
|
||||
this.lastUpdate = library.lastUpdate
|
||||
@ -62,7 +70,7 @@ class Library {
|
||||
icon: this.icon,
|
||||
mediaType: this.mediaType,
|
||||
provider: this.provider,
|
||||
disableWatcher: this.disableWatcher,
|
||||
settings: this.settings.toJSON(),
|
||||
createdAt: this.createdAt,
|
||||
lastUpdate: this.lastUpdate
|
||||
}
|
||||
@ -89,7 +97,7 @@ class Library {
|
||||
this.icon = data.icon || 'database'
|
||||
this.mediaType = data.mediaType || 'book'
|
||||
this.provider = data.provider || 'google'
|
||||
this.disableWatcher = !!data.disableWatcher
|
||||
this.settings = new LibrarySettings(data.settings)
|
||||
this.createdAt = Date.now()
|
||||
this.lastUpdate = Date.now()
|
||||
}
|
||||
@ -105,10 +113,10 @@ class Library {
|
||||
}
|
||||
})
|
||||
|
||||
if (payload.disableWatcher !== this.disableWatcher) {
|
||||
this.disableWatcher = !!payload.disableWatcher
|
||||
if (payload.settings && this.settings.update(payload.settings)) {
|
||||
hasUpdates = true
|
||||
}
|
||||
|
||||
if (!isNaN(payload.displayOrder) && payload.displayOrder !== this.displayOrder) {
|
||||
this.displayOrder = Number(payload.displayOrder)
|
||||
hasUpdates = true
|
||||
|
34
server/objects/settings/LibrarySettings.js
Normal file
34
server/objects/settings/LibrarySettings.js
Normal file
@ -0,0 +1,34 @@
|
||||
const { BookCoverAspectRatio } = require('../../utils/constants')
|
||||
const Logger = require('../../Logger')
|
||||
|
||||
class LibrarySettings {
|
||||
constructor(settings) {
|
||||
this.disableWatcher = false
|
||||
|
||||
if (settings) {
|
||||
this.construct(settings)
|
||||
}
|
||||
}
|
||||
|
||||
construct(settings) {
|
||||
this.disableWatcher = !!settings.disableWatcher
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
disableWatcher: this.disableWatcher
|
||||
}
|
||||
}
|
||||
|
||||
update(payload) {
|
||||
var hasUpdates = false
|
||||
for (const key in payload) {
|
||||
if (this[key] !== payload[key]) {
|
||||
this[key] = payload[key]
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
return hasUpdates
|
||||
}
|
||||
}
|
||||
module.exports = LibrarySettings
|
@ -1,5 +1,5 @@
|
||||
const { BookCoverAspectRatio, BookshelfView } = require('../utils/constants')
|
||||
const Logger = require('../Logger')
|
||||
const { BookCoverAspectRatio, BookshelfView } = require('../../utils/constants')
|
||||
const Logger = require('../../Logger')
|
||||
|
||||
class ServerSettings {
|
||||
constructor(settings) {
|
Loading…
Reference in New Issue
Block a user