mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-02-26 15:11:06 +01:00
Add start of library scan scheduling and cron expression builder
This commit is contained in:
parent
01333b6401
commit
9a57fcad40
@ -47,6 +47,11 @@ export default {
|
|||||||
title: 'Settings',
|
title: 'Settings',
|
||||||
component: 'modals-libraries-library-settings'
|
component: 'modals-libraries-library-settings'
|
||||||
}
|
}
|
||||||
|
// {
|
||||||
|
// id: 'schedule',
|
||||||
|
// title: 'Schedule',
|
||||||
|
// component: 'modals-libraries-schedule-scan'
|
||||||
|
// }
|
||||||
],
|
],
|
||||||
libraryCopy: null
|
libraryCopy: null
|
||||||
}
|
}
|
||||||
@ -84,6 +89,7 @@ export default {
|
|||||||
},
|
},
|
||||||
updateLibrary(library) {
|
updateLibrary(library) {
|
||||||
this.mapLibraryToCopy(library)
|
this.mapLibraryToCopy(library)
|
||||||
|
console.log('Updated library', this.libraryCopy)
|
||||||
},
|
},
|
||||||
getNewLibraryData() {
|
getNewLibraryData() {
|
||||||
return {
|
return {
|
||||||
@ -93,9 +99,11 @@ export default {
|
|||||||
icon: 'database',
|
icon: 'database',
|
||||||
mediaType: 'book',
|
mediaType: 'book',
|
||||||
settings: {
|
settings: {
|
||||||
|
coverAspectRatio: this.$constants.BookCoverAspectRatio.SQUARE,
|
||||||
disableWatcher: false,
|
disableWatcher: false,
|
||||||
skipMatchingMediaWithAsin: false,
|
skipMatchingMediaWithAsin: false,
|
||||||
skipMatchingMediaWithIsbn: false
|
skipMatchingMediaWithIsbn: false,
|
||||||
|
autoScanCronExpression: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -112,7 +120,9 @@ export default {
|
|||||||
if (key === 'folders') {
|
if (key === 'folders') {
|
||||||
this.libraryCopy.folders = library.folders.map((f) => ({ ...f }))
|
this.libraryCopy.folders = library.folders.map((f) => ({ ...f }))
|
||||||
} else if (key === 'settings') {
|
} else if (key === 'settings') {
|
||||||
this.libraryCopy.settings = { ...library.settings }
|
for (const settingKey in library.settings) {
|
||||||
|
this.libraryCopy.settings[settingKey] = library.settings[settingKey]
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.libraryCopy[key] = library[key]
|
this.libraryCopy[key] = library[key]
|
||||||
}
|
}
|
||||||
|
47
client/components/modals/libraries/ScheduleScan.vue
Normal file
47
client/components/modals/libraries/ScheduleScan.vue
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full h-full px-4 py-1 mb-4">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<p class="text-lg">Schedule Automatic Library Scans</p>
|
||||||
|
<ui-checkbox v-model="enableAutoScan" @input="toggleEnableAutoScan" label="Enable" checkbox-bg="bg" label-class="pl-2 text-base" />
|
||||||
|
</div>
|
||||||
|
<widgets-cron-expression-builder v-if="enableAutoScan" v-model="cronExpression" @input="updatedCron" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
library: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null
|
||||||
|
},
|
||||||
|
processing: Boolean
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
cronExpression: null,
|
||||||
|
enableAutoScan: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {
|
||||||
|
toggleEnableAutoScan(v) {
|
||||||
|
if (!v) this.updatedCron(null)
|
||||||
|
},
|
||||||
|
updatedCron(expression) {
|
||||||
|
this.$emit('update', {
|
||||||
|
settings: {
|
||||||
|
autoScanCronExpression: expression
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.cronExpression = this.library.settings.autoScanCronExpression
|
||||||
|
this.enableAutoScan = !!this.cronExpression
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
192
client/components/widgets/CronExpressionBuilder.vue
Normal file
192
client/components/widgets/CronExpressionBuilder.vue
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full py-2">
|
||||||
|
<template v-if="!showAdvancedView">
|
||||||
|
<ui-multi-select-dropdown v-model="selectedWeekdays" @input="updateCron" label="Weekdays to run" :items="weekdays" />
|
||||||
|
|
||||||
|
<div v-show="selectedWeekdays.length" class="flex items-center py-2">
|
||||||
|
<ui-text-input-with-label v-model="selectedHour" @input="updateCron" @blur="hourBlur" type="number" label="Hour" class="max-w-20" />
|
||||||
|
<p class="text-xl px-2 mt-4">:</p>
|
||||||
|
<ui-text-input-with-label v-model="selectedMinute" @input="updateCron" @blur="minuteBlur" type="number" label="Minute" class="max-w-20" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="description" class="w-full bg-primary bg-opacity-75 rounded-xl p-4 text-center mt-2">
|
||||||
|
<p class="text-lg text-gray-200" v-html="description" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<p class="px-1 text-sm font-semibold">Cron Expression</p>
|
||||||
|
<ui-text-input v-model="customCronExpression" @blur="cronExpressionBlur" label="Cron Expression" :padding-y="1" text-center class="w-full text-4xl -tracking-widest mb-2" />
|
||||||
|
<ui-btn v-if="!customCronError" small :disabled="isValidating" @click="validateCron">Validate Cron Expression</ui-btn>
|
||||||
|
<p v-else class="text-error text-xl">{{ customCronError }}</p>
|
||||||
|
</template>
|
||||||
|
<div class="flex justify-end mt-4">
|
||||||
|
<ui-checkbox v-model="showAdvancedView" small checkbox-bg="bg" label="Advanced" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showAdvancedView: false,
|
||||||
|
selectedHour: 0,
|
||||||
|
selectedMinute: 0,
|
||||||
|
selectedWeekdays: [],
|
||||||
|
cronExpression: '0 0 * * *',
|
||||||
|
customCronExpression: '0 0 * * *',
|
||||||
|
customCronError: '',
|
||||||
|
isValidating: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
minuteIsValid() {
|
||||||
|
return !(isNaN(this.selectedMinute) || this.selectedMinute === '' || this.selectedMinute < 0 || this.selectedMinute > 59)
|
||||||
|
},
|
||||||
|
hourIsValid() {
|
||||||
|
return !(isNaN(this.selectedHour) || this.selectedHour === '' || this.selectedHour < 0 || this.selectedHour > 23)
|
||||||
|
},
|
||||||
|
description() {
|
||||||
|
if (!this.selectedWeekdays.length) return ''
|
||||||
|
|
||||||
|
if (!this.hourIsValid) {
|
||||||
|
return `<span class="text-error">Invalid hour must be 0-23 | ${this.selectedHour < 0 || this.selectedHour > 23}</span>`
|
||||||
|
}
|
||||||
|
if (!this.minuteIsValid) {
|
||||||
|
return `<span class="text-error">Invalid minute must be 0-59</span>`
|
||||||
|
}
|
||||||
|
|
||||||
|
var description = 'Run every '
|
||||||
|
const weekdayTexts =
|
||||||
|
this.selectedWeekdays.length === 7
|
||||||
|
? 'day'
|
||||||
|
: this.selectedWeekdays
|
||||||
|
.map((weekday) => {
|
||||||
|
return this.weekdays.find((w) => w.value === weekday).text
|
||||||
|
})
|
||||||
|
.join(', ')
|
||||||
|
description += `<span class="font-bold text-white">${weekdayTexts}</span>`
|
||||||
|
|
||||||
|
const hourString = this.selectedHour.toString()
|
||||||
|
const minuteString = this.selectedMinute.toString().padStart(2, '0')
|
||||||
|
description += ` at <span class="font-bold text-white">${hourString}:${minuteString}</span>`
|
||||||
|
return description
|
||||||
|
},
|
||||||
|
weekdays() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: 'Sunday',
|
||||||
|
value: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Monday',
|
||||||
|
value: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Tuesday',
|
||||||
|
value: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Wednesday',
|
||||||
|
value: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Thursday',
|
||||||
|
value: 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Friday',
|
||||||
|
value: 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Saturday',
|
||||||
|
value: 6
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateCron() {
|
||||||
|
if (!this.minuteIsValid || !this.hourIsValid || !this.selectedWeekdays.length) {
|
||||||
|
this.cronExpression = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.selectedWeekdays.sort()
|
||||||
|
this.cronExpression = `${this.selectedMinute} ${this.selectedHour} * * ${this.selectedWeekdays.join(',')}`
|
||||||
|
this.customCronExpression = this.cronExpression
|
||||||
|
this.$emit('input', this.cronExpression)
|
||||||
|
},
|
||||||
|
minuteBlur() {
|
||||||
|
const v = this.selectedMinute
|
||||||
|
if (v === '' || v === null || isNaN(v) || v < 0) {
|
||||||
|
this.selectedMinute = 0
|
||||||
|
} else if (v > 59) {
|
||||||
|
this.selectedMinute = 59
|
||||||
|
} else {
|
||||||
|
this.selectedMinute = Number(v)
|
||||||
|
}
|
||||||
|
this.updateCron()
|
||||||
|
},
|
||||||
|
hourBlur() {
|
||||||
|
const v = this.selectedHour
|
||||||
|
if (v === '' || v === null || isNaN(v) || v < 0) {
|
||||||
|
this.selectedHour = 0
|
||||||
|
} else if (v > 23) {
|
||||||
|
this.selectedHour = 23
|
||||||
|
} else {
|
||||||
|
this.selectedHour = Number(v)
|
||||||
|
}
|
||||||
|
this.updateCron()
|
||||||
|
},
|
||||||
|
simpleValidateCustomCron() {
|
||||||
|
return this.customCronExpression && this.customCronExpression.split(' ').length === 5
|
||||||
|
},
|
||||||
|
cronExpressionBlur() {
|
||||||
|
this.customCronError = ''
|
||||||
|
if (!this.simpleValidateCustomCron()) {
|
||||||
|
this.customCronError = 'Invalid cron expression'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.customCronExpression !== this.cronExpression) {
|
||||||
|
this.selectedWeekdays = []
|
||||||
|
this.selectedHour = 0
|
||||||
|
this.selectedMinute = 0
|
||||||
|
this.cronExpression = this.customCronExpression
|
||||||
|
}
|
||||||
|
this.$emit('input', this.cronExpression)
|
||||||
|
},
|
||||||
|
validateCron() {
|
||||||
|
this.isValidating = true
|
||||||
|
this.$axios
|
||||||
|
.$post('/api/validate-cron', { expression: this.customCronExpression })
|
||||||
|
.then(() => {
|
||||||
|
this.$toast.success('Cron expression is valid!')
|
||||||
|
this.isValidating = false
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Invalid cron', error)
|
||||||
|
var errMsg = error.response ? error.response.data || '' : ''
|
||||||
|
this.$toast.error('Invalid cron: ' + errMsg)
|
||||||
|
this.isValidating = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
if (!this.value) return
|
||||||
|
// TODO: parse
|
||||||
|
// const pieces = this.value.split(' ')
|
||||||
|
// this.selectedMinute = Number(pieces[0])
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -6,6 +6,7 @@ class LibrarySettings {
|
|||||||
this.disableWatcher = false
|
this.disableWatcher = false
|
||||||
this.skipMatchingMediaWithAsin = false
|
this.skipMatchingMediaWithAsin = false
|
||||||
this.skipMatchingMediaWithIsbn = false
|
this.skipMatchingMediaWithIsbn = false
|
||||||
|
this.autoScanCronExpression = null
|
||||||
|
|
||||||
if (settings) {
|
if (settings) {
|
||||||
this.construct(settings)
|
this.construct(settings)
|
||||||
@ -17,6 +18,7 @@ class LibrarySettings {
|
|||||||
this.disableWatcher = !!settings.disableWatcher
|
this.disableWatcher = !!settings.disableWatcher
|
||||||
this.skipMatchingMediaWithAsin = !!settings.skipMatchingMediaWithAsin
|
this.skipMatchingMediaWithAsin = !!settings.skipMatchingMediaWithAsin
|
||||||
this.skipMatchingMediaWithIsbn = !!settings.skipMatchingMediaWithIsbn
|
this.skipMatchingMediaWithIsbn = !!settings.skipMatchingMediaWithIsbn
|
||||||
|
this.autoScanCronExpression = settings.autoScanCronExpression || null
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
@ -24,7 +26,8 @@ class LibrarySettings {
|
|||||||
coverAspectRatio: this.coverAspectRatio,
|
coverAspectRatio: this.coverAspectRatio,
|
||||||
disableWatcher: this.disableWatcher,
|
disableWatcher: this.disableWatcher,
|
||||||
skipMatchingMediaWithAsin: this.skipMatchingMediaWithAsin,
|
skipMatchingMediaWithAsin: this.skipMatchingMediaWithAsin,
|
||||||
skipMatchingMediaWithIsbn: this.skipMatchingMediaWithIsbn
|
skipMatchingMediaWithIsbn: this.skipMatchingMediaWithIsbn,
|
||||||
|
autoScanCronExpression: this.autoScanCronExpression
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user