mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-02 00:40:39 +02:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b961c424f | |||
| 389b603d7d | |||
| 721de0a343 | |||
| 0aadf579f3 | |||
| 4ec217e5d0 | |||
| 0f01f21a0a | |||
| 46668854ad | |||
| a690dfe671 | |||
| 7528e8df41 | |||
| 8224ca7650 | |||
| a574d06e22 | |||
| dd9a072231 | |||
| 2304f37cbe | |||
| 0c20988e18 | |||
| 9a57fcad40 | |||
| 01333b6401 | |||
| 8509ca3249 | |||
| 7a69afdcd9 | |||
| 2c0c53bbf1 | |||
| 9f200ece99 | |||
| c5f91ec508 | |||
| d06c61b329 | |||
| be4f11a60e | |||
| 0c5db214d1 | |||
| 1ad9ea92b6 | |||
| d15120eb5f | |||
| b9deb32b20 |
@@ -3,7 +3,7 @@
|
|||||||
<div id="appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-2 md:px-6 py-1 z-50">
|
<div id="appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-2 md:px-6 py-1 z-50">
|
||||||
<div class="flex h-full items-center">
|
<div class="flex h-full items-center">
|
||||||
<nuxt-link to="/">
|
<nuxt-link to="/">
|
||||||
<img src="/icon.svg" class="w-10 min-w-10 h-10 mr-2 sm:w-12 sm:min-w-12 sm:h-12 sm:mr-4" />
|
<img src="/icon.svg" class="w-8 min-w-8 h-8 mr-2 sm:w-12 sm:min-w-12 sm:h-12 sm:mr-4" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link to="/">
|
<nuxt-link to="/">
|
||||||
@@ -12,10 +12,10 @@
|
|||||||
|
|
||||||
<ui-libraries-dropdown class="mr-2" />
|
<ui-libraries-dropdown class="mr-2" />
|
||||||
|
|
||||||
<controls-global-search v-if="currentLibrary" class="" />
|
<controls-global-search v-if="currentLibrary" class="mr-1 sm:mr-0" />
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
<span v-if="showExperimentalFeatures" class="material-icons text-4xl text-warning pr-0 sm:pr-2 md:pr-4">logo_dev</span>
|
<span v-if="showExperimentalFeatures" class="material-icons text-2xl md:text-4xl text-warning pr-0 sm:pr-2 md:pr-4">logo_dev</span>
|
||||||
|
|
||||||
<ui-tooltip v-if="isChromecastInitialized && !isHttps" direction="bottom" text="Casting requires a secure connection" class="flex items-center">
|
<ui-tooltip v-if="isChromecastInitialized && !isHttps" direction="bottom" text="Casting requires a secure connection" class="flex items-center">
|
||||||
<span class="material-icons-outlined text-warning text-opacity-50"> cast </span>
|
<span class="material-icons-outlined text-warning text-opacity-50"> cast </span>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<!-- Regular bookshelf view -->
|
<!-- Regular bookshelf view -->
|
||||||
<div v-else class="w-full">
|
<div v-else class="w-full">
|
||||||
<template v-for="(shelf, index) in shelves">
|
<template v-for="(shelf, index) in shelves">
|
||||||
<app-book-shelf-row :key="index" :index="index" :shelf="shelf" :size-multiplier="sizeMultiplier" :book-cover-width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<app-book-shelf-row :key="index" :index="index" :shelf="shelf" :size-multiplier="sizeMultiplier" :book-cover-width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,11 +70,8 @@ export default {
|
|||||||
libraryName() {
|
libraryName() {
|
||||||
return this.$store.getters['libraries/getCurrentLibraryName']
|
return this.$store.getters['libraries/getCurrentLibraryName']
|
||||||
},
|
},
|
||||||
bookshelfView() {
|
|
||||||
return this.$store.getters['getServerSetting']('bookshelfView')
|
|
||||||
},
|
|
||||||
isAlternativeBookshelfView() {
|
isAlternativeBookshelfView() {
|
||||||
return this.bookshelfView === this.$constants.BookshelfView.TITLES
|
return this.$store.getters['getHomeBookshelfView'] === this.$constants.BookshelfView.TITLES
|
||||||
},
|
},
|
||||||
bookCoverWidth() {
|
bookCoverWidth() {
|
||||||
var coverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
|
var coverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
|
||||||
@@ -82,13 +79,10 @@ export default {
|
|||||||
return coverSize
|
return coverSize
|
||||||
},
|
},
|
||||||
coverAspectRatio() {
|
coverAspectRatio() {
|
||||||
return this.$store.getters['getServerSetting']('coverAspectRatio')
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
isCoverSquareAspectRatio() {
|
isCoverSquareAspectRatio() {
|
||||||
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE
|
return this.coverAspectRatio == 1
|
||||||
},
|
|
||||||
bookCoverAspectRatio() {
|
|
||||||
return this.isCoverSquareAspectRatio ? 1 : 1.6
|
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120
|
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120
|
||||||
|
|||||||
@@ -110,24 +110,20 @@ export default {
|
|||||||
return this.$store.getters['user/getUserSetting']('collapseSeries')
|
return this.$store.getters['user/getUserSetting']('collapseSeries')
|
||||||
},
|
},
|
||||||
coverAspectRatio() {
|
coverAspectRatio() {
|
||||||
return this.$store.getters['getServerSetting']('coverAspectRatio')
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
|
||||||
bookshelfView() {
|
|
||||||
return this.$store.getters['getServerSetting']('bookshelfView')
|
|
||||||
},
|
},
|
||||||
sortingIgnorePrefix() {
|
sortingIgnorePrefix() {
|
||||||
return this.$store.getters['getServerSetting']('sortingIgnorePrefix')
|
return this.$store.getters['getServerSetting']('sortingIgnorePrefix')
|
||||||
},
|
},
|
||||||
isCoverSquareAspectRatio() {
|
isCoverSquareAspectRatio() {
|
||||||
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE
|
return this.coverAspectRatio == 1
|
||||||
|
},
|
||||||
|
bookshelfView() {
|
||||||
|
return this.$store.getters['getBookshelfView']
|
||||||
},
|
},
|
||||||
isAlternativeBookshelfView() {
|
isAlternativeBookshelfView() {
|
||||||
// if (!this.isEntityBook) return false // Only used for bookshelf showing books
|
|
||||||
return this.bookshelfView === this.$constants.BookshelfView.TITLES
|
return this.bookshelfView === this.$constants.BookshelfView.TITLES
|
||||||
},
|
},
|
||||||
bookCoverAspectRatio() {
|
|
||||||
return this.isCoverSquareAspectRatio ? 1 : 1.6
|
|
||||||
},
|
|
||||||
hasFilter() {
|
hasFilter() {
|
||||||
return this.filterBy && this.filterBy !== 'all'
|
return this.filterBy && this.filterBy !== 'all'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div v-if="streamLibraryItem" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 sm:h-44 md:h-40 z-40 bg-primary px-4 pb-1 md:pb-4 pt-2">
|
<div v-if="streamLibraryItem" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 sm:h-44 md:h-40 z-40 bg-primary px-4 pb-1 md:pb-4 pt-2">
|
||||||
<div id="videoDock" />
|
<div id="videoDock" />
|
||||||
<nuxt-link v-if="!playerHandler.isVideo" :to="`/item/${streamLibraryItem.id}`" class="absolute left-1 sm:left-4 cursor-pointer" :style="{ top: bookCoverPosTop + 'px' }">
|
<nuxt-link v-if="!playerHandler.isVideo" :to="`/item/${streamLibraryItem.id}`" class="absolute left-1 sm:left-4 cursor-pointer" :style="{ top: bookCoverPosTop + 'px' }">
|
||||||
<covers-book-cover :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-book-cover :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<div class="flex items-start mb-6 md:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : 'pl-20 sm:pl-24'">
|
<div class="flex items-start mb-6 md:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : 'pl-20 sm:pl-24'">
|
||||||
<div>
|
<div>
|
||||||
@@ -77,16 +77,13 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
coverAspectRatio() {
|
coverAspectRatio() {
|
||||||
return this.$store.getters['getServerSetting']('coverAspectRatio')
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
|
||||||
bookCoverAspectRatio() {
|
|
||||||
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6
|
|
||||||
},
|
},
|
||||||
bookCoverWidth() {
|
bookCoverWidth() {
|
||||||
return 88
|
return 88
|
||||||
},
|
},
|
||||||
bookCoverPosTop() {
|
bookCoverPosTop() {
|
||||||
if (this.bookCoverAspectRatio === 1) return -10
|
if (this.coverAspectRatio == 1) return -10
|
||||||
return -64
|
return -64
|
||||||
},
|
},
|
||||||
cover() {
|
cover() {
|
||||||
@@ -367,7 +364,7 @@ export default {
|
|||||||
if (payload.startTime !== null && !isNaN(payload.startTime)) {
|
if (payload.startTime !== null && !isNaN(payload.startTime)) {
|
||||||
this.seek(payload.startTime)
|
this.seek(payload.startTime)
|
||||||
} else {
|
} else {
|
||||||
this.playerHandler.play()
|
this.playerHandler.play()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="text-gray-300 text-xs md:text-sm">by {{ book.author }}</p>
|
<p class="text-gray-300 text-xs md:text-sm">by {{ book.author }}</p>
|
||||||
<p v-if="book.narrator" class="text-gray-400 text-xs">Narrated by {{ book.narrator }}</p>
|
<p v-if="book.narrator" class="text-gray-400 text-xs">Narrated by {{ book.narrator }}</p>
|
||||||
<p v-if="book.duration" class="text-gray-400 text-xs">Runtime: {{ $elapsedPrettyExtended(book.duration) }}</p>
|
<p v-if="book.duration" class="text-gray-400 text-xs">Runtime: {{ $elapsedPrettyExtended(book.duration * 60) }}</p>
|
||||||
<div v-if="book.series && book.series.length" class="flex py-1 -mx-1">
|
<div v-if="book.series && book.series.length" class="flex py-1 -mx-1">
|
||||||
<div v-for="(series, index) in book.series" :key="index" class="bg-white bg-opacity-10 rounded-full px-1 py-0.5 mx-1">
|
<div v-for="(series, index) in book.series" :key="index" class="bg-white bg-opacity-10 rounded-full px-1 py-0.5 mx-1">
|
||||||
<p class="leading-3 text-xs text-gray-400">
|
<p class="leading-3 text-xs text-gray-400">
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
coverWidth() {
|
coverWidth() {
|
||||||
if (this.bookCoverAspectRatio === 1) return 50 * 1.2
|
if (this.bookCoverAspectRatio === 1) return 50 * 1.2
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
name() {
|
name() {
|
||||||
return this.series.name
|
return this.series.name
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ export default {
|
|||||||
text: 'Author (Last, First)',
|
text: 'Author (Last, First)',
|
||||||
value: 'media.metadata.authorNameLF'
|
value: 'media.metadata.authorNameLF'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Published Year',
|
||||||
|
value: 'media.metadata.publishedYear'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'Added At',
|
text: 'Added At',
|
||||||
value: 'addedAt'
|
value: 'addedAt'
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<template>
|
||||||
|
<modals-modal v-model="show" name="backup-scheduler" :width="700" :height="'unset'" :processing="processing">
|
||||||
|
<template #outer>
|
||||||
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
|
<p class="font-book text-3xl text-white truncate">Set Backup Schedule</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-if="show && newCronExpression" 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">
|
||||||
|
<widgets-cron-expression-builder ref="expressionBuilder" v-model="newCronExpression" @input="expressionUpdated" />
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end">
|
||||||
|
<ui-btn :disabled="!isUpdated" @click="submit">{{ isUpdated ? 'Save Backup Schedule' : 'No update necessary' }}</ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</modals-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: Boolean,
|
||||||
|
cronExpression: {
|
||||||
|
type: String,
|
||||||
|
default: '* * * * *'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
processing: false,
|
||||||
|
newCronExpression: null,
|
||||||
|
isUpdated: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show: {
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
show: {
|
||||||
|
get() {
|
||||||
|
return this.value
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
expressionUpdated() {
|
||||||
|
this.isUpdated = this.newCronExpression !== this.cronExpression
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.newCronExpression = this.cronExpression
|
||||||
|
this.isUpdated = false
|
||||||
|
},
|
||||||
|
submit() {
|
||||||
|
// If custom expression input is focused then unfocus it instead of submitting
|
||||||
|
if (this.$refs.expressionBuilder && this.$refs.expressionBuilder.checkBlurExpressionInput) {
|
||||||
|
if (this.$refs.expressionBuilder.checkBlurExpressionInput()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processing = true
|
||||||
|
|
||||||
|
var updatePayload = {
|
||||||
|
backupSchedule: this.newCronExpression
|
||||||
|
}
|
||||||
|
this.$store
|
||||||
|
.dispatch('updateServerSettings', updatePayload)
|
||||||
|
.then((success) => {
|
||||||
|
console.log('Updated Server Settings', success)
|
||||||
|
this.processing = false
|
||||||
|
this.show = false
|
||||||
|
this.$emit('update:cronExpression', this.newCronExpression)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to update server settings', error)
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {},
|
||||||
|
beforeDestroy() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -75,7 +75,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
collection() {
|
collection() {
|
||||||
return this.$store.state.globals.selectedCollection || {}
|
return this.$store.state.globals.selectedCollection || {}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<modals-modal v-model="show" name="listening-session-modal" :width="700" :height="'unset'">
|
<modals-modal v-model="show" name="listening-session-modal" :processing="processing" :width="700" :height="'unset'">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">Session {{ _session.id }}</p>
|
<p class="font-book text-3xl text-white truncate">Session {{ _session.id }}</p>
|
||||||
@@ -96,6 +96,10 @@
|
|||||||
<p v-if="deviceInfo.deviceType" class="mb-1">Type: {{ deviceInfo.deviceType }}</p>
|
<p v-if="deviceInfo.deviceType" class="mb-1">Type: {{ deviceInfo.deviceType }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<ui-btn small color="error" @click.stop="deleteSessionClick">Delete</ui-btn>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
</template>
|
</template>
|
||||||
@@ -110,7 +114,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {
|
||||||
|
processing: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
show: {
|
show: {
|
||||||
@@ -147,7 +153,37 @@ export default {
|
|||||||
return 'Unknown'
|
return 'Unknown'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {},
|
methods: {
|
||||||
|
deleteSessionClick() {
|
||||||
|
const payload = {
|
||||||
|
message: `Are you sure you want to delete this session?`,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.deleteSession()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
|
deleteSession() {
|
||||||
|
this.processing = true
|
||||||
|
this.$axios
|
||||||
|
.$delete(`/api/sessions/${this._session.id}`)
|
||||||
|
.then(() => {
|
||||||
|
this.processing = false
|
||||||
|
this.$toast.success('Session deleted successfully')
|
||||||
|
this.$emit('removedSession')
|
||||||
|
this.show = false
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.processing = false
|
||||||
|
console.error('Failed to delete session', error)
|
||||||
|
var errMsg = error.response ? error.response.data || '' : ''
|
||||||
|
this.$toast.error(errMsg || 'Failed to delete session')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -70,7 +70,7 @@ export default {
|
|||||||
return this.selectedLibraryItem ? this.selectedLibraryItem.media.metadata.title : ''
|
return this.selectedLibraryItem ? this.selectedLibraryItem.media.metadata.title : ''
|
||||||
},
|
},
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
selectedLibraryItem() {
|
selectedLibraryItem() {
|
||||||
return this.$store.state.selectedLibraryItem
|
return this.$store.state.selectedLibraryItem
|
||||||
|
|||||||
@@ -46,12 +46,14 @@ export default {
|
|||||||
{
|
{
|
||||||
id: 'chapters',
|
id: 'chapters',
|
||||||
title: 'Chapters',
|
title: 'Chapters',
|
||||||
component: 'modals-item-tabs-chapters'
|
component: 'modals-item-tabs-chapters',
|
||||||
|
mediaType: 'book'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'episodes',
|
id: 'episodes',
|
||||||
title: 'Episodes',
|
title: 'Episodes',
|
||||||
component: 'modals-item-tabs-episodes'
|
component: 'modals-item-tabs-episodes',
|
||||||
|
mediaType: 'podcast'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'files',
|
id: 'files',
|
||||||
@@ -66,7 +68,16 @@ export default {
|
|||||||
{
|
{
|
||||||
id: 'manage',
|
id: 'manage',
|
||||||
title: 'Manage',
|
title: 'Manage',
|
||||||
component: 'modals-item-tabs-manage'
|
component: 'modals-item-tabs-manage',
|
||||||
|
mediaType: 'book',
|
||||||
|
admin: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'schedule',
|
||||||
|
title: 'Schedule',
|
||||||
|
component: 'modals-item-tabs-schedule',
|
||||||
|
mediaType: 'podcast',
|
||||||
|
admin: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -120,13 +131,17 @@ export default {
|
|||||||
userCanDownload() {
|
userCanDownload() {
|
||||||
return this.$store.getters['user/getUserCanDownload']
|
return this.$store.getters['user/getUserCanDownload']
|
||||||
},
|
},
|
||||||
|
userIsAdminOrUp() {
|
||||||
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
|
},
|
||||||
availableTabs() {
|
availableTabs() {
|
||||||
if (!this.userCanUpdate && !this.userCanDownload) return []
|
if (!this.userCanUpdate && !this.userCanDownload) return []
|
||||||
return this.tabs.filter((tab) => {
|
return this.tabs.filter((tab) => {
|
||||||
if (tab.experimental && !this.showExperimentalFeatures) return false
|
if (tab.experimental && !this.showExperimentalFeatures) return false
|
||||||
if (tab.id === 'manage' && (this.isMissing || this.mediaType !== 'book')) return false
|
if (tab.mediaType && this.mediaType !== tab.mediaType) return false
|
||||||
if (this.mediaType == 'podcast' && tab.id == 'chapters') return false
|
if (tab.admin && !this.userIsAdminOrUp) return false
|
||||||
if (this.mediaType == 'book' && tab.id == 'episodes') return false
|
|
||||||
|
if (tab.id === 'manage' && this.isMissing) return false
|
||||||
|
|
||||||
if ((tab.id === 'manage' || tab.id === 'files') && this.userCanDownload) return true
|
if ((tab.id === 'manage' || tab.id === 'files') && this.userCanDownload) return true
|
||||||
if (tab.id !== 'manage' && tab.id !== 'files' && this.userCanUpdate) return true
|
if (tab.id !== 'manage' && tab.id !== 'files' && this.userCanUpdate) return true
|
||||||
|
|||||||
@@ -129,11 +129,8 @@ export default {
|
|||||||
else if (this.provider == 'itunes') return 'Search Term'
|
else if (this.provider == 'itunes') return 'Search Term'
|
||||||
return 'Search Title'
|
return 'Search Title'
|
||||||
},
|
},
|
||||||
coverAspectRatio() {
|
|
||||||
return this.$store.getters['getServerSetting']('coverAspectRatio')
|
|
||||||
},
|
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
libraryItemId() {
|
libraryItemId() {
|
||||||
return this.libraryItem ? this.libraryItem.id : null
|
return this.libraryItem ? this.libraryItem.id : null
|
||||||
|
|||||||
@@ -25,59 +25,65 @@
|
|||||||
<cards-book-match-card :key="index" :book="res" :is-podcast="isPodcast" :book-cover-aspect-ratio="bookCoverAspectRatio" @select="selectMatch" />
|
<cards-book-match-card :key="index" :book="res" :is-podcast="isPodcast" :book-cover-aspect-ratio="bookCoverAspectRatio" @select="selectMatch" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatch" class="absolute top-0 left-0 w-full bg-bg h-full p-8 max-h-full overflow-y-auto overflow-x-hidden">
|
<div v-if="selectedMatch" class="absolute top-0 left-0 w-full bg-bg h-full px-2 py-6 md:p-8 max-h-full overflow-y-auto overflow-x-hidden">
|
||||||
<div class="flex mb-2">
|
<div class="flex mb-4">
|
||||||
<div class="w-8 h-8 rounded-full hover:bg-white hover:bg-opacity-10 flex items-center justify-center cursor-pointer" @click="selectedMatch = null">
|
<div class="w-8 h-8 rounded-full hover:bg-white hover:bg-opacity-10 flex items-center justify-center cursor-pointer" @click="selectedMatch = null">
|
||||||
<span class="material-icons text-3xl">arrow_back</span>
|
<span class="material-icons text-3xl">arrow_back</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xl pl-3">Update Book Details</p>
|
<p class="text-xl pl-3">Update Book Details</p>
|
||||||
</div>
|
</div>
|
||||||
|
<ui-checkbox v-model="selectAll" checkbox-bg="bg" @input="selectAllToggled" />
|
||||||
<form @submit.prevent="submitMatchUpdate">
|
<form @submit.prevent="submitMatchUpdate">
|
||||||
<div v-if="selectedMatch.cover" class="flex items-center py-2">
|
<div v-if="selectedMatch.cover" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.cover" />
|
<ui-checkbox v-model="selectedMatchUsage.cover" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<ui-text-input-with-label v-model="selectedMatch.cover" :disabled="!selectedMatchUsage.cover" label="Cover" class="flex-grow ml-4" />
|
<ui-text-input-with-label v-model="selectedMatch.cover" :disabled="!selectedMatchUsage.cover" readonly label="Cover" class="flex-grow mx-4" />
|
||||||
|
<div class="min-w-12 max-w-12 md:min-w-16 md:max-w-16">
|
||||||
|
<a :href="selectedMatch.cover" target="_blank" class="w-full bg-primary">
|
||||||
|
<img :src="selectedMatch.cover" class="h-full w-full object-contain" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatch.title" class="flex items-center py-2">
|
<div v-if="selectedMatch.title" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.title" />
|
<ui-checkbox v-model="selectedMatchUsage.title" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.title" :disabled="!selectedMatchUsage.title" label="Title" />
|
<ui-text-input-with-label v-model="selectedMatch.title" :disabled="!selectedMatchUsage.title" label="Title" />
|
||||||
<p v-if="mediaMetadata.title" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.title || '' }}</p>
|
<p v-if="mediaMetadata.title" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.title || '' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatch.subtitle" class="flex items-center py-2">
|
<div v-if="selectedMatch.subtitle" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.subtitle" />
|
<ui-checkbox v-model="selectedMatchUsage.subtitle" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.subtitle" :disabled="!selectedMatchUsage.subtitle" label="Subtitle" />
|
<ui-text-input-with-label v-model="selectedMatch.subtitle" :disabled="!selectedMatchUsage.subtitle" label="Subtitle" />
|
||||||
<p v-if="mediaMetadata.subtitle" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.subtitle || '' }}</p>
|
<p v-if="mediaMetadata.subtitle" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.subtitle || '' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatch.author" class="flex items-center py-2">
|
<div v-if="selectedMatch.author" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.author" />
|
<ui-checkbox v-model="selectedMatchUsage.author" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.author" :disabled="!selectedMatchUsage.author" label="Author" />
|
<ui-text-input-with-label v-model="selectedMatch.author" :disabled="!selectedMatchUsage.author" label="Author" />
|
||||||
<p v-if="mediaMetadata.authorName" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.authorName || '' }}</p>
|
<p v-if="mediaMetadata.authorName" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.authorName || '' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatch.narrator" class="flex items-center py-2">
|
<div v-if="selectedMatch.narrator" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.narrator" />
|
<ui-checkbox v-model="selectedMatchUsage.narrator" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.narrator" :disabled="!selectedMatchUsage.narrator" label="Narrator" />
|
<ui-text-input-with-label v-model="selectedMatch.narrator" :disabled="!selectedMatchUsage.narrator" label="Narrator" />
|
||||||
<p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.narratorName || '' }}</p>
|
<p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.narratorName || '' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatch.description" class="flex items-center py-2">
|
<div v-if="selectedMatch.description" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.description" />
|
<ui-checkbox v-model="selectedMatchUsage.description" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<ui-textarea-with-label v-model="selectedMatch.description" :rows="3" :disabled="!selectedMatchUsage.description" label="Description" class="flex-grow ml-4" />
|
<ui-textarea-with-label v-model="selectedMatch.description" :rows="3" :disabled="!selectedMatchUsage.description" label="Description" class="flex-grow ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatch.publisher" class="flex items-center py-2">
|
<div v-if="selectedMatch.publisher" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.publisher" />
|
<ui-checkbox v-model="selectedMatchUsage.publisher" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.publisher" :disabled="!selectedMatchUsage.publisher" label="Publisher" />
|
<ui-text-input-with-label v-model="selectedMatch.publisher" :disabled="!selectedMatchUsage.publisher" label="Publisher" />
|
||||||
<p v-if="mediaMetadata.publisher" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.publisher || '' }}</p>
|
<p v-if="mediaMetadata.publisher" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.publisher || '' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatch.publishedYear" class="flex items-center py-2">
|
<div v-if="selectedMatch.publishedYear" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.publishedYear" />
|
<ui-checkbox v-model="selectedMatchUsage.publishedYear" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.publishedYear" :disabled="!selectedMatchUsage.publishedYear" label="Published Year" />
|
<ui-text-input-with-label v-model="selectedMatch.publishedYear" :disabled="!selectedMatchUsage.publishedYear" label="Published Year" />
|
||||||
<p v-if="mediaMetadata.publishedYear" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.publishedYear || '' }}</p>
|
<p v-if="mediaMetadata.publishedYear" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.publishedYear || '' }}</p>
|
||||||
@@ -85,46 +91,46 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="selectedMatch.series" class="flex items-center py-2">
|
<div v-if="selectedMatch.series" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.series" />
|
<ui-checkbox v-model="selectedMatchUsage.series" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<widgets-series-input-widget v-model="selectedMatch.series" />
|
<widgets-series-input-widget v-model="selectedMatch.series" />
|
||||||
<p v-if="mediaMetadata.seriesName" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.seriesName || '' }}</p>
|
<p v-if="mediaMetadata.seriesName" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.seriesName || '' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatch.volumeNumber" class="flex items-center py-2">
|
<div v-if="selectedMatch.volumeNumber" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.volumeNumber" />
|
<ui-checkbox v-model="selectedMatchUsage.volumeNumber" @input="checkboxToggled" />
|
||||||
<ui-text-input-with-label v-model="selectedMatch.volumeNumber" :disabled="!selectedMatchUsage.volumeNumber" label="Volume Number" class="flex-grow ml-4" />
|
<ui-text-input-with-label v-model="selectedMatch.volumeNumber" :disabled="!selectedMatchUsage.volumeNumber" label="Volume Number" class="flex-grow ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatch.genres" class="flex items-center py-2">
|
<div v-if="selectedMatch.genres" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.genres" />
|
<ui-checkbox v-model="selectedMatchUsage.genres" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.genres" :disabled="!selectedMatchUsage.genres" label="Genres" />
|
<ui-text-input-with-label v-model="selectedMatch.genres" :disabled="!selectedMatchUsage.genres" label="Genres" />
|
||||||
<p v-if="mediaMetadata.genresList" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.genresList || '' }}</p>
|
<p v-if="mediaMetadata.genresList" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.genresList || '' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatch.tags" class="flex items-center py-2">
|
<div v-if="selectedMatch.tags" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.tags" />
|
<ui-checkbox v-model="selectedMatchUsage.tags" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.tags" :disabled="!selectedMatchUsage.tags" label="Tags" />
|
<ui-text-input-with-label v-model="selectedMatch.tags" :disabled="!selectedMatchUsage.tags" label="Tags" />
|
||||||
<p v-if="mediaMetadata.tagsList" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.tagsList || '' }}</p>
|
<p v-if="mediaMetadata.tagsList" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.tagsList || '' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatch.language" class="flex items-center py-2">
|
<div v-if="selectedMatch.language" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.language" />
|
<ui-checkbox v-model="selectedMatchUsage.language" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.language" :disabled="!selectedMatchUsage.language" label="Language" />
|
<ui-text-input-with-label v-model="selectedMatch.language" :disabled="!selectedMatchUsage.language" label="Language" />
|
||||||
<p v-if="mediaMetadata.language" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.language || '' }}</p>
|
<p v-if="mediaMetadata.language" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.language || '' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatch.isbn" class="flex items-center py-2">
|
<div v-if="selectedMatch.isbn" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.isbn" />
|
<ui-checkbox v-model="selectedMatchUsage.isbn" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.isbn" :disabled="!selectedMatchUsage.isbn" label="ISBN" />
|
<ui-text-input-with-label v-model="selectedMatch.isbn" :disabled="!selectedMatchUsage.isbn" label="ISBN" />
|
||||||
<p v-if="mediaMetadata.isbn" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.isbn || '' }}</p>
|
<p v-if="mediaMetadata.isbn" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.isbn || '' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatch.asin" class="flex items-center py-2">
|
<div v-if="selectedMatch.asin" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.asin" />
|
<ui-checkbox v-model="selectedMatchUsage.asin" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.asin" :disabled="!selectedMatchUsage.asin" label="ASIN" />
|
<ui-text-input-with-label v-model="selectedMatch.asin" :disabled="!selectedMatchUsage.asin" label="ASIN" />
|
||||||
<p v-if="mediaMetadata.asin" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.asin || '' }}</p>
|
<p v-if="mediaMetadata.asin" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.asin || '' }}</p>
|
||||||
@@ -132,28 +138,28 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="selectedMatch.itunesId" class="flex items-center py-2">
|
<div v-if="selectedMatch.itunesId" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.itunesId" />
|
<ui-checkbox v-model="selectedMatchUsage.itunesId" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.itunesId" type="number" :disabled="!selectedMatchUsage.itunesId" label="iTunes ID" />
|
<ui-text-input-with-label v-model="selectedMatch.itunesId" type="number" :disabled="!selectedMatchUsage.itunesId" label="iTunes ID" />
|
||||||
<p v-if="mediaMetadata.itunesId" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.itunesId || '' }}</p>
|
<p v-if="mediaMetadata.itunesId" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.itunesId || '' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatch.feedUrl" class="flex items-center py-2">
|
<div v-if="selectedMatch.feedUrl" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.feedUrl" />
|
<ui-checkbox v-model="selectedMatchUsage.feedUrl" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.feedUrl" :disabled="!selectedMatchUsage.feedUrl" label="RSS Feed URL" />
|
<ui-text-input-with-label v-model="selectedMatch.feedUrl" :disabled="!selectedMatchUsage.feedUrl" label="RSS Feed URL" />
|
||||||
<p v-if="mediaMetadata.feedUrl" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.feedUrl || '' }}</p>
|
<p v-if="mediaMetadata.feedUrl" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.feedUrl || '' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatch.itunesPageUrl" class="flex items-center py-2">
|
<div v-if="selectedMatch.itunesPageUrl" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.itunesPageUrl" />
|
<ui-checkbox v-model="selectedMatchUsage.itunesPageUrl" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.itunesPageUrl" :disabled="!selectedMatchUsage.itunesPageUrl" label="iTunes Page URL" />
|
<ui-text-input-with-label v-model="selectedMatch.itunesPageUrl" :disabled="!selectedMatchUsage.itunesPageUrl" label="iTunes Page URL" />
|
||||||
<p v-if="mediaMetadata.itunesPageUrl" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.itunesPageUrl || '' }}</p>
|
<p v-if="mediaMetadata.itunesPageUrl" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.itunesPageUrl || '' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatch.releaseDate" class="flex items-center py-2">
|
<div v-if="selectedMatch.releaseDate" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.releaseDate" />
|
<ui-checkbox v-model="selectedMatchUsage.releaseDate" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.releaseDate" :disabled="!selectedMatchUsage.releaseDate" label="Release Date" />
|
<ui-text-input-with-label v-model="selectedMatch.releaseDate" :disabled="!selectedMatchUsage.releaseDate" label="Release Date" />
|
||||||
<p v-if="mediaMetadata.releaseDate" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.releaseDate || '' }}</p>
|
<p v-if="mediaMetadata.releaseDate" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.releaseDate || '' }}</p>
|
||||||
@@ -209,7 +215,8 @@ export default {
|
|||||||
itunesId: true,
|
itunesId: true,
|
||||||
feedUrl: true,
|
feedUrl: true,
|
||||||
releaseDate: true
|
releaseDate: true
|
||||||
}
|
},
|
||||||
|
selectAll: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -246,7 +253,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
providers() {
|
providers() {
|
||||||
if (this.isPodcast) return this.$store.state.scanners.podcastProviders
|
if (this.isPodcast) return this.$store.state.scanners.podcastProviders
|
||||||
@@ -271,6 +278,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
selectAllToggled(val) {
|
||||||
|
for (const key in this.selectedMatchUsage) {
|
||||||
|
this.selectedMatchUsage[key] = val
|
||||||
|
}
|
||||||
|
},
|
||||||
|
checkboxToggled() {
|
||||||
|
this.selectAll = Object.values(this.selectedMatchUsage).findIndex((v) => v == false) < 0
|
||||||
|
},
|
||||||
persistProvider() {
|
persistProvider() {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('book-provider', this.provider)
|
localStorage.setItem('book-provider', this.provider)
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full h-full relative">
|
||||||
|
<div id="scheduleWrapper" class="w-full overflow-y-auto px-2 py-4 md:px-6 md:py-6">
|
||||||
|
<template v-if="!feedUrl">
|
||||||
|
<widgets-alert type="warning" class="text-base mb-4">No RSS feed URL is set for this podcast</widgets-alert>
|
||||||
|
</template>
|
||||||
|
<template v-if="feedUrl || autoDownloadEpisodes">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<p class="text-base md:text-xl font-semibold">Schedule Automatic Episode Downloads</p>
|
||||||
|
<ui-checkbox v-model="enableAutoDownloadEpisodes" label="Enable" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2">
|
||||||
|
<ui-text-input ref="maxEpisodesInput" type="number" v-model="newMaxEpisodesToKeep" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updatedMaxEpisodesToKeep" />
|
||||||
|
<ui-tooltip text="Value of 0 sets no max limit. After a new episode is auto-downloaded this will delete the oldest episode if you have more than X episodes. <br>This will only delete 1 episode per new download.">
|
||||||
|
<p class="pl-4 text-base">
|
||||||
|
Max episodes to keep
|
||||||
|
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||||
|
</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<widgets-cron-expression-builder ref="cronExpressionBuilder" v-if="enableAutoDownloadEpisodes" v-model="cronExpression" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="feedUrl || autoDownloadEpisodes" class="absolute bottom-0 left-0 w-full py-2 md:py-4 bg-bg border-t border-white border-opacity-5">
|
||||||
|
<div class="flex items-center px-2 md:px-4">
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<ui-btn @click="save" :disabled="!isUpdated" :color="isUpdated ? 'success' : 'primary'" class="mx-2">{{ isUpdated ? 'Save' : 'No update necessary' }}</ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
processing: Boolean,
|
||||||
|
libraryItem: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
enableAutoDownloadEpisodes: false,
|
||||||
|
cronExpression: null,
|
||||||
|
newMaxEpisodesToKeep: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isProcessing: {
|
||||||
|
get() {
|
||||||
|
return this.processing
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('update:processing', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
userIsAdminOrUp() {
|
||||||
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
|
},
|
||||||
|
media() {
|
||||||
|
return this.libraryItem ? this.libraryItem.media || {} : {}
|
||||||
|
},
|
||||||
|
mediaMetadata() {
|
||||||
|
return this.media.metadata || {}
|
||||||
|
},
|
||||||
|
libraryItemId() {
|
||||||
|
return this.libraryItem ? this.libraryItem.id : null
|
||||||
|
},
|
||||||
|
feedUrl() {
|
||||||
|
return this.mediaMetadata.feedUrl
|
||||||
|
},
|
||||||
|
autoDownloadEpisodes() {
|
||||||
|
return !!this.media.autoDownloadEpisodes
|
||||||
|
},
|
||||||
|
autoDownloadSchedule() {
|
||||||
|
return this.media.autoDownloadSchedule
|
||||||
|
},
|
||||||
|
maxEpisodesToKeep() {
|
||||||
|
return this.media.maxEpisodesToKeep
|
||||||
|
},
|
||||||
|
isUpdated() {
|
||||||
|
return this.autoDownloadSchedule !== this.cronExpression || this.autoDownloadEpisodes !== this.enableAutoDownloadEpisodes || this.maxEpisodesToKeep !== Number(this.newMaxEpisodesToKeep)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updatedMaxEpisodesToKeep() {
|
||||||
|
if (isNaN(this.newMaxEpisodesToKeep) || this.newMaxEpisodesToKeep < 0) {
|
||||||
|
this.newMaxEpisodesToKeep = 0
|
||||||
|
} else {
|
||||||
|
this.newMaxEpisodesToKeep = Number(this.newMaxEpisodesToKeep)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
save() {
|
||||||
|
// If custom expression input is focused then unfocus it instead of submitting
|
||||||
|
if (this.$refs.cronExpressionBuilder && this.$refs.cronExpressionBuilder.checkBlurExpressionInput) {
|
||||||
|
if (this.$refs.cronExpressionBuilder.checkBlurExpressionInput()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.$refs.maxEpisodesInput && this.$refs.maxEpisodesInput.isFocused) {
|
||||||
|
this.$refs.maxEpisodesInput.blur()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePayload = {
|
||||||
|
autoDownloadEpisodes: this.enableAutoDownloadEpisodes
|
||||||
|
}
|
||||||
|
if (this.enableAutoDownloadEpisodes) {
|
||||||
|
updatePayload.autoDownloadSchedule = this.cronExpression
|
||||||
|
}
|
||||||
|
if (this.newMaxEpisodesToKeep !== this.maxEpisodesToKeep) {
|
||||||
|
updatePayload.maxEpisodesToKeep = this.newMaxEpisodesToKeep
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateDetails(updatePayload)
|
||||||
|
},
|
||||||
|
async updateDetails(updatePayload) {
|
||||||
|
this.isProcessing = true
|
||||||
|
var updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, updatePayload).catch((error) => {
|
||||||
|
console.error('Failed to update', error)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
this.isProcessing = false
|
||||||
|
if (updateResult) {
|
||||||
|
if (updateResult.updated) {
|
||||||
|
this.$toast.success('Item details updated')
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
this.$toast.info('No updates were necessary')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.enableAutoDownloadEpisodes = this.autoDownloadEpisodes
|
||||||
|
this.cronExpression = this.autoDownloadSchedule
|
||||||
|
this.newMaxEpisodesToKeep = this.maxEpisodesToKeep
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#scheduleWrapper {
|
||||||
|
height: calc(100% - 80px);
|
||||||
|
max-height: calc(100% - 80px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full px-4 py-2 mb-4">
|
<div class="w-full h-full px-1 md:px-4 py-2 mb-4">
|
||||||
<div v-if="!showDirectoryPicker" class="w-full h-full py-4">
|
<div v-if="!showDirectoryPicker" class="w-full h-full py-4">
|
||||||
<div class="flex flex-wrap md:flex-nowrap -mx-1">
|
<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">
|
<div class="w-2/5 md:w-72 px-1 py-1 md:py-0">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<modals-modal v-model="show" name="edit-library" :width="700" :height="'unset'" :processing="processing">
|
<modals-modal v-model="show" name="edit-library" :width="700" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
<p class="font-book text-xl md:text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="absolute -top-10 left-0 z-10 w-full flex">
|
<div class="absolute -top-10 left-0 z-10 w-full flex">
|
||||||
@@ -11,10 +11,10 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</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">
|
<div class="px-2 md: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" />
|
<component v-if="libraryCopy && show" ref="tabComponent" :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="absolute bottom-0 left-0 w-full px-4 py-4 border-t border-white border-opacity-10">
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<ui-btn @click="submit">{{ buttonText }}</ui-btn>
|
<ui-btn @click="submit">{{ buttonText }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,6 +46,11 @@ export default {
|
|||||||
id: 'settings',
|
id: 'settings',
|
||||||
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]
|
||||||
}
|
}
|
||||||
@@ -134,6 +144,13 @@ export default {
|
|||||||
submit() {
|
submit() {
|
||||||
if (!this.validate()) return
|
if (!this.validate()) return
|
||||||
|
|
||||||
|
// If custom expression input is focused then unfocus it instead of submitting
|
||||||
|
if (this.$refs.tabComponent && this.$refs.tabComponent.checkBlurExpressionInput) {
|
||||||
|
if (this.$refs.tabComponent.checkBlurExpressionInput()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.library) {
|
if (this.library) {
|
||||||
this.submitUpdateLibrary()
|
this.submitUpdateLibrary()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,23 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full px-4 py-1 mb-4">
|
<div class="w-full h-full px-1 md:px-4 py-1 mb-4">
|
||||||
|
<div class="flex items-center py-2">
|
||||||
|
<ui-toggle-switch v-model="useSquareBookCovers" @input="formUpdated" />
|
||||||
|
<ui-tooltip :text="tooltips.coverAspectRatio">
|
||||||
|
<p class="pl-4 text-base">
|
||||||
|
Use square book covers
|
||||||
|
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||||
|
</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
<div class="py-3">
|
<div class="py-3">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<ui-toggle-switch v-if="!globalWatcherDisabled" v-model="disableWatcher" @input="formUpdated" />
|
<ui-toggle-switch v-if="!globalWatcherDisabled" v-model="disableWatcher" @input="formUpdated" />
|
||||||
<ui-toggle-switch v-else disabled :value="false" />
|
<ui-toggle-switch v-else disabled :value="false" />
|
||||||
<p class="pl-4 text-lg">Disable folder watcher for library</p>
|
<p class="pl-4 text-base">Disable folder watcher for library</p>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="globalWatcherDisabled" class="text-xs text-warning">*Watcher is disabled globally in server settings</p>
|
<p v-if="globalWatcherDisabled" class="text-xs text-warning">*Watcher is disabled globally in server settings</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="mediaType == 'book'" class="py-3">
|
<div v-if="mediaType == 'book'" class="py-3">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<ui-toggle-switch v-model="skipMatchingMediaWithAsin" @input="formUpdated" />
|
<ui-toggle-switch v-model="skipMatchingMediaWithAsin" @input="formUpdated" />
|
||||||
<p class="pl-4 text-lg">Skip matching books that already have an ASIN</p>
|
<p class="pl-4 text-base">Skip matching books that already have an ASIN</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="mediaType == 'book'" class="py-3">
|
<div v-if="mediaType == 'book'" class="py-3">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<ui-toggle-switch v-model="skipMatchingMediaWithIsbn" @input="formUpdated" />
|
<ui-toggle-switch v-model="skipMatchingMediaWithIsbn" @input="formUpdated" />
|
||||||
<p class="pl-4 text-lg">Skip matching books that already have an ISBN</p>
|
<p class="pl-4 text-base">Skip matching books that already have an ISBN</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -35,9 +44,13 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
provider: null,
|
provider: null,
|
||||||
|
useSquareBookCovers: false,
|
||||||
disableWatcher: false,
|
disableWatcher: false,
|
||||||
skipMatchingMediaWithAsin: false,
|
skipMatchingMediaWithAsin: false,
|
||||||
skipMatchingMediaWithIsbn: false
|
skipMatchingMediaWithIsbn: false,
|
||||||
|
tooltips: {
|
||||||
|
coverAspectRatio: 'Prefer to use square covers over standard 1.6:1 book covers'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -59,6 +72,7 @@ export default {
|
|||||||
getLibraryData() {
|
getLibraryData() {
|
||||||
return {
|
return {
|
||||||
settings: {
|
settings: {
|
||||||
|
coverAspectRatio: this.useSquareBookCovers ? this.$constants.BookCoverAspectRatio.SQUARE : this.$constants.BookCoverAspectRatio.STANDARD,
|
||||||
disableWatcher: !!this.disableWatcher,
|
disableWatcher: !!this.disableWatcher,
|
||||||
skipMatchingMediaWithAsin: !!this.skipMatchingMediaWithAsin,
|
skipMatchingMediaWithAsin: !!this.skipMatchingMediaWithAsin,
|
||||||
skipMatchingMediaWithIsbn: !!this.skipMatchingMediaWithIsbn
|
skipMatchingMediaWithIsbn: !!this.skipMatchingMediaWithIsbn
|
||||||
@@ -69,6 +83,7 @@ export default {
|
|||||||
this.$emit('update', this.getLibraryData())
|
this.$emit('update', this.getLibraryData())
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
|
this.useSquareBookCovers = this.librarySettings.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE
|
||||||
this.disableWatcher = !!this.librarySettings.disableWatcher
|
this.disableWatcher = !!this.librarySettings.disableWatcher
|
||||||
this.skipMatchingMediaWithAsin = !!this.librarySettings.skipMatchingMediaWithAsin
|
this.skipMatchingMediaWithAsin = !!this.librarySettings.skipMatchingMediaWithAsin
|
||||||
this.skipMatchingMediaWithIsbn = !!this.librarySettings.skipMatchingMediaWithIsbn
|
this.skipMatchingMediaWithIsbn = !!this.librarySettings.skipMatchingMediaWithIsbn
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full h-full px-1 md:px-4 py-1 mb-4">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<p class="text-base md:text-xl font-semibold">Schedule Automatic Library Scans</p>
|
||||||
|
<ui-checkbox v-model="enableAutoScan" @input="toggleEnableAutoScan" label="Enable" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" />
|
||||||
|
</div>
|
||||||
|
<widgets-cron-expression-builder ref="cronExpressionBuilder" 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: {
|
||||||
|
checkBlurExpressionInput() {
|
||||||
|
// returns true if advanced cron input is focused
|
||||||
|
if (!this.$refs.cronExpressionBuilder) return false
|
||||||
|
return this.$refs.cronExpressionBuilder.checkBlurExpressionInput()
|
||||||
|
},
|
||||||
|
toggleEnableAutoScan(v) {
|
||||||
|
if (!v) this.updatedCron(null)
|
||||||
|
else if (!this.cronExpression) {
|
||||||
|
this.cronExpression = '0 0 * * 1'
|
||||||
|
this.updatedCron(this.cronExpression)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updatedCron(expression) {
|
||||||
|
this.$emit('update', {
|
||||||
|
settings: {
|
||||||
|
autoScanCronExpression: expression
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.cronExpression = this.library.settings.autoScanCronExpression
|
||||||
|
this.enableAutoScan = !!this.cronExpression
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -7,16 +7,17 @@
|
|||||||
</template>
|
</template>
|
||||||
<div ref="wrapper" class="px-8 py-6 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
<div ref="wrapper" class="px-8 py-6 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<p class="text-lg text-gray-200 mb-4">
|
<p v-if="episode" class="text-lg text-gray-200 mb-4">
|
||||||
Are you sure you want to remove episode<br /><span class="text-base">{{ episodeTitle }}</span
|
Are you sure you want to remove episode<br /><span class="text-base">{{ episodeTitle }}</span
|
||||||
>?
|
>?
|
||||||
</p>
|
</p>
|
||||||
|
<p v-else class="text-lg text-gray-200 mb-4">Are you sure you want to remove {{ episodes.length }} episodes?</p>
|
||||||
<p class="text-xs font-semibold text-warning text-opacity-90">Note: This does not delete the audio file unless toggling "Hard delete file"</p>
|
<p class="text-xs font-semibold text-warning text-opacity-90">Note: This does not delete the audio file unless toggling "Hard delete file"</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center pt-4">
|
<div class="flex justify-between items-center pt-4">
|
||||||
<ui-checkbox v-model="hardDeleteFile" label="Hard delete file" check-color="error" checkbox-bg="bg" small label-class="text-base text-gray-200 pl-3" />
|
<ui-checkbox v-model="hardDeleteFile" label="Hard delete file" check-color="error" checkbox-bg="bg" small label-class="text-base text-gray-200 pl-3" />
|
||||||
|
|
||||||
<ui-btn @click="submit">{{ hardDeleteFile ? 'Delete episode' : 'Remove episode' }}</ui-btn>
|
<ui-btn @click="submit">{{ btnText }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
@@ -30,9 +31,9 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
episode: {
|
episodes: {
|
||||||
type: Object,
|
type: Array,
|
||||||
default: () => {}
|
default: () => []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -55,34 +56,49 @@ export default {
|
|||||||
this.$emit('input', val)
|
this.$emit('input', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
episode() {
|
||||||
|
if (this.episodes.length === 1) return this.episodes[0]
|
||||||
|
return null
|
||||||
|
},
|
||||||
title() {
|
title() {
|
||||||
|
if (this.episodes.length > 1) return `Remove ${this.episodes.length} episodes`
|
||||||
return 'Remove Episode'
|
return 'Remove Episode'
|
||||||
},
|
},
|
||||||
episodeId() {
|
btnText() {
|
||||||
return this.episode ? this.episode.id : null
|
if (this.episodes.length > 1) return this.hardDeleteFile ? `Delete ${this.episodes.length} episodes` : `Remove ${this.episodes.length} episodes`
|
||||||
|
return this.hardDeleteFile ? 'Delete episode' : 'Remove episode'
|
||||||
},
|
},
|
||||||
episodeTitle() {
|
episodeTitle() {
|
||||||
return this.episode ? this.episode.title : null
|
return this.episode ? this.episode.title : null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
submit() {
|
async submit() {
|
||||||
this.processing = true
|
this.processing = true
|
||||||
|
|
||||||
var queryString = this.hardDeleteFile ? '?hard=1' : ''
|
var queryString = this.hardDeleteFile ? '?hard=1' : ''
|
||||||
this.$axios
|
for (const episode of this.episodes) {
|
||||||
.$delete(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}${queryString}`)
|
const success = await this.$axios
|
||||||
.then(() => {
|
.$delete(`/api/podcasts/${this.libraryItem.id}/episode/${episode.id}${queryString}`)
|
||||||
|
.then(() => true)
|
||||||
|
.catch((error) => {
|
||||||
|
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to remove episode'
|
||||||
|
console.error('Failed to remove episode', error)
|
||||||
|
this.$toast.error(errorMsg)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
if (!success) {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
this.$toast.success('Podcast episode removed')
|
|
||||||
this.show = false
|
this.show = false
|
||||||
})
|
this.$emit('clearSelected')
|
||||||
.catch((error) => {
|
return
|
||||||
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed remove episode'
|
}
|
||||||
console.error('Failed update episode', error)
|
}
|
||||||
this.processing = false
|
|
||||||
this.$toast.error(errorMsg)
|
this.processing = false
|
||||||
})
|
this.$toast.success(`${this.episodes.length} episode${this.episodes.length > 1 ? 's' : ''} removed`)
|
||||||
|
this.show = false
|
||||||
|
this.$emit('clearSelected')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export default {
|
|||||||
return this.mediaMetadata.author
|
return this.mediaMetadata.author
|
||||||
},
|
},
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {},
|
methods: {},
|
||||||
|
|||||||
@@ -23,9 +23,16 @@
|
|||||||
<ui-rich-text-editor label="Description" v-model="newEpisode.description" />
|
<ui-rich-text-editor label="Description" v-model="newEpisode.description" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end pt-4">
|
<div class="flex items-center justify-end pt-4">
|
||||||
<ui-btn @click="submit">Submit</ui-btn>
|
<ui-btn @click="submit">Submit</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="enclosureUrl" class="py-4">
|
||||||
|
<p class="text-xs text-gray-300 font-semibold">Episode URL from RSS feed</p>
|
||||||
|
<a :href="enclosureUrl" target="_blank" class="text-xs text-blue-400 hover:text-blue-500 hover:underline">{{ enclosureUrl }}</a>
|
||||||
|
</div>
|
||||||
|
<div v-else class="py-4">
|
||||||
|
<p class="text-xs text-gray-300 font-semibold">Episode not linked to RSS feed episode</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -76,6 +83,12 @@ export default {
|
|||||||
},
|
},
|
||||||
episodeId() {
|
episodeId() {
|
||||||
return this.episode ? this.episode.id : null
|
return this.episode ? this.episode.id : null
|
||||||
|
},
|
||||||
|
enclosure() {
|
||||||
|
return this.episode ? this.episode.enclosure || {} : {}
|
||||||
|
},
|
||||||
|
enclosureUrl() {
|
||||||
|
return this.enclosure.url
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
<div v-if="selectedBackup" class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300">
|
<div v-if="selectedBackup" class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300">
|
||||||
<p class="text-error text-lg font-semibold">Important Notice!</p>
|
<p class="text-error text-lg font-semibold">Important Notice!</p>
|
||||||
<p class="text-base py-1">Applying a backup will overwrite users, user progress, book details, settings, and covers stored in metadata with the backed up data.</p>
|
<p class="text-base py-1">Applying a backup will overwrite users, user progress, book details, settings, and covers stored in metadata with the backed up data.</p>
|
||||||
<p class="text-base py-1">Backups <strong>do not</strong> modify any files in your library folders, only data in the audiobookshelf created <span class="font-mono">/config</span> and <span class="font-mono">/metadata</span> directories. If you have enabled server settings to store cover art and metadata in your library folders then those are not backup up or overwritten.</p>
|
<p class="text-base py-1">Backups <strong>do not</strong> modify any files in your library folders, only data in the audiobookshelf created <span class="font-mono">/config</span> and <span class="font-mono">/metadata</span> directories. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.</p>
|
||||||
<p class="text-base py-1">All clients using your server will be automatically refreshed.</p>
|
<p class="text-base py-1">All clients using your server will be automatically refreshed.</p>
|
||||||
|
|
||||||
<p class="text-lg text-center my-8">Are you sure you want to apply the backup created on {{ selectedBackup.datePretty }}?</p>
|
<p class="text-lg text-center my-8">Are you sure you want to apply the backup created on {{ selectedBackup.datePretty }}?</p>
|
||||||
|
|||||||
@@ -52,11 +52,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
coverAspectRatio() {
|
|
||||||
return this.$store.getters['getServerSetting']('coverAspectRatio')
|
|
||||||
},
|
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
totalDuration() {
|
totalDuration() {
|
||||||
var _total = 0
|
var _total = 0
|
||||||
|
|||||||
@@ -164,8 +164,8 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.$root.socket) {
|
if (this.$root.socket) {
|
||||||
this.$root.socket.off('user_added', this.newUserAdded)
|
this.$root.socket.off('user_added', this.addUpdateUser)
|
||||||
this.$root.socket.off('user_updated', this.userUpdated)
|
this.$root.socket.off('user_updated', this.addUpdateUser)
|
||||||
this.$root.socket.off('user_removed', this.userRemoved)
|
this.$root.socket.off('user_removed', this.userRemoved)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,6 +208,6 @@ export default {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
background-color: #272727
|
background-color: #272727;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full px-2 py-3 overflow-hidden relative border-b border-white border-opacity-10" @mouseover="mouseover" @mouseleave="mouseleave">
|
<div class="w-full px-2 py-3 overflow-hidden relative border-b border-white border-opacity-10" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||||
<div v-if="episode" class="flex items-center h-24 cursor-pointer" @click="$emit('view', episode)">
|
<div v-if="episode" class="flex items-center cursor-pointer" :class="{ 'opacity-70': isSelected || selectionMode }" @click="clickedEpisode">
|
||||||
<div class="flex-grow px-2">
|
<div class="flex-grow px-2">
|
||||||
<p class="text-sm font-semibold">
|
<p class="text-sm font-semibold">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
@@ -8,6 +8,12 @@
|
|||||||
|
|
||||||
<p class="text-sm text-gray-200 episode-subtitle mt-1.5 mb-0.5">{{ subtitle }}</p>
|
<p class="text-sm text-gray-200 episode-subtitle mt-1.5 mb-0.5">{{ subtitle }}</p>
|
||||||
|
|
||||||
|
<div class="flex justify-between pt-2 max-w-xl">
|
||||||
|
<p v-if="episode.season" class="text-sm text-gray-300">Season #{{ episode.season }}</p>
|
||||||
|
<p v-if="episode.episode" class="text-sm text-gray-300">Episode #{{ episode.episode }}</p>
|
||||||
|
<p v-if="publishedAt" class="text-sm text-gray-300">Published {{ $formatDate(publishedAt, 'MMM do, yyyy') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center pt-2">
|
<div class="flex items-center pt-2">
|
||||||
<button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="userIsFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick">
|
<button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="userIsFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick">
|
||||||
<span class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
|
<span class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
|
||||||
@@ -17,20 +23,18 @@
|
|||||||
<ui-tooltip :text="userIsFinished ? 'Mark as Not Finished' : 'Mark as Finished'" direction="top">
|
<ui-tooltip :text="userIsFinished ? 'Mark as Not Finished' : 'Mark as Finished'" direction="top">
|
||||||
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
|
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<p v-if="episode.season" class="px-4 text-sm text-gray-300">Season #{{ episode.season }}</p>
|
|
||||||
<p v-if="episode.episode" class="px-4 text-sm text-gray-300">Episode #{{ episode.episode }}</p>
|
<ui-icon-btn v-if="userCanUpdate" icon="edit" borderless @click="clickEdit" />
|
||||||
<p v-if="publishedAt" class="px-4 text-sm text-gray-300">Published {{ $formatDate(publishedAt, 'MMM do, yyyy') }}</p>
|
<ui-icon-btn v-if="userCanDelete" icon="close" borderless @click="removeClick" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-24 min-w-24" />
|
<div v-if="isHovering || isSelected || selectionMode" class="hidden md:block w-12 min-w-12" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-24 min-w-24 -right-0 absolute top-0 h-full transform transition-transform" :class="!isHovering ? 'translate-x-32' : 'translate-x-0'">
|
<div v-if="isSelected || selectionMode" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-10 z-10 cursor-pointer" @click.stop="clickedSelectionBg" />
|
||||||
|
<div class="hidden md:block md:w-12 md:min-w-12 md:-right-0 md:absolute md:top-0 h-full transform transition-transform z-20" :class="!isHovering && !isSelected && !selectionMode ? 'translate-x-24' : 'translate-x-0'">
|
||||||
<div class="flex h-full items-center">
|
<div class="flex h-full items-center">
|
||||||
<div class="mx-1">
|
<div class="mx-1">
|
||||||
<ui-icon-btn v-if="userCanUpdate" icon="edit" borderless @click="clickEdit" />
|
<ui-checkbox v-model="isSelected" @input="selectedUpdated" checkbox-bg="bg" />
|
||||||
</div>
|
|
||||||
<div class="mx-1">
|
|
||||||
<ui-icon-btn v-if="userCanDelete" icon="close" borderless @click="removeClick" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,13 +50,15 @@ export default {
|
|||||||
episode: {
|
episode: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
}
|
},
|
||||||
|
selectionMode: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isProcessingReadUpdate: false,
|
isProcessingReadUpdate: false,
|
||||||
processingRemove: false,
|
processingRemove: false,
|
||||||
isHovering: false
|
isHovering: false,
|
||||||
|
isSelected: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -104,8 +110,17 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
clickedEpisode() {
|
||||||
|
this.$emit('view', this.episode)
|
||||||
|
},
|
||||||
|
clickedSelectionBg() {
|
||||||
|
this.isSelected = !this.isSelected
|
||||||
|
this.selectedUpdated(this.isSelected)
|
||||||
|
},
|
||||||
|
selectedUpdated(value) {
|
||||||
|
this.$emit('selected', { isSelected: value, episode: this.episode })
|
||||||
|
},
|
||||||
mouseover() {
|
mouseover() {
|
||||||
// if (this.isDragging) return
|
|
||||||
this.isHovering = true
|
this.isHovering = true
|
||||||
},
|
},
|
||||||
mouseleave() {
|
mouseleave() {
|
||||||
|
|||||||
@@ -3,14 +3,18 @@
|
|||||||
<div class="flex items-center mb-4">
|
<div class="flex items-center mb-4">
|
||||||
<p class="text-lg mb-0 font-semibold">Episodes</p>
|
<p class="text-lg mb-0 font-semibold">Episodes</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<controls-episode-sort-select v-model="sortKey" :descending.sync="sortDesc" class="w-36 sm:w-44 md:w-48 h-9 ml-1 sm:ml-4" />
|
<template v-if="isSelectionMode">
|
||||||
|
<ui-btn color="error" small @click="removeSelectedEpisodes">Remove {{ selectedEpisodes.length }} episode{{ selectedEpisodes.length > 1 ? 's' : '' }}</ui-btn>
|
||||||
|
<ui-btn small class="ml-2" @click="clearSelected">Cancel</ui-btn>
|
||||||
|
</template>
|
||||||
|
<controls-episode-sort-select v-else v-model="sortKey" :descending.sync="sortDesc" class="w-36 sm:w-44 md:w-48 h-9 ml-1 sm:ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<p v-if="!episodes.length" class="py-4 text-center text-lg">No Episodes</p>
|
<p v-if="!episodes.length" class="py-4 text-center text-lg">No Episodes</p>
|
||||||
<template v-for="episode in episodesSorted">
|
<template v-for="episode in episodesSorted">
|
||||||
<tables-podcast-episode-table-row :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" class="item" @remove="removeEpisode" @edit="editEpisode" @view="viewEpisode" />
|
<tables-podcast-episode-table-row ref="episodeRow" :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" :selection-mode="isSelectionMode" class="item" @remove="removeEpisode" @edit="editEpisode" @view="viewEpisode" @selected="episodeSelected" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<modals-podcast-remove-episode v-model="showPodcastRemoveModal" :library-item="libraryItem" :episode="selectedEpisode" />
|
<modals-podcast-remove-episode v-model="showPodcastRemoveModal" @input="removeEpisodeModalToggled" :library-item="libraryItem" :episodes="episodesToRemove" @clearSelected="clearSelected" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -28,7 +32,9 @@ export default {
|
|||||||
sortKey: 'publishedAt',
|
sortKey: 'publishedAt',
|
||||||
sortDesc: true,
|
sortDesc: true,
|
||||||
selectedEpisode: null,
|
selectedEpisode: null,
|
||||||
showPodcastRemoveModal: false
|
showPodcastRemoveModal: false,
|
||||||
|
selectedEpisodes: [],
|
||||||
|
episodesToRemove: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -37,6 +43,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isSelectionMode() {
|
||||||
|
return this.selectedEpisodes.length > 0
|
||||||
|
},
|
||||||
userCanUpdate() {
|
userCanUpdate() {
|
||||||
return this.$store.getters['user/getUserCanUpdate']
|
return this.$store.getters['user/getUserCanUpdate']
|
||||||
},
|
},
|
||||||
@@ -59,8 +68,31 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
removeEpisodeModalToggled(val) {
|
||||||
|
if (!val) this.episodesToRemove = []
|
||||||
|
},
|
||||||
|
clearSelected() {
|
||||||
|
const episodeRows = this.$refs.episodeRow
|
||||||
|
if (episodeRows && episodeRows.length) {
|
||||||
|
for (const epRow of episodeRows) {
|
||||||
|
if (epRow) epRow.isSelected = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.selectedEpisodes = []
|
||||||
|
},
|
||||||
|
removeSelectedEpisodes() {
|
||||||
|
this.episodesToRemove = this.selectedEpisodes
|
||||||
|
this.showPodcastRemoveModal = true
|
||||||
|
},
|
||||||
|
episodeSelected({ isSelected, episode }) {
|
||||||
|
if (isSelected) {
|
||||||
|
this.selectedEpisodes.push(episode)
|
||||||
|
} else {
|
||||||
|
this.selectedEpisodes = this.selectedEpisodes.filter((ep) => ep.id !== episode.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
removeEpisode(episode) {
|
removeEpisode(episode) {
|
||||||
this.selectedEpisode = episode
|
this.episodesToRemove = [episode]
|
||||||
this.showPodcastRemoveModal = true
|
this.showPodcastRemoveModal = true
|
||||||
},
|
},
|
||||||
editEpisode(episode) {
|
editEpisode(episode) {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export default {
|
|||||||
value: Boolean,
|
value: Boolean,
|
||||||
label: String,
|
label: String,
|
||||||
small: Boolean,
|
small: Boolean,
|
||||||
|
medium: Boolean,
|
||||||
checkboxBg: {
|
checkboxBg: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'white'
|
default: 'white'
|
||||||
@@ -47,6 +48,7 @@ export default {
|
|||||||
wrapperClass() {
|
wrapperClass() {
|
||||||
var classes = [`bg-${this.checkboxBg} border-${this.borderColor}`]
|
var classes = [`bg-${this.checkboxBg} border-${this.borderColor}`]
|
||||||
if (this.small) classes.push('w-4 h-4')
|
if (this.small) classes.push('w-4 h-4')
|
||||||
|
else if (this.medium) classes.push('w-5 h-5')
|
||||||
else classes.push('w-6 h-6')
|
else classes.push('w-6 h-6')
|
||||||
|
|
||||||
return classes.join(' ')
|
return classes.join(' ')
|
||||||
@@ -55,11 +57,13 @@ export default {
|
|||||||
if (this.labelClass) return this.labelClass
|
if (this.labelClass) return this.labelClass
|
||||||
var classes = ['pl-1']
|
var classes = ['pl-1']
|
||||||
if (this.small) classes.push('text-xs md:text-sm')
|
if (this.small) classes.push('text-xs md:text-sm')
|
||||||
|
else if (this.medium) classes.push('text-base md:text-lg')
|
||||||
return classes.join(' ')
|
return classes.join(' ')
|
||||||
},
|
},
|
||||||
svgClass() {
|
svgClass() {
|
||||||
var classes = [`text-${this.checkColor}`]
|
var classes = [`text-${this.checkColor}`]
|
||||||
if (this.small) classes.push('w-3 h-3')
|
if (this.small) classes.push('w-3 h-3')
|
||||||
|
else if (this.medium) classes.push('w-3.5 h-3.5')
|
||||||
else classes.push('w-4 h-4')
|
else classes.push('w-4 h-4')
|
||||||
|
|
||||||
return classes.join(' ')
|
return classes.join(' ')
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="currentLibrary" class="relative h-8 max-w-52 min-w-32" v-click-outside="clickOutsideObj">
|
<div v-if="currentLibrary" class="relative h-8 max-w-52 md:min-w-32" v-click-outside="clickOutsideObj">
|
||||||
<button type="button" :disabled="disabled" class="w-10 sm:w-full relative h-full border border-white border-opacity-10 hover:border-opacity-20 rounded shadow-sm px-2 text-left text-sm focus:outline-none cursor-pointer bg-black bg-opacity-20 text-gray-400 hover:text-gray-200" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
<button type="button" :disabled="disabled" class="w-10 sm:w-full relative h-full border border-white border-opacity-10 hover:border-opacity-20 rounded shadow-sm px-2 text-left text-sm focus:outline-none cursor-pointer bg-black bg-opacity-20 text-gray-400 hover:text-gray-200" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||||
<div class="flex items-center justify-center sm:justify-start">
|
<div class="flex items-center justify-center sm:justify-start">
|
||||||
<widgets-library-icon :icon="currentLibraryIcon" class="sm:mr-1.5" />
|
<widgets-library-icon :icon="currentLibraryIcon" class="sm:mr-1.5" />
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showPassword: false,
|
showPassword: false,
|
||||||
isHovering: false
|
isHovering: false,
|
||||||
|
isFocused: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -66,9 +67,11 @@ export default {
|
|||||||
this.inputValue = ''
|
this.inputValue = ''
|
||||||
},
|
},
|
||||||
focused() {
|
focused() {
|
||||||
|
this.isFocused = true
|
||||||
this.$emit('focus')
|
this.$emit('focus')
|
||||||
},
|
},
|
||||||
blurred() {
|
blurred() {
|
||||||
|
this.isFocused = false
|
||||||
this.$emit('blur')
|
this.$emit('blur')
|
||||||
},
|
},
|
||||||
change(e) {
|
change(e) {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<p class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }">
|
<slot>
|
||||||
{{ label }}<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em>
|
<p class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }">
|
||||||
</p>
|
{{ label }}<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em>
|
||||||
|
</p>
|
||||||
|
</slot>
|
||||||
<ui-text-input ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" class="w-full" @blur="inputBlurred" />
|
<ui-text-input ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" class="w-full" @blur="inputBlurred" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -41,13 +41,6 @@ export default {
|
|||||||
this.setTooltipPosition(this.tooltip)
|
this.setTooltipPosition(this.tooltip)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getTextWidth() {
|
|
||||||
var styles = {
|
|
||||||
'font-size': '0.75rem'
|
|
||||||
}
|
|
||||||
var size = this.$calculateTextSize(this.text, styles)
|
|
||||||
return size.width
|
|
||||||
},
|
|
||||||
createTooltip() {
|
createTooltip() {
|
||||||
if (!this.$refs.box) return
|
if (!this.$refs.box) return
|
||||||
var tooltip = document.createElement('div')
|
var tooltip = document.createElement('div')
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
cardScaleMulitiplier() {
|
cardScaleMulitiplier() {
|
||||||
return this.height / 192
|
return this.height / 192
|
||||||
|
|||||||
@@ -0,0 +1,312 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full py-2">
|
||||||
|
<div class="flex -mb-px">
|
||||||
|
<div class="w-1/2 h-8 rounded-tl-md relative border border-black-200 flex items-center justify-center cursor-pointer" :class="!showAdvancedView ? 'text-white bg-bg hover:bg-opacity-60 border-b-bg' : 'text-gray-400 hover:text-gray-300 bg-primary bg-opacity-70 hover:bg-opacity-60'" @click="showAdvancedView = false">
|
||||||
|
<p class="text-sm">Scheduler</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-1/2 h-8 rounded-tr-md relative border border-black-200 flex items-center justify-center -ml-px cursor-pointer" :class="showAdvancedView ? 'text-white bg-bg hover:bg-opacity-60 border-b-bg' : 'text-gray-400 hover:text-gray-300 bg-primary bg-opacity-70 hover:bg-opacity-60'" @click="showAdvancedView = true">
|
||||||
|
<p class="text-sm">Advanced</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-2 py-4 md:p-4 border border-black-200 rounded-b-md mr-px" style="min-height: 280px">
|
||||||
|
<template v-if="!showAdvancedView">
|
||||||
|
<ui-dropdown v-model="selectedInterval" @input="updateCron" label="Interval" :items="intervalOptions" class="mb-2" />
|
||||||
|
|
||||||
|
<ui-multi-select-dropdown v-if="selectedInterval === 'custom'" v-model="selectedWeekdays" @input="updateCron" label="Weekdays to run" :items="weekdays" />
|
||||||
|
|
||||||
|
<div v-if="(selectedWeekdays.length && selectedInterval === 'custom') || selectedInterval === 'daily'" 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-2 md:p-4 text-center mt-2">
|
||||||
|
<p class="text-base md: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 ref="customExpressionInput" v-model="customCronExpression" @blur="cronExpressionBlur" label="Cron Expression" :padding-y="2" text-center class="w-full text-2xl md:text-4xl -tracking-widest mb-4 font-mono" />
|
||||||
|
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<widgets-loading-spinner v-if="isValidating" class="mr-2" />
|
||||||
|
<span v-else class="material-icons-outlined mr-2 text-xl" :class="isValid ? 'text-success' : 'text-error'">{{ isValid ? 'check_circle_outline' : 'error_outline' }}</span>
|
||||||
|
<p v-if="isValidating" class="text-gray-300 text-base md:text-lg text-center">Checking cron...</p>
|
||||||
|
<p v-else-if="customCronError" class="text-error text-base md:text-lg text-center">{{ customCronError }}</p>
|
||||||
|
<p v-else class="text-success text-base md:text-lg text-center">Valid cron expression</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedInterval: 'custom',
|
||||||
|
showAdvancedView: false,
|
||||||
|
selectedHour: 0,
|
||||||
|
selectedMinute: 0,
|
||||||
|
selectedWeekdays: [],
|
||||||
|
cronExpression: '0 0 * * *',
|
||||||
|
customCronExpression: '0 0 * * *',
|
||||||
|
customCronError: '',
|
||||||
|
isValidating: false,
|
||||||
|
validatedCron: null,
|
||||||
|
isValid: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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.selectedInterval !== 'custom' || !this.selectedWeekdays.length) && this.selectedInterval !== 'daily') 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 '
|
||||||
|
var weekdayTexts = ''
|
||||||
|
if (this.selectedWeekdays.length === 7 || this.selectedInterval === 'daily') {
|
||||||
|
weekdayTexts = 'day'
|
||||||
|
} else {
|
||||||
|
weekdayTexts = 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
|
||||||
|
},
|
||||||
|
intervalOptions() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: 'Custom daily/weekly',
|
||||||
|
value: 'custom'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Every day',
|
||||||
|
value: 'daily'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Every 12 hours',
|
||||||
|
value: '0 */12 * * *'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Every 6 hours',
|
||||||
|
value: '0 */6 * * *'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Every 2 hours',
|
||||||
|
value: '0 */2 * * *'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Every hour',
|
||||||
|
value: '0 * * * *'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Every 30 minutes',
|
||||||
|
value: '*/30 * * * *'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Every 15 minutes',
|
||||||
|
value: '*/15 * * * *'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
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: {
|
||||||
|
checkBlurExpressionInput() {
|
||||||
|
if (!this.showAdvancedView || !this.$refs.customExpressionInput) return false
|
||||||
|
if (this.$refs.customExpressionInput.isFocused) {
|
||||||
|
this.$refs.customExpressionInput.blur()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
updateCron() {
|
||||||
|
if (this.selectedInterval === 'custom') {
|
||||||
|
if (!this.minuteIsValid || !this.hourIsValid || !this.selectedWeekdays.length) {
|
||||||
|
this.cronExpression = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.selectedWeekdays.sort()
|
||||||
|
|
||||||
|
const daysOfWeekPiece = this.selectedWeekdays.length === 7 ? '*' : this.selectedWeekdays.join(',')
|
||||||
|
this.cronExpression = `${this.selectedMinute} ${this.selectedHour} * * ${daysOfWeekPiece}`
|
||||||
|
} else if (this.selectedInterval === 'daily') {
|
||||||
|
if (!this.minuteIsValid || !this.hourIsValid) {
|
||||||
|
this.cronExpression = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.cronExpression = `${this.selectedMinute} ${this.selectedHour} * * *`
|
||||||
|
} else {
|
||||||
|
this.cronExpression = this.selectedInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
this.customCronExpression = this.cronExpression
|
||||||
|
this.validatedCron = this.cronExpression
|
||||||
|
this.isValid = true
|
||||||
|
this.customCronError = ''
|
||||||
|
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()
|
||||||
|
},
|
||||||
|
async cronExpressionBlur() {
|
||||||
|
this.customCronError = ''
|
||||||
|
if (!this.customCronExpression || this.customCronExpression.split(' ').length !== 5) {
|
||||||
|
this.customCronError = 'Invalid cron expression'
|
||||||
|
this.isValid = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.customCronExpression !== this.cronExpression) {
|
||||||
|
this.selectedWeekdays = []
|
||||||
|
this.selectedHour = 0
|
||||||
|
this.selectedMinute = 0
|
||||||
|
this.cronExpression = this.customCronExpression
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.validatedCron || this.validatedCron !== this.cronExpression) {
|
||||||
|
const validationPayload = await this.validateCron()
|
||||||
|
this.isValid = validationPayload.isValid
|
||||||
|
this.validatedCron = this.cronExpression
|
||||||
|
this.customCronError = validationPayload.error || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isValid) {
|
||||||
|
this.$emit('input', this.cronExpression)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
validateCron() {
|
||||||
|
this.isValidating = true
|
||||||
|
return this.$axios
|
||||||
|
.$post('/api/validate-cron', { expression: this.customCronExpression })
|
||||||
|
.then(() => {
|
||||||
|
this.isValidating = false
|
||||||
|
return {
|
||||||
|
isValid: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Invalid cron', error)
|
||||||
|
var errMsg = error.response ? error.response.data || '' : ''
|
||||||
|
this.isValidating = false
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
error: errMsg || 'Invalid cron expression'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
if (!this.value) return
|
||||||
|
const pieces = this.value.split(' ')
|
||||||
|
if (pieces.length !== 5) {
|
||||||
|
console.error('Invalid cron expression input', this.value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const intervalMatch = this.intervalOptions.find((opt) => opt.value === this.value)
|
||||||
|
if (intervalMatch) {
|
||||||
|
this.selectedInterval = this.value
|
||||||
|
} else {
|
||||||
|
var isCustomCron = false
|
||||||
|
if (isNaN(pieces[0]) || isNaN(pieces[1])) {
|
||||||
|
isCustomCron = true
|
||||||
|
} else if (pieces[2] !== '*' || pieces[3] !== '*') {
|
||||||
|
isCustomCron = true
|
||||||
|
} else if (pieces[4] !== '*' && pieces[4].split(',').some((num) => isNaN(num))) {
|
||||||
|
isCustomCron = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCustomCron) {
|
||||||
|
this.showAdvancedView = true
|
||||||
|
} else {
|
||||||
|
if (pieces[4] === '*') this.selectedInterval = 'daily'
|
||||||
|
|
||||||
|
this.selectedWeekdays = pieces[4] === '*' ? [0, 1, 2, 3, 4, 5, 6] : pieces[4].split(',').map((num) => Number(num))
|
||||||
|
this.selectedHour = pieces[1]
|
||||||
|
this.selectedMinute = pieces[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.cronExpression = this.value
|
||||||
|
this.customCronExpression = this.value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -46,7 +46,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
cardScaleMulitiplier() {
|
cardScaleMulitiplier() {
|
||||||
return this.height / 192
|
return this.height / 192
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
cardScaleMulitiplier() {
|
cardScaleMulitiplier() {
|
||||||
return this.height / 192
|
return this.height / 192
|
||||||
|
|||||||
@@ -39,10 +39,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-grow px-1 pt-6">
|
|
||||||
<ui-checkbox v-model="autoDownloadEpisodes" label="Auto Download New Episodes" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -71,7 +67,6 @@ export default {
|
|||||||
explicit: false,
|
explicit: false,
|
||||||
language: null
|
language: null
|
||||||
},
|
},
|
||||||
autoDownloadEpisodes: false,
|
|
||||||
newTags: []
|
newTags: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -196,10 +191,6 @@ export default {
|
|||||||
updatePayload.tags = [...this.newTags]
|
updatePayload.tags = [...this.newTags]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.media.autoDownloadEpisodes !== this.autoDownloadEpisodes) {
|
|
||||||
updatePayload.autoDownloadEpisodes = !!this.autoDownloadEpisodes
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
updatePayload,
|
updatePayload,
|
||||||
hasChanges: !!Object.keys(updatePayload).length
|
hasChanges: !!Object.keys(updatePayload).length
|
||||||
@@ -219,7 +210,6 @@ export default {
|
|||||||
this.details.language = this.mediaMetadata.language || ''
|
this.details.language = this.mediaMetadata.language || ''
|
||||||
this.details.explicit = !!this.mediaMetadata.explicit
|
this.details.explicit = !!this.mediaMetadata.explicit
|
||||||
|
|
||||||
this.autoDownloadEpisodes = !!this.media.autoDownloadEpisodes
|
|
||||||
this.newTags = [...(this.media.tags || [])]
|
this.newTags = [...(this.media.tags || [])]
|
||||||
},
|
},
|
||||||
submitForm() {
|
submitForm() {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
cardScaleMulitiplier() {
|
cardScaleMulitiplier() {
|
||||||
return this.height / 192
|
return this.height / 192
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export default {
|
|||||||
index,
|
index,
|
||||||
width: this.entityWidth,
|
width: this.entityWidth,
|
||||||
height: this.entityHeight,
|
height: this.entityHeight,
|
||||||
bookCoverAspectRatio: this.bookCoverAspectRatio,
|
bookCoverAspectRatio: this.coverAspectRatio,
|
||||||
bookshelfView: this.bookshelfView,
|
bookshelfView: this.bookshelfView,
|
||||||
sortingIgnorePrefix: !!this.sortingIgnorePrefix
|
sortingIgnorePrefix: !!this.sortingIgnorePrefix
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ module.exports = {
|
|||||||
'@/plugins/constants.js',
|
'@/plugins/constants.js',
|
||||||
'@/plugins/init.client.js',
|
'@/plugins/init.client.js',
|
||||||
'@/plugins/axios.js',
|
'@/plugins/axios.js',
|
||||||
'@/plugins/toast.js'
|
'@/plugins/toast.js',
|
||||||
|
'@/plugins/utils.js'
|
||||||
],
|
],
|
||||||
|
|
||||||
// Auto import components: https://go.nuxtjs.dev/config-components
|
// Auto import components: https://go.nuxtjs.dev/config-components
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.1.2",
|
"version": "2.1.3",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.1.2",
|
"version": "2.1.3",
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -144,12 +144,6 @@ export default {
|
|||||||
isPodcastLibrary() {
|
isPodcastLibrary() {
|
||||||
return this.mediaType === 'podcast'
|
return this.mediaType === 'podcast'
|
||||||
},
|
},
|
||||||
coverAspectRatio() {
|
|
||||||
return this.$store.getters['getServerSetting']('coverAspectRatio')
|
|
||||||
},
|
|
||||||
bookCoverAspectRatio() {
|
|
||||||
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6
|
|
||||||
},
|
|
||||||
streamLibraryItem() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamLibraryItem
|
return this.$store.state.streamLibraryItem
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
streamLibraryItem() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamLibraryItem
|
return this.$store.state.streamLibraryItem
|
||||||
|
|||||||
@@ -5,20 +5,22 @@
|
|||||||
<h1 class="text-xl">Backups</h1>
|
<h1 class="text-xl">Backups</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-base mb-4 text-gray-300">Backups include users, user progress, book details, server settings and covers stored in <span class="font-mono text-gray-100">/metadata/items</span>. <br />Backups <strong>do not</strong> include any files stored in your library folders.</p>
|
<p class="text-base mb-4 text-gray-300">Backups include users, user progress, library item details, server settings, and images stored in <span class="font-mono text-gray-100">/metadata/items</span> & <span class="font-mono text-gray-100">/metadata/authors</span>. <br />Backups <strong>do not</strong> include any files stored in your library folders.</p>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="dailyBackups" small :disabled="updatingServerSettings" @input="updateBackupsSettings" />
|
<ui-toggle-switch v-model="enableBackups" small :disabled="updatingServerSettings" @input="updateBackupsSettings" />
|
||||||
<ui-tooltip :text="dailyBackupsTooltip">
|
<ui-tooltip :text="backupsTooltip">
|
||||||
<p class="pl-4 text-lg">Run daily backups <span class="material-icons icon-text">info_outlined</span></p>
|
<p class="pl-4 text-lg">Enable automatic backups <span class="material-icons icon-text">info_outlined</span></p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div class="flex items-center py-2">
|
<div v-if="enableBackups" class="mb-6">
|
||||||
<ui-text-input v-model="cronExpression" :disabled="updatingServerSettings" class="w-32" @change="changedCronExpression" />
|
<div class="flex items-center pl-6">
|
||||||
|
<span class="material-icons-outlined text-black-50">schedule</span>
|
||||||
<p class="pl-4 text-lg">Cron expression</p>
|
<p class="text-gray-100 px-2">{{ scheduleDescription }}</p>
|
||||||
</div> -->
|
<span class="material-icons text-lg text-black-50 hover:text-yellow-500 cursor-pointer" @click="showCronBuilder = !showCronBuilder">edit</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-text-input type="number" v-model="backupsToKeep" no-spinner :disabled="updatingServerSettings" :padding-x="1" text-center class="w-10" @change="updateBackupsSettings" />
|
<ui-text-input type="number" v-model="backupsToKeep" no-spinner :disabled="updatingServerSettings" :padding-x="1" text-center class="w-10" @change="updateBackupsSettings" />
|
||||||
@@ -36,6 +38,8 @@
|
|||||||
|
|
||||||
<tables-backups-table />
|
<tables-backups-table />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<modals-backup-schedule-modal v-model="showCronBuilder" :cron-expression.sync="cronExpression" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -44,11 +48,12 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
updatingServerSettings: false,
|
updatingServerSettings: false,
|
||||||
dailyBackups: true,
|
enableBackups: true,
|
||||||
backupsToKeep: 2,
|
backupsToKeep: 2,
|
||||||
maxBackupSize: 1,
|
maxBackupSize: 1,
|
||||||
// cronExpression: '',
|
cronExpression: '',
|
||||||
newServerSettings: {}
|
newServerSettings: {},
|
||||||
|
showCronBuilder: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -60,29 +65,22 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
dailyBackupsTooltip() {
|
backupsTooltip() {
|
||||||
return 'Runs at 1:30am every day (your server time). Saved in /metadata/backups.'
|
return 'Backups saved in /metadata/backups'
|
||||||
},
|
},
|
||||||
maxBackupSizeTooltip() {
|
maxBackupSizeTooltip() {
|
||||||
return 'As a safeguard against misconfiguration, backups will fail if they exceed the configured size.'
|
return 'As a safeguard against misconfiguration, backups will fail if they exceed the configured size.'
|
||||||
},
|
},
|
||||||
serverSettings() {
|
serverSettings() {
|
||||||
return this.$store.state.serverSettings
|
return this.$store.state.serverSettings
|
||||||
|
},
|
||||||
|
scheduleDescription() {
|
||||||
|
if (!this.cronExpression) return ''
|
||||||
|
const parsed = this.$parseCronExpression(this.cronExpression)
|
||||||
|
return parsed ? parsed.description : 'Custom cron expression ' + this.cronExpression
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// changedCronExpression() {
|
|
||||||
// this.$axios
|
|
||||||
// .$post('/api/validate-cron', { expression: this.cronExpression })
|
|
||||||
// .then(() => {
|
|
||||||
// console.log('Cron is valid')
|
|
||||||
// })
|
|
||||||
// .catch((error) => {
|
|
||||||
// console.error('Cron validation failed', error)
|
|
||||||
// const msg = (error.response ? error.response.data : null) || 'Unknown cron validation error'
|
|
||||||
// this.$toast.error(msg)
|
|
||||||
// })
|
|
||||||
// },
|
|
||||||
updateBackupsSettings() {
|
updateBackupsSettings() {
|
||||||
if (isNaN(this.maxBackupSize) || this.maxBackupSize <= 0) {
|
if (isNaN(this.maxBackupSize) || this.maxBackupSize <= 0) {
|
||||||
this.$toast.error('Invalid maximum backup size')
|
this.$toast.error('Invalid maximum backup size')
|
||||||
@@ -93,7 +91,7 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var updatePayload = {
|
var updatePayload = {
|
||||||
backupSchedule: this.dailyBackups ? '30 1 * * *' : false,
|
backupSchedule: this.enableBackups ? this.cronExpression : false,
|
||||||
backupsToKeep: Number(this.backupsToKeep),
|
backupsToKeep: Number(this.backupsToKeep),
|
||||||
maxBackupSize: Number(this.maxBackupSize)
|
maxBackupSize: Number(this.maxBackupSize)
|
||||||
}
|
}
|
||||||
@@ -116,9 +114,9 @@ export default {
|
|||||||
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
||||||
|
|
||||||
this.backupsToKeep = this.newServerSettings.backupsToKeep || 2
|
this.backupsToKeep = this.newServerSettings.backupsToKeep || 2
|
||||||
this.dailyBackups = !!this.newServerSettings.backupSchedule
|
this.enableBackups = !!this.newServerSettings.backupSchedule
|
||||||
this.maxBackupSize = this.newServerSettings.maxBackupSize || 1
|
this.maxBackupSize = this.newServerSettings.maxBackupSize || 1
|
||||||
// this.cronExpression = '30 1 * * *'
|
this.cronExpression = this.newServerSettings.backupSchedule || '30 1 * * *'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|||||||
@@ -53,10 +53,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="useSquareBookCovers" :disabled="updatingServerSettings" @input="updateBookCoverAspectRatio" />
|
<ui-toggle-switch v-model="homeUseAlternativeBookshelfView" :disabled="updatingServerSettings" @input="updateHomeAlternativeBookshelfView" />
|
||||||
<ui-tooltip :text="tooltips.coverAspectRatio">
|
<ui-tooltip :text="tooltips.bookshelfView">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
Square book covers
|
Alternative bookshelf view for home page
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
@@ -270,7 +270,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
isResettingLibraryItems: false,
|
isResettingLibraryItems: false,
|
||||||
updatingServerSettings: false,
|
updatingServerSettings: false,
|
||||||
useSquareBookCovers: false,
|
homeUseAlternativeBookshelfView: false,
|
||||||
useAlternativeBookshelfView: false,
|
useAlternativeBookshelfView: false,
|
||||||
isPurgingCache: false,
|
isPurgingCache: false,
|
||||||
newServerSettings: {},
|
newServerSettings: {},
|
||||||
@@ -362,6 +362,11 @@ export default {
|
|||||||
coverAspectRatio: val ? this.$constants.BookCoverAspectRatio.SQUARE : this.$constants.BookCoverAspectRatio.STANDARD
|
coverAspectRatio: val ? this.$constants.BookCoverAspectRatio.SQUARE : this.$constants.BookCoverAspectRatio.STANDARD
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
updateHomeAlternativeBookshelfView(val) {
|
||||||
|
this.updateServerSettings({
|
||||||
|
homeBookshelfView: val ? this.$constants.BookshelfView.TITLES : this.$constants.BookshelfView.STANDARD
|
||||||
|
})
|
||||||
|
},
|
||||||
updateAlternativeBookshelfView(val) {
|
updateAlternativeBookshelfView(val) {
|
||||||
this.updateServerSettings({
|
this.updateServerSettings({
|
||||||
bookshelfView: val ? this.$constants.BookshelfView.TITLES : this.$constants.BookshelfView.STANDARD
|
bookshelfView: val ? this.$constants.BookshelfView.TITLES : this.$constants.BookshelfView.STANDARD
|
||||||
@@ -391,8 +396,7 @@ export default {
|
|||||||
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
||||||
this.newServerSettings.sortingPrefixes = [...(this.newServerSettings.sortingPrefixes || [])]
|
this.newServerSettings.sortingPrefixes = [...(this.newServerSettings.sortingPrefixes || [])]
|
||||||
|
|
||||||
this.useSquareBookCovers = this.newServerSettings.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE
|
this.homeUseAlternativeBookshelfView = this.newServerSettings.homeBookshelfView === this.$constants.BookshelfView.TITLES
|
||||||
|
|
||||||
this.useAlternativeBookshelfView = this.newServerSettings.bookshelfView === this.$constants.BookshelfView.TITLES
|
this.useAlternativeBookshelfView = this.newServerSettings.bookshelfView === this.$constants.BookshelfView.TITLES
|
||||||
},
|
},
|
||||||
resetLibraryItems() {
|
resetLibraryItems() {
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
<p v-else class="text-white text-opacity-50">No sessions yet...</p>
|
<p v-else class="text-white text-opacity-50">No sessions yet...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<modals-listening-session-modal v-model="showSessionModal" :session="selectedSession" />
|
<modals-listening-session-modal v-model="showSessionModal" :session="selectedSession" @removedSession="removedSession" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -111,6 +111,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
removedSession() {
|
||||||
|
this.loadSessions(this.currentPage)
|
||||||
|
},
|
||||||
async clickCurrentTime(session) {
|
async clickCurrentTime(session) {
|
||||||
if (this.processingGoToTimestamp) return
|
if (this.processingGoToTimestamp) return
|
||||||
this.processingGoToTimestamp = true
|
this.processingGoToTimestamp = true
|
||||||
|
|||||||
@@ -105,11 +105,8 @@ export default {
|
|||||||
userToken() {
|
userToken() {
|
||||||
return this.user.token
|
return this.user.token
|
||||||
},
|
},
|
||||||
coverAspectRatio() {
|
|
||||||
return this.$store.getters['getServerSetting']('coverAspectRatio')
|
|
||||||
},
|
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
username() {
|
username() {
|
||||||
return this.user.username
|
return this.user.username
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<modals-listening-session-modal v-model="showSessionModal" :session="selectedSession" />
|
<modals-listening-session-modal v-model="showSessionModal" :session="selectedSession" @removedSession="removedSession" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -98,6 +98,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
removedSession() {
|
||||||
|
this.loadSessions(this.currentPage)
|
||||||
|
},
|
||||||
async clickCurrentTime(session) {
|
async clickCurrentTime(session) {
|
||||||
if (this.processingGoToTimestamp) return
|
if (this.processingGoToTimestamp) return
|
||||||
this.processingGoToTimestamp = true
|
this.processingGoToTimestamp = true
|
||||||
|
|||||||
@@ -24,12 +24,13 @@
|
|||||||
<div class="flex-grow px-2 py-6 md:py-0 md:px-10">
|
<div class="flex-grow px-2 py-6 md:py-0 md:px-10">
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<div class="flex sm:items-end flex-col sm:flex-row">
|
<h1 class="text-2xl md:text-3xl font-semibold">
|
||||||
<h1 class="text-2xl md:text-3xl font-sans">
|
{{ title }}
|
||||||
{{ title }}
|
</h1>
|
||||||
</h1>
|
|
||||||
<p v-if="bookSubtitle" class="sm:ml-4 text-gray-400 text-xl md:text-2xl">{{ bookSubtitle }}</p>
|
<p v-if="bookSubtitle" class="text-gray-200 text-xl md:text-2xl">{{ bookSubtitle }}</p>
|
||||||
</div>
|
|
||||||
|
<nuxt-link v-for="_series in seriesList" :key="_series.id" :to="`/library/${libraryId}/series/${_series.id}`" class="hover:underline font-sans text-gray-300 text-lg leading-7"> {{ _series.text }}</nuxt-link>
|
||||||
|
|
||||||
<template v-if="!isVideo">
|
<template v-if="!isVideo">
|
||||||
<p v-if="isPodcast" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">by {{ podcastAuthor || 'Unknown' }}</p>
|
<p v-if="isPodcast" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">by {{ podcastAuthor || 'Unknown' }}</p>
|
||||||
@@ -39,8 +40,6 @@
|
|||||||
<p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p>
|
<p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<nuxt-link v-for="_series in seriesList" :key="_series.id" :to="`/library/${libraryId}/series/${_series.id}`" class="hover:underline font-sans text-gray-300 text-lg leading-7"> {{ _series.text }}</nuxt-link>
|
|
||||||
|
|
||||||
<div v-if="narrator" class="flex py-0.5 mt-4">
|
<div v-if="narrator" class="flex py-0.5 mt-4">
|
||||||
<div class="w-32">
|
<div class="w-32">
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">Narrated By</span>
|
<span class="text-white text-opacity-60 uppercase text-sm">Narrated By</span>
|
||||||
@@ -247,11 +246,8 @@ export default {
|
|||||||
isFile() {
|
isFile() {
|
||||||
return this.libraryItem.isFile
|
return this.libraryItem.isFile
|
||||||
},
|
},
|
||||||
coverAspectRatio() {
|
|
||||||
return this.$store.getters['getServerSetting']('coverAspectRatio')
|
|
||||||
},
|
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
bookCoverWidth() {
|
bookCoverWidth() {
|
||||||
return 208
|
return 208
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ const Hotkeys = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Constants
|
||||||
|
}
|
||||||
export default ({ app }, inject) => {
|
export default ({ app }, inject) => {
|
||||||
inject('constants', Constants)
|
inject('constants', Constants)
|
||||||
inject('keynames', KeyNames)
|
inject('keynames', KeyNames)
|
||||||
|
|||||||
@@ -30,92 +30,6 @@ Vue.prototype.$addDaysToDate = (jsdate, daysToAdd) => {
|
|||||||
return date
|
return date
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.prototype.$bytesPretty = (bytes, decimals = 2) => {
|
|
||||||
if (isNaN(bytes) || bytes == 0) {
|
|
||||||
return '0 Bytes'
|
|
||||||
}
|
|
||||||
const k = 1024
|
|
||||||
const dm = decimals < 0 ? 0 : decimals
|
|
||||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
Vue.prototype.$elapsedPretty = (seconds, useFullNames = false) => {
|
|
||||||
if (seconds < 60) {
|
|
||||||
return `${Math.floor(seconds)} sec${useFullNames ? 'onds' : ''}`
|
|
||||||
}
|
|
||||||
var minutes = Math.floor(seconds / 60)
|
|
||||||
if (minutes < 70) {
|
|
||||||
return `${minutes} min${useFullNames ? `ute${minutes === 1 ? '' : 's'}` : ''}`
|
|
||||||
}
|
|
||||||
var hours = Math.floor(minutes / 60)
|
|
||||||
minutes -= hours * 60
|
|
||||||
if (!minutes) {
|
|
||||||
return `${hours} ${useFullNames ? 'hours' : 'hr'}`
|
|
||||||
}
|
|
||||||
return `${hours} ${useFullNames ? `hour${hours === 1 ? '' : 's'}` : 'hr'} ${minutes} ${useFullNames ? `minute${minutes === 1 ? '' : 's'}` : 'min'}`
|
|
||||||
}
|
|
||||||
|
|
||||||
Vue.prototype.$secondsToTimestamp = (seconds) => {
|
|
||||||
if (!seconds) return '0:00'
|
|
||||||
var _seconds = seconds
|
|
||||||
var _minutes = Math.floor(seconds / 60)
|
|
||||||
_seconds -= _minutes * 60
|
|
||||||
var _hours = Math.floor(_minutes / 60)
|
|
||||||
_minutes -= _hours * 60
|
|
||||||
_seconds = Math.floor(_seconds)
|
|
||||||
if (!_hours) {
|
|
||||||
return `${_minutes}:${_seconds.toString().padStart(2, '0')}`
|
|
||||||
}
|
|
||||||
return `${_hours}:${_minutes.toString().padStart(2, '0')}:${_seconds.toString().padStart(2, '0')}`
|
|
||||||
}
|
|
||||||
|
|
||||||
Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true) => {
|
|
||||||
if (isNaN(seconds) || seconds === null) return ''
|
|
||||||
seconds = Math.round(seconds)
|
|
||||||
|
|
||||||
var minutes = Math.floor(seconds / 60)
|
|
||||||
seconds -= minutes * 60
|
|
||||||
var hours = Math.floor(minutes / 60)
|
|
||||||
minutes -= hours * 60
|
|
||||||
|
|
||||||
var days = 0
|
|
||||||
if (useDays || Math.floor(hours / 24) >= 100) {
|
|
||||||
days = Math.floor(hours / 24)
|
|
||||||
hours -= days * 24
|
|
||||||
}
|
|
||||||
|
|
||||||
var strs = []
|
|
||||||
if (days) strs.push(`${days}d`)
|
|
||||||
if (hours) strs.push(`${hours}h`)
|
|
||||||
if (minutes) strs.push(`${minutes}m`)
|
|
||||||
if (seconds) strs.push(`${seconds}s`)
|
|
||||||
return strs.join(' ')
|
|
||||||
}
|
|
||||||
|
|
||||||
Vue.prototype.$calculateTextSize = (text, styles = {}) => {
|
|
||||||
const el = document.createElement('p')
|
|
||||||
|
|
||||||
let attr = 'margin:0px;opacity:1;position:absolute;top:100px;left:100px;z-index:99;'
|
|
||||||
for (const key in styles) {
|
|
||||||
if (styles[key] && String(styles[key]).length > 0) {
|
|
||||||
attr += `${key}:${styles[key]};`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
el.setAttribute('style', attr)
|
|
||||||
el.innerText = text
|
|
||||||
|
|
||||||
document.body.appendChild(el)
|
|
||||||
const boundingBox = el.getBoundingClientRect()
|
|
||||||
el.remove()
|
|
||||||
return {
|
|
||||||
height: boundingBox.height,
|
|
||||||
width: boundingBox.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Vue.prototype.$sanitizeFilename = (input, colonReplacement = ' - ') => {
|
Vue.prototype.$sanitizeFilename = (input, colonReplacement = ' - ') => {
|
||||||
if (typeof input !== 'string') {
|
if (typeof input !== 'string') {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
Vue.prototype.$bytesPretty = (bytes, decimals = 2) => {
|
||||||
|
if (isNaN(bytes) || bytes == 0) {
|
||||||
|
return '0 Bytes'
|
||||||
|
}
|
||||||
|
const k = 1024
|
||||||
|
const dm = decimals < 0 ? 0 : decimals
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.prototype.$elapsedPretty = (seconds, useFullNames = false) => {
|
||||||
|
if (seconds < 60) {
|
||||||
|
return `${Math.floor(seconds)} sec${useFullNames ? 'onds' : ''}`
|
||||||
|
}
|
||||||
|
var minutes = Math.floor(seconds / 60)
|
||||||
|
if (minutes < 70) {
|
||||||
|
return `${minutes} min${useFullNames ? `ute${minutes === 1 ? '' : 's'}` : ''}`
|
||||||
|
}
|
||||||
|
var hours = Math.floor(minutes / 60)
|
||||||
|
minutes -= hours * 60
|
||||||
|
if (!minutes) {
|
||||||
|
return `${hours} ${useFullNames ? 'hours' : 'hr'}`
|
||||||
|
}
|
||||||
|
return `${hours} ${useFullNames ? `hour${hours === 1 ? '' : 's'}` : 'hr'} ${minutes} ${useFullNames ? `minute${minutes === 1 ? '' : 's'}` : 'min'}`
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.prototype.$secondsToTimestamp = (seconds) => {
|
||||||
|
if (!seconds) return '0:00'
|
||||||
|
var _seconds = seconds
|
||||||
|
var _minutes = Math.floor(seconds / 60)
|
||||||
|
_seconds -= _minutes * 60
|
||||||
|
var _hours = Math.floor(_minutes / 60)
|
||||||
|
_minutes -= _hours * 60
|
||||||
|
_seconds = Math.floor(_seconds)
|
||||||
|
if (!_hours) {
|
||||||
|
return `${_minutes}:${_seconds.toString().padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
return `${_hours}:${_minutes.toString().padStart(2, '0')}:${_seconds.toString().padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true) => {
|
||||||
|
if (isNaN(seconds) || seconds === null) return ''
|
||||||
|
seconds = Math.round(seconds)
|
||||||
|
|
||||||
|
var minutes = Math.floor(seconds / 60)
|
||||||
|
seconds -= minutes * 60
|
||||||
|
var hours = Math.floor(minutes / 60)
|
||||||
|
minutes -= hours * 60
|
||||||
|
|
||||||
|
var days = 0
|
||||||
|
if (useDays || Math.floor(hours / 24) >= 100) {
|
||||||
|
days = Math.floor(hours / 24)
|
||||||
|
hours -= days * 24
|
||||||
|
}
|
||||||
|
|
||||||
|
var strs = []
|
||||||
|
if (days) strs.push(`${days}d`)
|
||||||
|
if (hours) strs.push(`${hours}h`)
|
||||||
|
if (minutes) strs.push(`${minutes}m`)
|
||||||
|
if (seconds) strs.push(`${seconds}s`)
|
||||||
|
return strs.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.prototype.$parseCronExpression = (expression) => {
|
||||||
|
if (!expression) return null
|
||||||
|
const pieces = expression.split(' ')
|
||||||
|
if (pieces.length !== 5) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const commonPatterns = [
|
||||||
|
{
|
||||||
|
text: 'Every 12 hours',
|
||||||
|
value: '0 */12 * * *'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Every 6 hours',
|
||||||
|
value: '0 */6 * * *'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Every 2 hours',
|
||||||
|
value: '0 */2 * * *'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Every hour',
|
||||||
|
value: '0 * * * *'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Every 30 minutes',
|
||||||
|
value: '*/30 * * * *'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Every 15 minutes',
|
||||||
|
value: '*/15 * * * *'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Every minute',
|
||||||
|
value: '* * * * *'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const patternMatch = commonPatterns.find(p => p.value === expression)
|
||||||
|
if (patternMatch) {
|
||||||
|
return {
|
||||||
|
description: patternMatch.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(pieces[0]) || isNaN(pieces[1])) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (pieces[2] !== '*' || pieces[3] !== '*') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (pieces[4] !== '*' && pieces[4].split(',').some(p => isNaN(p))) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
|
||||||
|
var weekdayText = 'day'
|
||||||
|
if (pieces[4] !== '*') weekdayText = pieces[4].split(',').map(p => weekdays[p]).join(', ')
|
||||||
|
|
||||||
|
return {
|
||||||
|
description: `Run every ${weekdayText} at ${pieces[1]}:${pieces[0].padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { checkForUpdate, currentVersion } from '@/plugins/version'
|
import { checkForUpdate, currentVersion } from '@/plugins/version'
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
const { Constants } = require('../plugins/constants')
|
||||||
|
|
||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
Source: null,
|
Source: null,
|
||||||
@@ -45,6 +46,14 @@ export const getters = {
|
|||||||
if (!state.streamLibraryItem) return null
|
if (!state.streamLibraryItem) return null
|
||||||
if (!episodeId) return state.streamLibraryItem.id == libraryItemId
|
if (!episodeId) return state.streamLibraryItem.id == libraryItemId
|
||||||
return state.streamLibraryItem.id == libraryItemId && state.streamEpisodeId == episodeId
|
return state.streamLibraryItem.id == libraryItemId && state.streamEpisodeId == episodeId
|
||||||
|
},
|
||||||
|
getBookshelfView: state => {
|
||||||
|
if (!state.serverSettings || isNaN(state.serverSettings.bookshelfView)) return Constants.BookshelfView.STANDARD
|
||||||
|
return state.serverSettings.bookshelfView
|
||||||
|
},
|
||||||
|
getHomeBookshelfView: state => {
|
||||||
|
if (!state.serverSettings || isNaN(state.serverSettings.homeBookshelfView)) return Constants.BookshelfView.STANDARD
|
||||||
|
return state.serverSettings.homeBookshelfView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
const { Constants } = require('../plugins/constants')
|
||||||
|
|
||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
libraries: [],
|
libraries: [],
|
||||||
lastLoad: 0,
|
lastLoad: 0,
|
||||||
@@ -42,6 +44,14 @@ export const getters = {
|
|||||||
})
|
})
|
||||||
if (!librariesSorted.length) return null
|
if (!librariesSorted.length) return null
|
||||||
return librariesSorted[0]
|
return librariesSorted[0]
|
||||||
|
},
|
||||||
|
getCurrentLibrarySettings: (state, getters) => {
|
||||||
|
if (!getters.getCurrentLibrary) return null
|
||||||
|
return getters.getCurrentLibrary.settings
|
||||||
|
},
|
||||||
|
getBookCoverAspectRatio: (state, getters) => {
|
||||||
|
if (!getters.getCurrentLibrarySettings || isNaN(getters.getCurrentLibrarySettings.coverAspectRatio)) return 1
|
||||||
|
return getters.getCurrentLibrarySettings.coverAspectRatio === Constants.BookCoverAspectRatio.STANDARD ? 1.6 : 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ export const actions = {
|
|||||||
if (state.settings.orderBy == 'media.duration') {
|
if (state.settings.orderBy == 'media.duration') {
|
||||||
settingsUpdate.orderBy = 'media.numTracks'
|
settingsUpdate.orderBy = 'media.numTracks'
|
||||||
}
|
}
|
||||||
|
if (state.settings.orderBy == 'media.metadata.publishedYear') {
|
||||||
|
settingsUpdate.orderBy = 'media.metadata.title'
|
||||||
|
}
|
||||||
var invalidFilters = ['series', 'authors', 'narrators', 'languages', 'progress', 'issues']
|
var invalidFilters = ['series', 'authors', 'narrators', 'languages', 'progress', 'issues']
|
||||||
var filterByFirstPart = (state.settings.filterBy || '').split('.').shift()
|
var filterByFirstPart = (state.settings.filterBy || '').split('.').shift()
|
||||||
if (invalidFilters.includes(filterByFirstPart)) {
|
if (invalidFilters.includes(filterByFirstPart)) {
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ module.exports = {
|
|||||||
'py-1.5',
|
'py-1.5',
|
||||||
'bg-info',
|
'bg-info',
|
||||||
'px-1.5',
|
'px-1.5',
|
||||||
'min-w-5'
|
'min-w-5',
|
||||||
|
'w-3.5',
|
||||||
|
'h-3.5',
|
||||||
|
'border-warning'
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
@@ -44,6 +47,7 @@ module.exports = {
|
|||||||
minWidth: {
|
minWidth: {
|
||||||
'5': '1.25rem',
|
'5': '1.25rem',
|
||||||
'6': '1.5rem',
|
'6': '1.5rem',
|
||||||
|
'8': '2rem',
|
||||||
'10': '2.5rem',
|
'10': '2.5rem',
|
||||||
'12': '3rem',
|
'12': '3rem',
|
||||||
'16': '4rem',
|
'16': '4rem',
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.1.2",
|
"version": "2.1.3",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.1.2",
|
"version": "2.1.3",
|
||||||
"description": "Self-hosted audiobook and podcast server",
|
"description": "Self-hosted audiobook and podcast server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+27
-29
@@ -121,7 +121,7 @@ class Db {
|
|||||||
async init() {
|
async init() {
|
||||||
await this.load()
|
await this.load()
|
||||||
|
|
||||||
if (!this.serverSettings) {
|
if (!this.serverSettings) { // Create first load server settings
|
||||||
this.serverSettings = new ServerSettings()
|
this.serverSettings = new ServerSettings()
|
||||||
await this.insertEntity('settings', this.serverSettings)
|
await this.insertEntity('settings', this.serverSettings)
|
||||||
}
|
}
|
||||||
@@ -142,7 +142,7 @@ class Db {
|
|||||||
this.libraries.sort((a, b) => a.displayOrder - b.displayOrder)
|
this.libraries.sort((a, b) => a.displayOrder - b.displayOrder)
|
||||||
Logger.info(`[DB] ${this.libraries.length} Libraries Loaded`)
|
Logger.info(`[DB] ${this.libraries.length} Libraries Loaded`)
|
||||||
})
|
})
|
||||||
var p4 = this.settingsDb.select(() => true).then((results) => {
|
var p4 = this.settingsDb.select(() => true).then(async (results) => {
|
||||||
if (results.data && results.data.length) {
|
if (results.data && results.data.length) {
|
||||||
this.settings = results.data
|
this.settings = results.data
|
||||||
var serverSettings = this.settings.find(s => s.id === 'server-settings')
|
var serverSettings = this.settings.find(s => s.id === 'server-settings')
|
||||||
@@ -152,6 +152,18 @@ class Db {
|
|||||||
// Check if server was upgraded
|
// Check if server was upgraded
|
||||||
if (!this.serverSettings.version || this.serverSettings.version !== version) {
|
if (!this.serverSettings.version || this.serverSettings.version !== version) {
|
||||||
this.previousVersion = this.serverSettings.version || '1.0.0'
|
this.previousVersion = this.serverSettings.version || '1.0.0'
|
||||||
|
|
||||||
|
// Library settings and server settings updated in 2.1.3 - run migration
|
||||||
|
if (this.previousVersion.localeCompare('2.1.3') < 0) {
|
||||||
|
Logger.info(`[Db] Running servers & library settings migration`)
|
||||||
|
for (const library of this.libraries) {
|
||||||
|
if (library.settings.coverAspectRatio !== serverSettings.coverAspectRatio) {
|
||||||
|
library.settings.coverAspectRatio = serverSettings.coverAspectRatio
|
||||||
|
await this.updateEntity('library', library)
|
||||||
|
Logger.debug(`[Db] Library ${library.name} migrated`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,17 +195,6 @@ class Db {
|
|||||||
getLibraryItemsInLibrary(libraryId) {
|
getLibraryItemsInLibrary(libraryId) {
|
||||||
return this.libraryItems.filter(li => li.libraryId === libraryId)
|
return this.libraryItems.filter(li => li.libraryId === libraryId)
|
||||||
}
|
}
|
||||||
getPlaybackSession(id) {
|
|
||||||
return this.sessionsDb.select((pb) => pb.id == id).then((results) => {
|
|
||||||
if (results.data.length) {
|
|
||||||
return new PlaybackSession(results.data[0])
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}).catch((error) => {
|
|
||||||
Logger.error('Failed to get session', error)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateLibraryItem(libraryItem) {
|
async updateLibraryItem(libraryItem) {
|
||||||
return this.updateLibraryItems([libraryItem])
|
return this.updateLibraryItems([libraryItem])
|
||||||
@@ -368,22 +369,7 @@ class Db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return entityDb.update((record) => record.id === entity.id, () => jsonEntity).then((results) => {
|
return entityDb.update((record) => record.id === entity.id, () => jsonEntity).then((results) => {
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
Logger.debug(`[DB] Updated ${entityName}: ${results.updated}`)
|
||||||
Logger.debug(`[DB] Updated ${entityName}: ${results.updated} | Selected: ${results.selected}`)
|
|
||||||
|
|
||||||
if (!results.selected) {
|
|
||||||
entityDb.select(match => match.id == jsonEntity.id).then((results) => {
|
|
||||||
if (results.data.length) {
|
|
||||||
console.log('Said selected 0 but found it right here...', results.data[0].id)
|
|
||||||
} else {
|
|
||||||
console.log('Said selected 0 and no results for json entity id', jsonEntity.id)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Logger.debug(`[DB] Updated ${entityName}: ${results.updated}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
var arrayKey = this.getEntityArrayKey(entityName)
|
var arrayKey = this.getEntityArrayKey(entityName)
|
||||||
if (this[arrayKey]) {
|
if (this[arrayKey]) {
|
||||||
this[arrayKey] = this[arrayKey].map(e => {
|
this[arrayKey] = this[arrayKey].map(e => {
|
||||||
@@ -452,6 +438,18 @@ class Db {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPlaybackSession(id) {
|
||||||
|
return this.sessionsDb.select((pb) => pb.id == id).then((results) => {
|
||||||
|
if (results.data.length) {
|
||||||
|
return new PlaybackSession(results.data[0])
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}).catch((error) => {
|
||||||
|
Logger.error('Failed to get session', error)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
selectUserSessions(userId) {
|
selectUserSessions(userId) {
|
||||||
return this.sessionsDb.select((session) => session.userId === userId).then((results) => {
|
return this.sessionsDb.select((session) => session.userId === userId).then((results) => {
|
||||||
return results.data || []
|
return results.data || []
|
||||||
|
|||||||
+4
-2
@@ -32,6 +32,7 @@ const PlaybackSessionManager = require('./managers/PlaybackSessionManager')
|
|||||||
const PodcastManager = require('./managers/PodcastManager')
|
const PodcastManager = require('./managers/PodcastManager')
|
||||||
const AudioMetadataMangaer = require('./managers/AudioMetadataManager')
|
const AudioMetadataMangaer = require('./managers/AudioMetadataManager')
|
||||||
const RssFeedManager = require('./managers/RssFeedManager')
|
const RssFeedManager = require('./managers/RssFeedManager')
|
||||||
|
const CronManager = require('./managers/CronManager')
|
||||||
|
|
||||||
class Server {
|
class Server {
|
||||||
constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH) {
|
constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH) {
|
||||||
@@ -74,9 +75,10 @@ class Server {
|
|||||||
this.rssFeedManager = new RssFeedManager(this.db, this.emitter.bind(this))
|
this.rssFeedManager = new RssFeedManager(this.db, this.emitter.bind(this))
|
||||||
|
|
||||||
this.scanner = new Scanner(this.db, this.coverManager, this.emitter.bind(this))
|
this.scanner = new Scanner(this.db, this.coverManager, this.emitter.bind(this))
|
||||||
|
this.cronManager = new CronManager(this.db, this.scanner, this.podcastManager)
|
||||||
|
|
||||||
// Routers
|
// Routers
|
||||||
this.apiRouter = new ApiRouter(this.db, this.auth, this.scanner, this.playbackSessionManager, this.abMergeManager, this.coverManager, this.backupManager, this.watcher, this.cacheManager, this.podcastManager, this.audioMetadataManager, this.rssFeedManager, this.emitter.bind(this), this.clientEmitter.bind(this))
|
this.apiRouter = new ApiRouter(this.db, this.auth, this.scanner, this.playbackSessionManager, this.abMergeManager, this.coverManager, this.backupManager, this.watcher, this.cacheManager, this.podcastManager, this.audioMetadataManager, this.rssFeedManager, this.cronManager, this.emitter.bind(this), this.clientEmitter.bind(this))
|
||||||
this.hlsRouter = new HlsRouter(this.db, this.auth, this.playbackSessionManager, this.emitter.bind(this))
|
this.hlsRouter = new HlsRouter(this.db, this.auth, this.playbackSessionManager, this.emitter.bind(this))
|
||||||
this.staticRouter = new StaticRouter(this.db)
|
this.staticRouter = new StaticRouter(this.db)
|
||||||
|
|
||||||
@@ -150,7 +152,7 @@ class Server {
|
|||||||
await this.backupManager.init()
|
await this.backupManager.init()
|
||||||
await this.logManager.init()
|
await this.logManager.init()
|
||||||
await this.rssFeedManager.init()
|
await this.rssFeedManager.init()
|
||||||
this.podcastManager.init()
|
this.cronManager.init()
|
||||||
|
|
||||||
if (this.db.serverSettings.scannerDisableWatcher) {
|
if (this.db.serverSettings.scannerDisableWatcher) {
|
||||||
Logger.info(`[Server] Watcher is disabled`)
|
Logger.info(`[Server] Watcher is disabled`)
|
||||||
|
|||||||
@@ -108,6 +108,9 @@ class LibraryController {
|
|||||||
// Update watcher
|
// Update watcher
|
||||||
this.watcher.updateLibrary(library)
|
this.watcher.updateLibrary(library)
|
||||||
|
|
||||||
|
// Update auto scan cron
|
||||||
|
this.cronManager.updateLibraryScanCron(library)
|
||||||
|
|
||||||
// Remove libraryItems no longer in library
|
// Remove libraryItems no longer in library
|
||||||
var itemsToRemove = this.db.libraryItems.filter(li => li.libraryId === library.id && !library.checkFullPathInLibrary(li.path))
|
var itemsToRemove = this.db.libraryItems.filter(li => li.libraryId === library.id && !library.checkFullPathInLibrary(li.path))
|
||||||
if (itemsToRemove.length) {
|
if (itemsToRemove.length) {
|
||||||
|
|||||||
@@ -79,12 +79,27 @@ class LibraryItemController {
|
|||||||
await this.cacheManager.purgeCoverCache(libraryItem.id)
|
await this.cacheManager.purgeCoverCache(libraryItem.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Book specific
|
||||||
if (libraryItem.isBook) {
|
if (libraryItem.isBook) {
|
||||||
await this.createAuthorsAndSeriesForItemUpdate(mediaPayload)
|
await this.createAuthorsAndSeriesForItemUpdate(mediaPayload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Podcast specific
|
||||||
|
var isPodcastAutoDownloadUpdated = false
|
||||||
|
if (libraryItem.isPodcast) {
|
||||||
|
if (mediaPayload.autoDownloadEpisodes !== undefined && libraryItem.media.autoDownloadEpisodes !== mediaPayload.autoDownloadEpisodes) {
|
||||||
|
isPodcastAutoDownloadUpdated = true
|
||||||
|
} else if (mediaPayload.autoDownloadSchedule !== undefined && libraryItem.media.autoDownloadSchedule !== mediaPayload.autoDownloadSchedule) {
|
||||||
|
isPodcastAutoDownloadUpdated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var hasUpdates = libraryItem.media.update(mediaPayload)
|
var hasUpdates = libraryItem.media.update(mediaPayload)
|
||||||
if (hasUpdates) {
|
if (hasUpdates) {
|
||||||
|
if (isPodcastAutoDownloadUpdated) {
|
||||||
|
this.cronManager.checkUpdatePodcastCron(libraryItem)
|
||||||
|
}
|
||||||
|
|
||||||
Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`)
|
Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`)
|
||||||
await this.db.updateLibraryItem(libraryItem)
|
await this.db.updateLibraryItem(libraryItem)
|
||||||
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
const { sort } = require('../libs/fastSort')
|
||||||
const { isObject, toNumber } = require('../utils/index')
|
const { isObject, toNumber } = require('../utils/index')
|
||||||
|
|
||||||
class MeController {
|
class MeController {
|
||||||
@@ -240,5 +241,40 @@ class MeController {
|
|||||||
localProgressUpdates: updatedLocalMediaProgress
|
localProgressUpdates: updatedLocalMediaProgress
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET: api/me/items-in-progress
|
||||||
|
async getAllLibraryItemsInProgress(req, res) {
|
||||||
|
const limit = !isNaN(req.query.limit) ? Number(req.query.limit) || 25 : 25
|
||||||
|
|
||||||
|
var itemsInProgress = []
|
||||||
|
for (const mediaProgress of req.user.mediaProgress) {
|
||||||
|
if (!mediaProgress.isFinished && mediaProgress.progress > 0) {
|
||||||
|
const libraryItem = await this.db.getLibraryItem(mediaProgress.libraryItemId)
|
||||||
|
if (libraryItem) {
|
||||||
|
if (mediaProgress.episodeId && libraryItem.mediaType === 'podcast') {
|
||||||
|
const episode = libraryItem.media.episodes.find(ep => ep.id === mediaProgress.episodeId)
|
||||||
|
if (episode) {
|
||||||
|
const libraryItemWithEpisode = {
|
||||||
|
...libraryItem.toJSONMinified(),
|
||||||
|
recentEpisode: episode.toJSON(),
|
||||||
|
progressLastUpdate: mediaProgress.lastUpdate
|
||||||
|
}
|
||||||
|
itemsInProgress.push(libraryItemWithEpisode)
|
||||||
|
}
|
||||||
|
} else if (!mediaProgress.episodeId) {
|
||||||
|
itemsInProgress.push({
|
||||||
|
...libraryItem.toJSONMinified(),
|
||||||
|
progressLastUpdate: mediaProgress.lastUpdate
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsInProgress = sort(itemsInProgress).desc(li => li.progressLastUpdate).slice(0, limit)
|
||||||
|
res.json({
|
||||||
|
libraryItems: itemsInProgress
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = new MeController()
|
module.exports = new MeController()
|
||||||
@@ -42,7 +42,7 @@ class SessionController {
|
|||||||
res.json(payload)
|
res.json(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
getSession(req, res) {
|
getOpenSession(req, res) {
|
||||||
var libraryItem = this.db.getLibraryItem(req.session.libraryItemId)
|
var libraryItem = this.db.getLibraryItem(req.session.libraryItemId)
|
||||||
var sessionForClient = req.session.toJSONForClient(libraryItem)
|
var sessionForClient = req.session.toJSONForClient(libraryItem)
|
||||||
res.json(sessionForClient)
|
res.json(sessionForClient)
|
||||||
@@ -58,12 +58,24 @@ class SessionController {
|
|||||||
this.playbackSessionManager.closeSessionRequest(req.user, req.session, req.body, res)
|
this.playbackSessionManager.closeSessionRequest(req.user, req.session, req.body, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DELETE: api/session/:id
|
||||||
|
async delete(req, res) {
|
||||||
|
// if session is open then remove it
|
||||||
|
const openSession = this.playbackSessionManager.getSession(req.session.id)
|
||||||
|
if (openSession) {
|
||||||
|
await this.playbackSessionManager.removeSession(req.session.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.db.removeEntity('session', req.session.id)
|
||||||
|
res.sendStatus(200)
|
||||||
|
}
|
||||||
|
|
||||||
// POST: api/session/local
|
// POST: api/session/local
|
||||||
syncLocal(req, res) {
|
syncLocal(req, res) {
|
||||||
this.playbackSessionManager.syncLocalSessionRequest(req.user, req.body, res)
|
this.playbackSessionManager.syncLocalSessionRequest(req.user, req.body, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware(req, res, next) {
|
openSessionMiddleware(req, res, next) {
|
||||||
var playbackSession = this.playbackSessionManager.getSession(req.params.id)
|
var playbackSession = this.playbackSessionManager.getSession(req.params.id)
|
||||||
if (!playbackSession) return res.sendStatus(404)
|
if (!playbackSession) return res.sendStatus(404)
|
||||||
|
|
||||||
@@ -75,5 +87,21 @@ class SessionController {
|
|||||||
req.session = playbackSession
|
req.session = playbackSession
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async middleware(req, res, next) {
|
||||||
|
var playbackSession = await this.db.getPlaybackSession(req.params.id)
|
||||||
|
if (!playbackSession) return res.sendStatus(404)
|
||||||
|
|
||||||
|
if (req.method == 'DELETE' && !req.user.canDelete) {
|
||||||
|
Logger.warn(`[SessionController] User attempted to delete without permission`, req.user)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
} else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) {
|
||||||
|
Logger.warn('[SessionController] User attempted to update without permission', req.user.username)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.session = playbackSession
|
||||||
|
next()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = new SessionController()
|
module.exports = new SessionController()
|
||||||
@@ -3,14 +3,14 @@
|
|||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
const TimeMatcher = require('./time-matcher');
|
const TimeMatcher = require('./time-matcher');
|
||||||
|
|
||||||
class Scheduler extends EventEmitter{
|
class Scheduler extends EventEmitter {
|
||||||
constructor(pattern, timezone, autorecover){
|
constructor(pattern, timezone, autorecover) {
|
||||||
super();
|
super();
|
||||||
this.timeMatcher = new TimeMatcher(pattern, timezone);
|
this.timeMatcher = new TimeMatcher(pattern, timezone);
|
||||||
this.autorecover = autorecover;
|
this.autorecover = autorecover;
|
||||||
}
|
}
|
||||||
|
|
||||||
start(){
|
start() {
|
||||||
// clear timeout if exists
|
// clear timeout if exists
|
||||||
this.stop();
|
this.stop();
|
||||||
|
|
||||||
@@ -22,11 +22,11 @@ class Scheduler extends EventEmitter{
|
|||||||
const elapsedTime = process.hrtime(lastCheck);
|
const elapsedTime = process.hrtime(lastCheck);
|
||||||
const elapsedMs = (elapsedTime[0] * 1e9 + elapsedTime[1]) / 1e6;
|
const elapsedMs = (elapsedTime[0] * 1e9 + elapsedTime[1]) / 1e6;
|
||||||
const missedExecutions = Math.floor(elapsedMs / 1000);
|
const missedExecutions = Math.floor(elapsedMs / 1000);
|
||||||
|
|
||||||
for(let i = missedExecutions; i >= 0; i--){
|
for (let i = missedExecutions; i >= 0; i--) {
|
||||||
const date = new Date(new Date().getTime() - i * 1000);
|
const date = new Date(new Date().getTime() - i * 1000);
|
||||||
let date_tmp = this.timeMatcher.apply(date);
|
let date_tmp = this.timeMatcher.apply(date);
|
||||||
if(lastExecution.getTime() < date_tmp.getTime() && (i === 0 || this.autorecover) && this.timeMatcher.match(date)){
|
if (lastExecution.getTime() < date_tmp.getTime() && (i === 0 || this.autorecover) && this.timeMatcher.match(date)) {
|
||||||
this.emit('scheduled-time-matched', date_tmp);
|
this.emit('scheduled-time-matched', date_tmp);
|
||||||
date_tmp.setMilliseconds(0);
|
date_tmp.setMilliseconds(0);
|
||||||
lastExecution = date_tmp;
|
lastExecution = date_tmp;
|
||||||
@@ -38,8 +38,8 @@ class Scheduler extends EventEmitter{
|
|||||||
matchTime();
|
matchTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
stop(){
|
stop() {
|
||||||
if(this.timeout){
|
if (this.timeout) {
|
||||||
clearTimeout(this.timeout);
|
clearTimeout(this.timeout);
|
||||||
}
|
}
|
||||||
this.timeout = null;
|
this.timeout = null;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class BackupManager {
|
|||||||
constructor(db, emitter) {
|
constructor(db, emitter) {
|
||||||
this.BackupPath = Path.join(global.MetadataPath, 'backups')
|
this.BackupPath = Path.join(global.MetadataPath, 'backups')
|
||||||
this.ItemsMetadataPath = Path.join(global.MetadataPath, 'items')
|
this.ItemsMetadataPath = Path.join(global.MetadataPath, 'items')
|
||||||
|
this.AuthorsMetadataPath = Path.join(global.MetadataPath, 'authors')
|
||||||
|
|
||||||
this.db = db
|
this.db = db
|
||||||
this.emitter = emitter
|
this.emitter = emitter
|
||||||
@@ -56,14 +57,14 @@ class BackupManager {
|
|||||||
updateCronSchedule() {
|
updateCronSchedule() {
|
||||||
if (this.scheduleTask && !this.serverSettings.backupSchedule) {
|
if (this.scheduleTask && !this.serverSettings.backupSchedule) {
|
||||||
Logger.info(`[BackupManager] Disabling backup schedule`)
|
Logger.info(`[BackupManager] Disabling backup schedule`)
|
||||||
if (this.scheduleTask.destroy) this.scheduleTask.destroy()
|
if (this.scheduleTask.stop) this.scheduleTask.stop()
|
||||||
this.scheduleTask = null
|
this.scheduleTask = null
|
||||||
} else if (!this.scheduleTask && this.serverSettings.backupSchedule) {
|
} else if (!this.scheduleTask && this.serverSettings.backupSchedule) {
|
||||||
Logger.info(`[BackupManager] Starting backup schedule ${this.serverSettings.backupSchedule}`)
|
Logger.info(`[BackupManager] Starting backup schedule ${this.serverSettings.backupSchedule}`)
|
||||||
this.scheduleCron()
|
this.scheduleCron()
|
||||||
} else if (this.serverSettings.backupSchedule) {
|
} else if (this.serverSettings.backupSchedule) {
|
||||||
Logger.info(`[BackupManager] Restarting backup schedule ${this.serverSettings.backupSchedule}`)
|
Logger.info(`[BackupManager] Restarting backup schedule ${this.serverSettings.backupSchedule}`)
|
||||||
if (this.scheduleTask.destroy) this.scheduleTask.destroy()
|
if (this.scheduleTask.stop) this.scheduleTask.stop()
|
||||||
this.scheduleCron()
|
this.scheduleCron()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,6 +120,7 @@ class BackupManager {
|
|||||||
await zip.extract('config/', global.ConfigPath)
|
await zip.extract('config/', global.ConfigPath)
|
||||||
if (backup.backupMetadataCovers) {
|
if (backup.backupMetadataCovers) {
|
||||||
await zip.extract('metadata-items/', this.ItemsMetadataPath)
|
await zip.extract('metadata-items/', this.ItemsMetadataPath)
|
||||||
|
await zip.extract('metadata-authors/', this.AuthorsMetadataPath)
|
||||||
}
|
}
|
||||||
await this.db.reinit()
|
await this.db.reinit()
|
||||||
this.emitter('backup_applied')
|
this.emitter('backup_applied')
|
||||||
@@ -178,8 +180,6 @@ class BackupManager {
|
|||||||
async runBackup() {
|
async runBackup() {
|
||||||
// Check if Metadata Path is inside Config Path (otherwise there will be an infinite loop as the archiver tries to zip itself)
|
// Check if Metadata Path is inside Config Path (otherwise there will be an infinite loop as the archiver tries to zip itself)
|
||||||
Logger.info(`[BackupManager] Running Backup`)
|
Logger.info(`[BackupManager] Running Backup`)
|
||||||
var metadataItemsPath = this.serverSettings.backupMetadataCovers ? this.ItemsMetadataPath : null
|
|
||||||
|
|
||||||
var newBackup = new Backup()
|
var newBackup = new Backup()
|
||||||
|
|
||||||
const newBackData = {
|
const newBackData = {
|
||||||
@@ -188,7 +188,10 @@ class BackupManager {
|
|||||||
}
|
}
|
||||||
newBackup.setData(newBackData)
|
newBackup.setData(newBackData)
|
||||||
|
|
||||||
var zipResult = await this.zipBackup(metadataItemsPath, newBackup).then(() => true).catch((error) => {
|
var metadataAuthorsPath = this.AuthorsMetadataPath
|
||||||
|
if (!await fs.pathExists(metadataAuthorsPath)) metadataAuthorsPath = null
|
||||||
|
|
||||||
|
var zipResult = await this.zipBackup(metadataAuthorsPath, newBackup).then(() => true).catch((error) => {
|
||||||
Logger.error(`[BackupManager] Backup Failed ${error}`)
|
Logger.error(`[BackupManager] Backup Failed ${error}`)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@@ -228,7 +231,7 @@ class BackupManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
zipBackup(metadataItemsPath, backup) {
|
zipBackup(metadataAuthorsPath, backup) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// create a file to stream archive data to
|
// create a file to stream archive data to
|
||||||
const output = fs.createWriteStream(backup.fullPath)
|
const output = fs.createWriteStream(backup.fullPath)
|
||||||
@@ -299,10 +302,16 @@ class BackupManager {
|
|||||||
archive.directory(this.db.AuthorsPath, 'config/authors')
|
archive.directory(this.db.AuthorsPath, 'config/authors')
|
||||||
archive.directory(this.db.SeriesPath, 'config/series')
|
archive.directory(this.db.SeriesPath, 'config/series')
|
||||||
|
|
||||||
if (metadataItemsPath) {
|
if (this.serverSettings.backupMetadataCovers) {
|
||||||
Logger.debug(`[BackupManager] Backing up Metadata Items "${metadataItemsPath}"`)
|
Logger.debug(`[BackupManager] Backing up Metadata Items "${this.ItemsMetadataPath}"`)
|
||||||
archive.directory(metadataItemsPath, 'metadata-items')
|
archive.directory(this.ItemsMetadataPath, 'metadata-items')
|
||||||
|
|
||||||
|
if (metadataAuthorsPath) {
|
||||||
|
Logger.debug(`[BackupManager] Backing up Metadata Authors "${metadataAuthorsPath}"`)
|
||||||
|
archive.directory(metadataAuthorsPath, 'metadata-authors')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
archive.append(backup.detailsString, { name: 'details' })
|
archive.append(backup.detailsString, { name: 'details' })
|
||||||
|
|
||||||
archive.finalize()
|
archive.finalize()
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
const cron = require('../libs/nodeCron')
|
||||||
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
|
class CronManager {
|
||||||
|
constructor(db, scanner, podcastManager) {
|
||||||
|
this.db = db
|
||||||
|
this.scanner = scanner
|
||||||
|
this.podcastManager = podcastManager
|
||||||
|
|
||||||
|
this.libraryScanCrons = []
|
||||||
|
this.podcastCrons = []
|
||||||
|
|
||||||
|
this.podcastCronExpressionsExecuting = []
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.initLibraryScanCrons()
|
||||||
|
this.initPodcastCrons()
|
||||||
|
}
|
||||||
|
|
||||||
|
initLibraryScanCrons() {
|
||||||
|
for (const library of this.db.libraries) {
|
||||||
|
if (library.settings.autoScanCronExpression) {
|
||||||
|
this.startCronForLibrary(library)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startCronForLibrary(library) {
|
||||||
|
Logger.debug(`[CronManager] Init library scan cron for ${library.name} on schedule ${library.settings.autoScanCronExpression}`)
|
||||||
|
const libScanCron = cron.schedule(library.settings.autoScanCronExpression, () => {
|
||||||
|
Logger.debug(`[CronManager] Library scan cron executing for ${library.name}`)
|
||||||
|
this.scanner.scan(library)
|
||||||
|
})
|
||||||
|
this.libraryScanCrons.push({
|
||||||
|
libraryId: library.id,
|
||||||
|
expression: library.settings.autoScanCronExpression,
|
||||||
|
task: libScanCron
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
removeCronForLibrary(library) {
|
||||||
|
Logger.debug(`[CronManager] Removing library scan cron for ${library.name}`)
|
||||||
|
this.libraryScanCrons = this.libraryScanCrons.filter(lsc => lsc.libraryId !== library.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLibraryScanCron(library) {
|
||||||
|
const expression = library.settings.autoScanCronExpression
|
||||||
|
const existingCron = this.libraryScanCrons.find(lsc => lsc.libraryId === library.id)
|
||||||
|
|
||||||
|
if (!expression && existingCron) {
|
||||||
|
if (existingCron.task.stop) existingCron.task.stop()
|
||||||
|
|
||||||
|
this.removeCronForLibrary(library)
|
||||||
|
} else if (!existingCron && expression) {
|
||||||
|
this.startCronForLibrary(library)
|
||||||
|
} else if (existingCron && existingCron.expression !== expression) {
|
||||||
|
if (existingCron.task.stop) existingCron.task.stop()
|
||||||
|
|
||||||
|
this.removeCronForLibrary(library)
|
||||||
|
this.startCronForLibrary(library)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initPodcastCrons() {
|
||||||
|
const cronExpressionMap = {}
|
||||||
|
this.db.libraryItems.forEach((li) => {
|
||||||
|
if (li.mediaType === 'podcast' && li.media.autoDownloadEpisodes) {
|
||||||
|
if (!li.media.autoDownloadSchedule) {
|
||||||
|
Logger.error(`[CronManager] Podcast auto download schedule is not set for ${li.media.metadata.title}`)
|
||||||
|
} else {
|
||||||
|
if (!cronExpressionMap[li.media.autoDownloadSchedule]) {
|
||||||
|
cronExpressionMap[li.media.autoDownloadSchedule] = {
|
||||||
|
expression: li.media.autoDownloadSchedule,
|
||||||
|
libraryItemIds: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cronExpressionMap[li.media.autoDownloadSchedule].libraryItemIds.push(li.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!Object.keys(cronExpressionMap).length) return
|
||||||
|
|
||||||
|
Logger.debug(`[CronManager] Found ${Object.keys(cronExpressionMap).length} podcast episode schedules to start`)
|
||||||
|
for (const expression in cronExpressionMap) {
|
||||||
|
this.startPodcastCron(expression, cronExpressionMap[expression].libraryItemIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startPodcastCron(expression, libraryItemIds) {
|
||||||
|
try {
|
||||||
|
Logger.debug(`[CronManager] Scheduling podcast episode check cron "${expression}" for ${libraryItemIds.length} item(s)`)
|
||||||
|
const task = cron.schedule(expression, () => {
|
||||||
|
if (this.podcastCronExpressionsExecuting.includes(expression)) {
|
||||||
|
Logger.warn(`[CronManager] Podcast cron "${expression}" is already executing`)
|
||||||
|
} else {
|
||||||
|
this.executePodcastCron(expression, libraryItemIds)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.podcastCrons.push({
|
||||||
|
libraryItemIds,
|
||||||
|
expression,
|
||||||
|
task
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`[PodcastManager] Failed to schedule podcast cron ${this.serverSettings.podcastEpisodeSchedule}`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async executePodcastCron(expression, libraryItemIds) {
|
||||||
|
Logger.debug(`[CronManager] Start executing podcast cron ${expression} for ${libraryItemIds.length} item(s)`)
|
||||||
|
const podcastCron = this.podcastCrons.find(cron => cron.expression === expression)
|
||||||
|
if (!podcastCron) {
|
||||||
|
Logger.error(`[CronManager] Podcast cron not found for expression ${expression}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.podcastCronExpressionsExecuting.push(expression)
|
||||||
|
|
||||||
|
// Get podcast library items to check
|
||||||
|
const libraryItems = []
|
||||||
|
for (const libraryItemId of libraryItemIds) {
|
||||||
|
const libraryItem = this.db.libraryItems.find(li => li.id === libraryItemId)
|
||||||
|
if (!libraryItem) {
|
||||||
|
Logger.error(`[CronManager] Library item ${libraryItemId} not found for episode check cron ${expression}`)
|
||||||
|
podcastCron.libraryItemIds = podcastCron.libraryItemIds.filter(lid => lid !== libraryItemId) // Filter it out
|
||||||
|
} else {
|
||||||
|
libraryItems.push(libraryItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run episode checks
|
||||||
|
for (const libraryItem of libraryItems) {
|
||||||
|
const keepAutoDownloading = await this.podcastManager.runEpisodeCheck(libraryItem)
|
||||||
|
if (!keepAutoDownloading) { // auto download was disabled
|
||||||
|
podcastCron.libraryItemIds = podcastCron.libraryItemIds.filter(lid => lid !== libraryItemId) // Filter it out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop and remove cron if no more library items
|
||||||
|
if (!podcastCron.libraryItemIds.length) {
|
||||||
|
this.removePodcastEpisodeCron(podcastCron)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.debug(`[CronManager] Finished executing podcast cron ${expression} for ${libraryItems.length} item(s)`)
|
||||||
|
this.podcastCronExpressionsExecuting = this.podcastCronExpressionsExecuting.filter(exp => exp !== expression)
|
||||||
|
}
|
||||||
|
|
||||||
|
removePodcastEpisodeCron(podcastCron) {
|
||||||
|
Logger.info(`[CronManager] Stopping & removing podcast episode cron for ${podcastCron.expression}`)
|
||||||
|
if (podcastCron.task) podcastCron.task.stop()
|
||||||
|
this.podcastCrons = this.podcastCrons.filter(pc => pc.expression !== podcastCron.expression)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkUpdatePodcastCron(libraryItem) {
|
||||||
|
// Remove from old cron by library item id
|
||||||
|
const existingCron = this.podcastCrons.find(pc => pc.libraryItemIds.includes(libraryItem.id))
|
||||||
|
if (existingCron) {
|
||||||
|
existingCron.libraryItemIds = existingCron.libraryItemIds.filter(lid => lid !== libraryItem.id)
|
||||||
|
if (!existingCron.libraryItemIds.length) {
|
||||||
|
this.removePodcastEpisodeCron(existingCron)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to cron or start new cron
|
||||||
|
if (libraryItem.media.autoDownloadEpisodes && libraryItem.media.autoDownloadSchedule) {
|
||||||
|
const cronMatchingExpression = this.podcastCrons.find(pc => pc.expression === libraryItem.media.autoDownloadSchedule)
|
||||||
|
if (cronMatchingExpression) {
|
||||||
|
cronMatchingExpression.libraryItemIds.push(libraryItem.id)
|
||||||
|
Logger.info(`[CronManager] Added podcast "${libraryItem.media.metadata.title}" to auto dl episode cron "${cronMatchingExpression.expression}"`)
|
||||||
|
} else {
|
||||||
|
this.startPodcastCron(libraryItem.media.autoDownloadSchedule, [libraryItem.id])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = CronManager
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
const cron = require('../libs/nodeCron')
|
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
|
|
||||||
const { parsePodcastRssFeedXml } = require('../utils/podcastUtils')
|
const { parsePodcastRssFeedXml } = require('../utils/podcastUtils')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
const { downloadFile } = require('../utils/fileUtils')
|
const { downloadFile, removeFile } = require('../utils/fileUtils')
|
||||||
const { levenshteinDistance } = require('../utils/index')
|
const { levenshteinDistance } = require('../utils/index')
|
||||||
const opmlParser = require('../utils/parsers/parseOPML')
|
const opmlParser = require('../utils/parsers/parseOPML')
|
||||||
const prober = require('../utils/prober')
|
const prober = require('../utils/prober')
|
||||||
@@ -23,22 +22,14 @@ class PodcastManager {
|
|||||||
this.downloadQueue = []
|
this.downloadQueue = []
|
||||||
this.currentDownload = null
|
this.currentDownload = null
|
||||||
|
|
||||||
this.episodeScheduleTask = null
|
this.failedCheckMap = {}
|
||||||
this.failedCheckMap = {},
|
this.MaxFailedEpisodeChecks = 24
|
||||||
this.MaxFailedEpisodeChecks = 24
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get serverSettings() {
|
get serverSettings() {
|
||||||
return this.db.serverSettings || {}
|
return this.db.serverSettings || {}
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
|
||||||
var podcastsWithAutoDownload = this.db.libraryItems.some(li => li.mediaType === 'podcast' && li.media.autoDownloadEpisodes)
|
|
||||||
if (podcastsWithAutoDownload) {
|
|
||||||
this.schedulePodcastEpisodeCron()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getEpisodeDownloadsInQueue(libraryItemId) {
|
getEpisodeDownloadsInQueue(libraryItemId) {
|
||||||
return this.downloadQueue.filter(d => d.libraryItemId === libraryItemId)
|
return this.downloadQueue.filter(d => d.libraryItemId === libraryItemId)
|
||||||
}
|
}
|
||||||
@@ -56,14 +47,14 @@ class PodcastManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadPodcastEpisodes(libraryItem, episodesToDownload) {
|
async downloadPodcastEpisodes(libraryItem, episodesToDownload, isAutoDownload) {
|
||||||
var index = libraryItem.media.episodes.length + 1
|
var index = libraryItem.media.episodes.length + 1
|
||||||
episodesToDownload.forEach((ep) => {
|
episodesToDownload.forEach((ep) => {
|
||||||
var newPe = new PodcastEpisode()
|
var newPe = new PodcastEpisode()
|
||||||
newPe.setData(ep, index++)
|
newPe.setData(ep, index++)
|
||||||
newPe.libraryItemId = libraryItem.id
|
newPe.libraryItemId = libraryItem.id
|
||||||
var newPeDl = new PodcastEpisodeDownload()
|
var newPeDl = new PodcastEpisodeDownload()
|
||||||
newPeDl.setData(newPe, libraryItem)
|
newPeDl.setData(newPe, libraryItem, isAutoDownload)
|
||||||
this.startPodcastEpisodeDownload(newPeDl)
|
this.startPodcastEpisodeDownload(newPeDl)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -131,12 +122,46 @@ class PodcastManager {
|
|||||||
libraryItem.isInvalid = false
|
libraryItem.isInvalid = false
|
||||||
}
|
}
|
||||||
libraryItem.libraryFiles.push(libraryFile)
|
libraryItem.libraryFiles.push(libraryFile)
|
||||||
|
|
||||||
|
// Check setting maxEpisodesToKeep and remove episode if necessary
|
||||||
|
if (this.currentDownload.isAutoDownload) { // only applies for auto-downloaded episodes
|
||||||
|
if (libraryItem.media.maxEpisodesToKeep && libraryItem.media.episodesWithPubDate.length > libraryItem.media.maxEpisodesToKeep) {
|
||||||
|
Logger.info(`[PodcastManager] # of episodes (${libraryItem.media.episodesWithPubDate.length}) exceeds max episodes to keep (${libraryItem.media.maxEpisodesToKeep})`)
|
||||||
|
await this.removeOldestEpisode(libraryItem, podcastEpisode.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
libraryItem.updatedAt = Date.now()
|
libraryItem.updatedAt = Date.now()
|
||||||
await this.db.updateLibraryItem(libraryItem)
|
await this.db.updateLibraryItem(libraryItem)
|
||||||
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async removeOldestEpisode(libraryItem, episodeIdJustDownloaded) {
|
||||||
|
var smallestPublishedAt = 0
|
||||||
|
var oldestEpisode = null
|
||||||
|
libraryItem.media.episodesWithPubDate.filter(ep => ep.id !== episodeIdJustDownloaded).forEach((ep) => {
|
||||||
|
if (!smallestPublishedAt || ep.publishedAt < smallestPublishedAt) {
|
||||||
|
smallestPublishedAt = ep.publishedAt
|
||||||
|
oldestEpisode = ep
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// TODO: Should we check for open playback sessions for this episode?
|
||||||
|
// TODO: remove all user progress for this episode
|
||||||
|
if (oldestEpisode && oldestEpisode.audioFile) {
|
||||||
|
Logger.info(`[PodcastManager] Deleting oldest episode "${oldestEpisode.title}"`)
|
||||||
|
const successfullyDeleted = await removeFile(oldestEpisode.audioFile.metadata.path)
|
||||||
|
if (successfullyDeleted) {
|
||||||
|
libraryItem.media.removeEpisode(oldestEpisode.id)
|
||||||
|
libraryItem.removeLibraryFile(oldestEpisode.audioFile.ino)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
Logger.warn(`[PodcastManager] Failed to remove oldest episode "${oldestEpisode.title}"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
async getLibraryFile(path, relPath) {
|
async getLibraryFile(path, relPath) {
|
||||||
var newLibFile = new LibraryFile()
|
var newLibFile = new LibraryFile()
|
||||||
await newLibFile.setDataFromPath(path, relPath)
|
await newLibFile.setDataFromPath(path, relPath)
|
||||||
@@ -155,73 +180,45 @@ class PodcastManager {
|
|||||||
return newAudioFile
|
return newAudioFile
|
||||||
}
|
}
|
||||||
|
|
||||||
schedulePodcastEpisodeCron() {
|
// Returns false if auto download episodes was disabled (disabled if reaches max failed checks)
|
||||||
try {
|
async runEpisodeCheck(libraryItem) {
|
||||||
Logger.debug(`[PodcastManager] Scheduled podcast episode check cron "${this.serverSettings.podcastEpisodeSchedule}"`)
|
const lastEpisodeCheckDate = new Date(libraryItem.media.lastEpisodeCheck || 0)
|
||||||
this.episodeScheduleTask = cron.schedule(this.serverSettings.podcastEpisodeSchedule, () => {
|
const latestEpisodePublishedAt = libraryItem.media.latestEpisodePublished
|
||||||
Logger.debug(`[PodcastManager] Running cron`)
|
Logger.info(`[PodcastManager] runEpisodeCheck: "${libraryItem.media.metadata.title}" | Last check: ${lastEpisodeCheckDate} | ${latestEpisodePublishedAt ? `Latest episode pubDate: ${new Date(latestEpisodePublishedAt)}` : 'No latest episode'}`)
|
||||||
this.checkForNewEpisodes()
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error(`[PodcastManager] Failed to schedule podcast cron ${this.serverSettings.podcastEpisodeSchedule}`, error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelCron() {
|
// Use latest episode pubDate if exists OR fallback to using lastEpisodeCheckDate
|
||||||
Logger.debug(`[PodcastManager] Canceled new podcast episode check cron`)
|
// lastEpisodeCheckDate will be the current time when adding a new podcast
|
||||||
if (this.episodeScheduleTask) {
|
const dateToCheckForEpisodesAfter = latestEpisodePublishedAt || lastEpisodeCheckDate
|
||||||
this.episodeScheduleTask.destroy()
|
Logger.debug(`[PodcastManager] runEpisodeCheck: "${libraryItem.media.metadata.title}" checking for episodes after ${new Date(dateToCheckForEpisodesAfter)}`)
|
||||||
this.episodeScheduleTask = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkForNewEpisodes() {
|
var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, dateToCheckForEpisodesAfter)
|
||||||
var podcastsWithAutoDownload = this.db.libraryItems.filter(li => li.mediaType === 'podcast' && li.media.autoDownloadEpisodes)
|
Logger.debug(`[PodcastManager] runEpisodeCheck: ${newEpisodes ? newEpisodes.length : 'N/A'} episodes found`)
|
||||||
if (!podcastsWithAutoDownload.length) {
|
|
||||||
Logger.info(`[PodcastManager] checkForNewEpisodes - No podcasts with auto download set`)
|
|
||||||
this.cancelCron()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Logger.debug(`[PodcastManager] checkForNewEpisodes - Checking ${podcastsWithAutoDownload.length} Podcasts`)
|
|
||||||
|
|
||||||
for (const libraryItem of podcastsWithAutoDownload) {
|
if (!newEpisodes) { // Failed
|
||||||
const lastEpisodeCheckDate = new Date(libraryItem.media.lastEpisodeCheck || 0)
|
// Allow up to MaxFailedEpisodeChecks failed attempts before disabling auto download
|
||||||
const latestEpisodePublishedAt = libraryItem.media.latestEpisodePublished
|
if (!this.failedCheckMap[libraryItem.id]) this.failedCheckMap[libraryItem.id] = 0
|
||||||
Logger.info(`[PodcastManager] checkForNewEpisodes: "${libraryItem.media.metadata.title}" | Last check: ${lastEpisodeCheckDate} | ${latestEpisodePublishedAt ? `Latest episode pubDate: ${new Date(latestEpisodePublishedAt)}` : 'No latest episode'}`)
|
this.failedCheckMap[libraryItem.id]++
|
||||||
|
if (this.failedCheckMap[libraryItem.id] >= this.MaxFailedEpisodeChecks) {
|
||||||
// Use latest episode pubDate if exists OR fallback to using lastEpisodeCheckDate
|
Logger.error(`[PodcastManager] runEpisodeCheck ${this.failedCheckMap[libraryItem.id]} failed attempts at checking episodes for "${libraryItem.media.metadata.title}" - disabling auto download`)
|
||||||
// lastEpisodeCheckDate will be the current time when adding a new podcast
|
libraryItem.media.autoDownloadEpisodes = false
|
||||||
const dateToCheckForEpisodesAfter = latestEpisodePublishedAt || lastEpisodeCheckDate
|
|
||||||
Logger.debug(`[PodcastManager] checkForNewEpisodes: "${libraryItem.media.metadata.title}" checking for episodes after ${new Date(dateToCheckForEpisodesAfter)}`)
|
|
||||||
|
|
||||||
var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, dateToCheckForEpisodesAfter)
|
|
||||||
Logger.debug(`[PodcastManager] checkForNewEpisodes checked result ${newEpisodes ? newEpisodes.length : 'N/A'}`)
|
|
||||||
|
|
||||||
if (!newEpisodes) { // Failed
|
|
||||||
// Allow up to 3 failed attempts before disabling auto download
|
|
||||||
if (!this.failedCheckMap[libraryItem.id]) this.failedCheckMap[libraryItem.id] = 0
|
|
||||||
this.failedCheckMap[libraryItem.id]++
|
|
||||||
if (this.failedCheckMap[libraryItem.id] >= this.MaxFailedEpisodeChecks) {
|
|
||||||
Logger.error(`[PodcastManager] checkForNewEpisodes ${this.failedCheckMap[libraryItem.id]} failed attempts at checking episodes for "${libraryItem.media.metadata.title}" - disabling auto download`)
|
|
||||||
libraryItem.media.autoDownloadEpisodes = false
|
|
||||||
delete this.failedCheckMap[libraryItem.id]
|
|
||||||
} else {
|
|
||||||
Logger.warn(`[PodcastManager] checkForNewEpisodes ${this.failedCheckMap[libraryItem.id]} failed attempts at checking episodes for "${libraryItem.media.metadata.title}"`)
|
|
||||||
}
|
|
||||||
} else if (newEpisodes.length) {
|
|
||||||
delete this.failedCheckMap[libraryItem.id]
|
delete this.failedCheckMap[libraryItem.id]
|
||||||
Logger.info(`[PodcastManager] Found ${newEpisodes.length} new episodes for podcast "${libraryItem.media.metadata.title}" - starting download`)
|
|
||||||
this.downloadPodcastEpisodes(libraryItem, newEpisodes)
|
|
||||||
} else {
|
} else {
|
||||||
delete this.failedCheckMap[libraryItem.id]
|
Logger.warn(`[PodcastManager] runEpisodeCheck ${this.failedCheckMap[libraryItem.id]} failed attempts at checking episodes for "${libraryItem.media.metadata.title}"`)
|
||||||
Logger.debug(`[PodcastManager] No new episodes for "${libraryItem.media.metadata.title}"`)
|
|
||||||
}
|
}
|
||||||
|
} else if (newEpisodes.length) {
|
||||||
libraryItem.media.lastEpisodeCheck = Date.now()
|
delete this.failedCheckMap[libraryItem.id]
|
||||||
libraryItem.updatedAt = Date.now()
|
Logger.info(`[PodcastManager] Found ${newEpisodes.length} new episodes for podcast "${libraryItem.media.metadata.title}" - starting download`)
|
||||||
await this.db.updateLibraryItem(libraryItem)
|
this.downloadPodcastEpisodes(libraryItem, newEpisodes, true)
|
||||||
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
} else {
|
||||||
|
delete this.failedCheckMap[libraryItem.id]
|
||||||
|
Logger.debug(`[PodcastManager] No new episodes for "${libraryItem.media.metadata.title}"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
libraryItem.media.lastEpisodeCheck = Date.now()
|
||||||
|
libraryItem.updatedAt = Date.now()
|
||||||
|
await this.db.updateLibraryItem(libraryItem)
|
||||||
|
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
|
return libraryItem.media.autoDownloadEpisodes
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkPodcastForNewEpisodes(podcastLibraryItem, dateToCheckForEpisodesAfter) {
|
async checkPodcastForNewEpisodes(podcastLibraryItem, dateToCheckForEpisodesAfter) {
|
||||||
@@ -248,7 +245,7 @@ class PodcastManager {
|
|||||||
var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, libraryItem.media.lastEpisodeCheck)
|
var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, libraryItem.media.lastEpisodeCheck)
|
||||||
if (newEpisodes.length) {
|
if (newEpisodes.length) {
|
||||||
Logger.info(`[PodcastManager] Found ${newEpisodes.length} new episodes for podcast "${libraryItem.media.metadata.title}" - starting download`)
|
Logger.info(`[PodcastManager] Found ${newEpisodes.length} new episodes for podcast "${libraryItem.media.metadata.title}" - starting download`)
|
||||||
this.downloadPodcastEpisodes(libraryItem, newEpisodes)
|
this.downloadPodcastEpisodes(libraryItem, newEpisodes, false)
|
||||||
} else {
|
} else {
|
||||||
Logger.info(`[PodcastManager] No new episodes found for podcast "${libraryItem.media.metadata.title}"`)
|
Logger.info(`[PodcastManager] No new episodes found for podcast "${libraryItem.media.metadata.title}"`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class PodcastEpisodeDownload {
|
|||||||
this.url = null
|
this.url = null
|
||||||
this.libraryItem = null
|
this.libraryItem = null
|
||||||
|
|
||||||
|
this.isAutoDownload = false
|
||||||
this.isDownloading = false
|
this.isDownloading = false
|
||||||
this.isFinished = false
|
this.isFinished = false
|
||||||
this.failed = false
|
this.failed = false
|
||||||
@@ -46,11 +47,12 @@ class PodcastEpisodeDownload {
|
|||||||
return this.libraryItem ? this.libraryItem.id : null
|
return this.libraryItem ? this.libraryItem.id : null
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(podcastEpisode, libraryItem) {
|
setData(podcastEpisode, libraryItem, isAutoDownload) {
|
||||||
this.id = getId('epdl')
|
this.id = getId('epdl')
|
||||||
this.podcastEpisode = podcastEpisode
|
this.podcastEpisode = podcastEpisode
|
||||||
this.url = podcastEpisode.enclosure.url
|
this.url = podcastEpisode.enclosure.url
|
||||||
this.libraryItem = libraryItem
|
this.libraryItem = libraryItem
|
||||||
|
this.isAutoDownload = isAutoDownload
|
||||||
this.createdAt = Date.now()
|
this.createdAt = Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -276,13 +276,18 @@ class Book {
|
|||||||
if (opfMetadata.genres.length && (!this.metadata.genres.length || opfMetadataOverrideDetails)) {
|
if (opfMetadata.genres.length && (!this.metadata.genres.length || opfMetadataOverrideDetails)) {
|
||||||
metadataUpdatePayload[key] = opfMetadata.genres
|
metadataUpdatePayload[key] = opfMetadata.genres
|
||||||
}
|
}
|
||||||
} else if (key === 'author') {
|
} else if (key === 'authors') {
|
||||||
if (opfMetadata.author && (!this.metadata.authors.length || opfMetadataOverrideDetails)) {
|
if (opfMetadata.authors && opfMetadata.authors.length && (!this.metadata.authors.length || opfMetadataOverrideDetails)) {
|
||||||
metadataUpdatePayload.authors = this.metadata.parseAuthorsTag(opfMetadata.author)
|
metadataUpdatePayload.authors = opfMetadata.authors.map(authorName => {
|
||||||
|
return {
|
||||||
|
id: `new-${Math.floor(Math.random() * 1000000)}`,
|
||||||
|
name: authorName
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} else if (key === 'narrator') {
|
} else if (key === 'narrators') {
|
||||||
if (opfMetadata.narrator && (!this.metadata.narrators.length || opfMetadataOverrideDetails)) {
|
if (opfMetadata.narrators && opfMetadata.narrators.length && (!this.metadata.narrators.length || opfMetadataOverrideDetails)) {
|
||||||
metadataUpdatePayload.narrators = this.metadata.parseNarratorsTag(opfMetadata.narrator)
|
metadataUpdatePayload.narrators = opfMetadata.narrators
|
||||||
}
|
}
|
||||||
} else if (key === 'series') {
|
} else if (key === 'series') {
|
||||||
if (opfMetadata.series && (!this.metadata.series.length || opfMetadataOverrideDetails)) {
|
if (opfMetadata.series && (!this.metadata.series.length || opfMetadataOverrideDetails)) {
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ class Podcast {
|
|||||||
this.episodes = []
|
this.episodes = []
|
||||||
|
|
||||||
this.autoDownloadEpisodes = false
|
this.autoDownloadEpisodes = false
|
||||||
|
this.autoDownloadSchedule = null
|
||||||
this.lastEpisodeCheck = 0
|
this.lastEpisodeCheck = 0
|
||||||
|
this.maxEpisodesToKeep = 0
|
||||||
|
|
||||||
this.lastCoverSearch = null
|
this.lastCoverSearch = null
|
||||||
this.lastCoverSearchQuery = null
|
this.lastCoverSearchQuery = null
|
||||||
@@ -39,7 +41,9 @@ class Podcast {
|
|||||||
return podcastEpisode
|
return podcastEpisode
|
||||||
})
|
})
|
||||||
this.autoDownloadEpisodes = !!podcast.autoDownloadEpisodes
|
this.autoDownloadEpisodes = !!podcast.autoDownloadEpisodes
|
||||||
|
this.autoDownloadSchedule = podcast.autoDownloadSchedule || '0 * * * *' // Added in 2.1.3 so default to hourly
|
||||||
this.lastEpisodeCheck = podcast.lastEpisodeCheck || 0
|
this.lastEpisodeCheck = podcast.lastEpisodeCheck || 0
|
||||||
|
this.maxEpisodesToKeep = podcast.maxEpisodesToKeep || 0
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
@@ -50,7 +54,9 @@ class Podcast {
|
|||||||
tags: [...this.tags],
|
tags: [...this.tags],
|
||||||
episodes: this.episodes.map(e => e.toJSON()),
|
episodes: this.episodes.map(e => e.toJSON()),
|
||||||
autoDownloadEpisodes: this.autoDownloadEpisodes,
|
autoDownloadEpisodes: this.autoDownloadEpisodes,
|
||||||
lastEpisodeCheck: this.lastEpisodeCheck
|
autoDownloadSchedule: this.autoDownloadSchedule,
|
||||||
|
lastEpisodeCheck: this.lastEpisodeCheck,
|
||||||
|
maxEpisodesToKeep: this.maxEpisodesToKeep
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +67,9 @@ class Podcast {
|
|||||||
tags: [...this.tags],
|
tags: [...this.tags],
|
||||||
numEpisodes: this.episodes.length,
|
numEpisodes: this.episodes.length,
|
||||||
autoDownloadEpisodes: this.autoDownloadEpisodes,
|
autoDownloadEpisodes: this.autoDownloadEpisodes,
|
||||||
|
autoDownloadSchedule: this.autoDownloadSchedule,
|
||||||
lastEpisodeCheck: this.lastEpisodeCheck,
|
lastEpisodeCheck: this.lastEpisodeCheck,
|
||||||
|
maxEpisodesToKeep: this.maxEpisodesToKeep,
|
||||||
size: this.size
|
size: this.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,7 +82,9 @@ class Podcast {
|
|||||||
tags: [...this.tags],
|
tags: [...this.tags],
|
||||||
episodes: this.episodes.map(e => e.toJSONExpanded()),
|
episodes: this.episodes.map(e => e.toJSONExpanded()),
|
||||||
autoDownloadEpisodes: this.autoDownloadEpisodes,
|
autoDownloadEpisodes: this.autoDownloadEpisodes,
|
||||||
|
autoDownloadSchedule: this.autoDownloadSchedule,
|
||||||
lastEpisodeCheck: this.lastEpisodeCheck,
|
lastEpisodeCheck: this.lastEpisodeCheck,
|
||||||
|
maxEpisodesToKeep: this.maxEpisodesToKeep,
|
||||||
size: this.size
|
size: this.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,6 +123,9 @@ class Podcast {
|
|||||||
})
|
})
|
||||||
return largestPublishedAt
|
return largestPublishedAt
|
||||||
}
|
}
|
||||||
|
get episodesWithPubDate() {
|
||||||
|
return this.episodes.filter(ep => !!ep.publishedAt)
|
||||||
|
}
|
||||||
|
|
||||||
update(payload) {
|
update(payload) {
|
||||||
var json = this.toJSON()
|
var json = this.toJSON()
|
||||||
@@ -157,14 +170,15 @@ class Podcast {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(mediaMetadata) {
|
setData(mediaData) {
|
||||||
this.metadata = new PodcastMetadata()
|
this.metadata = new PodcastMetadata()
|
||||||
if (mediaMetadata.metadata) {
|
if (mediaData.metadata) {
|
||||||
this.metadata.setData(mediaMetadata.metadata)
|
this.metadata.setData(mediaData.metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.coverPath = mediaMetadata.coverPath || null
|
this.coverPath = mediaData.coverPath || null
|
||||||
this.autoDownloadEpisodes = !!mediaMetadata.autoDownloadEpisodes
|
this.autoDownloadEpisodes = !!mediaData.autoDownloadEpisodes
|
||||||
|
this.autoDownloadSchedule = mediaData.autoDownloadSchedule || global.ServerSettings.podcastEpisodeSchedule
|
||||||
this.lastEpisodeCheck = Date.now() // Makes sure new episodes are after this
|
this.lastEpisodeCheck = Date.now() // Makes sure new episodes are after this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
const { BookCoverAspectRatio } = require('../../utils/constants')
|
const { BookCoverAspectRatio } = require('../../utils/constants')
|
||||||
const Logger = require('../../Logger')
|
|
||||||
|
|
||||||
class LibrarySettings {
|
class LibrarySettings {
|
||||||
constructor(settings) {
|
constructor(settings) {
|
||||||
|
this.coverAspectRatio = BookCoverAspectRatio.SQUARE
|
||||||
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)
|
||||||
@@ -13,16 +14,20 @@ class LibrarySettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
construct(settings) {
|
construct(settings) {
|
||||||
|
this.coverAspectRatio = !isNaN(settings.coverAspectRatio) ? settings.coverAspectRatio : BookCoverAspectRatio.SQUARE
|
||||||
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() {
|
||||||
return {
|
return {
|
||||||
|
coverAspectRatio: this.coverAspectRatio,
|
||||||
disableWatcher: this.disableWatcher,
|
disableWatcher: this.disableWatcher,
|
||||||
skipMatchingMediaWithAsin: this.skipMatchingMediaWithAsin,
|
skipMatchingMediaWithAsin: this.skipMatchingMediaWithAsin,
|
||||||
skipMatchingMediaWithIsbn: this.skipMatchingMediaWithIsbn
|
skipMatchingMediaWithIsbn: this.skipMatchingMediaWithIsbn,
|
||||||
|
autoScanCronExpression: this.autoScanCronExpression
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,11 @@ class ServerSettings {
|
|||||||
this.loggerScannerLogsToKeep = 2
|
this.loggerScannerLogsToKeep = 2
|
||||||
|
|
||||||
// Cover
|
// Cover
|
||||||
|
// TODO: Remove after mobile apps are configured to use library server settings
|
||||||
this.coverAspectRatio = BookCoverAspectRatio.SQUARE
|
this.coverAspectRatio = BookCoverAspectRatio.SQUARE
|
||||||
|
|
||||||
|
// Bookshelf Display
|
||||||
|
this.homeBookshelfView = BookshelfView.STANDARD
|
||||||
this.bookshelfView = BookshelfView.STANDARD
|
this.bookshelfView = BookshelfView.STANDARD
|
||||||
|
|
||||||
// Podcasts
|
// Podcasts
|
||||||
@@ -80,13 +84,7 @@ class ServerSettings {
|
|||||||
this.scannerMaxThreads = isNullOrNaN(settings.scannerMaxThreads) ? 0 : Number(settings.scannerMaxThreads)
|
this.scannerMaxThreads = isNullOrNaN(settings.scannerMaxThreads) ? 0 : Number(settings.scannerMaxThreads)
|
||||||
|
|
||||||
this.storeCoverWithItem = !!settings.storeCoverWithItem
|
this.storeCoverWithItem = !!settings.storeCoverWithItem
|
||||||
if (settings.storeCoverWithBook != undefined) { // storeCoverWithBook was old name of setting < v2
|
|
||||||
this.storeCoverWithItem = !!settings.storeCoverWithBook
|
|
||||||
}
|
|
||||||
this.storeMetadataWithItem = !!settings.storeMetadataWithItem
|
this.storeMetadataWithItem = !!settings.storeMetadataWithItem
|
||||||
if (settings.storeMetadataWithBook != undefined) { // storeMetadataWithBook was old name of setting < v2
|
|
||||||
this.storeMetadataWithItem = !!settings.storeMetadataWithBook
|
|
||||||
}
|
|
||||||
|
|
||||||
this.rateLimitLoginRequests = !isNaN(settings.rateLimitLoginRequests) ? Number(settings.rateLimitLoginRequests) : 10
|
this.rateLimitLoginRequests = !isNaN(settings.rateLimitLoginRequests) ? Number(settings.rateLimitLoginRequests) : 10
|
||||||
this.rateLimitLoginWindow = !isNaN(settings.rateLimitLoginWindow) ? Number(settings.rateLimitLoginWindow) : 10 * 60 * 1000 // 10 Minutes
|
this.rateLimitLoginWindow = !isNaN(settings.rateLimitLoginWindow) ? Number(settings.rateLimitLoginWindow) : 10 * 60 * 1000 // 10 Minutes
|
||||||
@@ -100,6 +98,7 @@ class ServerSettings {
|
|||||||
this.loggerScannerLogsToKeep = settings.loggerScannerLogsToKeep || 2
|
this.loggerScannerLogsToKeep = settings.loggerScannerLogsToKeep || 2
|
||||||
|
|
||||||
this.coverAspectRatio = !isNaN(settings.coverAspectRatio) ? settings.coverAspectRatio : BookCoverAspectRatio.SQUARE
|
this.coverAspectRatio = !isNaN(settings.coverAspectRatio) ? settings.coverAspectRatio : BookCoverAspectRatio.SQUARE
|
||||||
|
this.homeBookshelfView = settings.homeBookshelfView || BookshelfView.STANDARD
|
||||||
this.bookshelfView = settings.bookshelfView || BookshelfView.STANDARD
|
this.bookshelfView = settings.bookshelfView || BookshelfView.STANDARD
|
||||||
|
|
||||||
this.sortingIgnorePrefix = !!settings.sortingIgnorePrefix
|
this.sortingIgnorePrefix = !!settings.sortingIgnorePrefix
|
||||||
@@ -110,6 +109,17 @@ class ServerSettings {
|
|||||||
this.logLevel = settings.logLevel || Logger.logLevel
|
this.logLevel = settings.logLevel || Logger.logLevel
|
||||||
this.version = settings.version || null
|
this.version = settings.version || null
|
||||||
|
|
||||||
|
// Migrations
|
||||||
|
if (settings.storeCoverWithBook != undefined) { // storeCoverWithBook was renamed to storeCoverWithItem in 2.0.0
|
||||||
|
this.storeCoverWithItem = !!settings.storeCoverWithBook
|
||||||
|
}
|
||||||
|
if (settings.storeMetadataWithBook != undefined) { // storeMetadataWithBook was renamed to storeMetadataWithItem in 2.0.0
|
||||||
|
this.storeMetadataWithItem = !!settings.storeMetadataWithBook
|
||||||
|
}
|
||||||
|
if (settings.homeBookshelfView == undefined) { // homeBookshelfView was added in 2.1.3
|
||||||
|
this.homeBookshelfView = settings.bookshelfView
|
||||||
|
}
|
||||||
|
|
||||||
if (this.logLevel !== Logger.logLevel) {
|
if (this.logLevel !== Logger.logLevel) {
|
||||||
Logger.setLogLevel(this.logLevel)
|
Logger.setLogLevel(this.logLevel)
|
||||||
}
|
}
|
||||||
@@ -140,6 +150,7 @@ class ServerSettings {
|
|||||||
loggerDailyLogsToKeep: this.loggerDailyLogsToKeep,
|
loggerDailyLogsToKeep: this.loggerDailyLogsToKeep,
|
||||||
loggerScannerLogsToKeep: this.loggerScannerLogsToKeep,
|
loggerScannerLogsToKeep: this.loggerScannerLogsToKeep,
|
||||||
coverAspectRatio: this.coverAspectRatio,
|
coverAspectRatio: this.coverAspectRatio,
|
||||||
|
homeBookshelfView: this.homeBookshelfView,
|
||||||
bookshelfView: this.bookshelfView,
|
bookshelfView: this.bookshelfView,
|
||||||
sortingIgnorePrefix: this.sortingIgnorePrefix,
|
sortingIgnorePrefix: this.sortingIgnorePrefix,
|
||||||
sortingPrefixes: [...this.sortingPrefixes],
|
sortingPrefixes: [...this.sortingPrefixes],
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const Series = require('../objects/entities/Series')
|
|||||||
const FileSystemController = require('../controllers/FileSystemController')
|
const FileSystemController = require('../controllers/FileSystemController')
|
||||||
|
|
||||||
class ApiRouter {
|
class ApiRouter {
|
||||||
constructor(db, auth, scanner, playbackSessionManager, abMergeManager, coverManager, backupManager, watcher, cacheManager, podcastManager, audioMetadataManager, rssFeedManager, emitter, clientEmitter) {
|
constructor(db, auth, scanner, playbackSessionManager, abMergeManager, coverManager, backupManager, watcher, cacheManager, podcastManager, audioMetadataManager, rssFeedManager, cronManager, emitter, clientEmitter) {
|
||||||
this.db = db
|
this.db = db
|
||||||
this.auth = auth
|
this.auth = auth
|
||||||
this.scanner = scanner
|
this.scanner = scanner
|
||||||
@@ -38,6 +38,7 @@ class ApiRouter {
|
|||||||
this.podcastManager = podcastManager
|
this.podcastManager = podcastManager
|
||||||
this.audioMetadataManager = audioMetadataManager
|
this.audioMetadataManager = audioMetadataManager
|
||||||
this.rssFeedManager = rssFeedManager
|
this.rssFeedManager = rssFeedManager
|
||||||
|
this.cronManager = cronManager
|
||||||
this.emitter = emitter
|
this.emitter = emitter
|
||||||
this.clientEmitter = clientEmitter
|
this.clientEmitter = clientEmitter
|
||||||
|
|
||||||
@@ -142,6 +143,7 @@ class ApiRouter {
|
|||||||
this.router.patch('/me/password', MeController.updatePassword.bind(this))
|
this.router.patch('/me/password', MeController.updatePassword.bind(this))
|
||||||
this.router.patch('/me/settings', MeController.updateSettings.bind(this))
|
this.router.patch('/me/settings', MeController.updateSettings.bind(this))
|
||||||
this.router.post('/me/sync-local-progress', MeController.syncLocalMediaProgress.bind(this))
|
this.router.post('/me/sync-local-progress', MeController.syncLocalMediaProgress.bind(this))
|
||||||
|
this.router.get('/me/items-in-progress', MeController.getAllLibraryItemsInProgress.bind(this))
|
||||||
|
|
||||||
//
|
//
|
||||||
// Backup Routes
|
// Backup Routes
|
||||||
@@ -175,9 +177,11 @@ class ApiRouter {
|
|||||||
// Playback Session Routes
|
// Playback Session Routes
|
||||||
//
|
//
|
||||||
this.router.get('/sessions', SessionController.getAllWithUserData.bind(this))
|
this.router.get('/sessions', SessionController.getAllWithUserData.bind(this))
|
||||||
this.router.get('/session/:id', SessionController.middleware.bind(this), SessionController.getSession.bind(this))
|
this.router.delete('/sessions/:id', SessionController.middleware.bind(this), SessionController.delete.bind(this))
|
||||||
this.router.post('/session/:id/sync', SessionController.middleware.bind(this), SessionController.sync.bind(this))
|
// TODO: Update these endpoints because they are only for open playback sessions
|
||||||
this.router.post('/session/:id/close', SessionController.middleware.bind(this), SessionController.close.bind(this))
|
this.router.get('/session/:id', SessionController.openSessionMiddleware.bind(this), SessionController.getOpenSession.bind(this))
|
||||||
|
this.router.post('/session/:id/sync', SessionController.openSessionMiddleware.bind(this), SessionController.sync.bind(this))
|
||||||
|
this.router.post('/session/:id/close', SessionController.openSessionMiddleware.bind(this), SessionController.close.bind(this))
|
||||||
this.router.post('/session/local', SessionController.syncLocal.bind(this))
|
this.router.post('/session/local', SessionController.syncLocal.bind(this))
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ async function getFileStat(path) {
|
|||||||
birthtime: stat.birthtime
|
birthtime: stat.birthtime
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to stat', err)
|
Logger.error('[fileUtils] Failed to stat', err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ async function getFileTimestampsWithIno(path) {
|
|||||||
ino: String(stat.ino)
|
ino: String(stat.ino)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to getFileTimestampsWithIno', err)
|
Logger.error('[fileUtils] Failed to getFileTimestampsWithIno', err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,4 +219,12 @@ module.exports.getAudioMimeTypeFromExtname = (extname) => {
|
|||||||
const formatUpper = extname.slice(1).toUpperCase()
|
const formatUpper = extname.slice(1).toUpperCase()
|
||||||
if (AudioMimeType[formatUpper]) return AudioMimeType[formatUpper]
|
if (AudioMimeType[formatUpper]) return AudioMimeType[formatUpper]
|
||||||
return null
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.removeFile = (path) => {
|
||||||
|
if (!path) return false
|
||||||
|
return fs.remove(path).then(() => true).catch((error) => {
|
||||||
|
Logger.error(`[fileUtils] Failed remove file "${path}"`, error)
|
||||||
|
return false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
const { sort, createNewSortInstance } = require('../libs/fastSort')
|
const { sort, createNewSortInstance } = require('../libs/fastSort')
|
||||||
const { getTitleIgnorePrefix } = require('../utils/index')
|
const { getTitleIgnorePrefix } = require('../utils/index')
|
||||||
const Logger = require('../Logger')
|
|
||||||
const naturalSort = createNewSortInstance({
|
const naturalSort = createNewSortInstance({
|
||||||
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,10 +15,9 @@ function parseCreators(metadata) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchCreator(creators, role) {
|
function fetchCreators(creators, role) {
|
||||||
if (!creators || !creators.length) return null
|
if (!creators || !creators.length) return null
|
||||||
var creator = creators.find(c => c.role === role)
|
return creators.filter(c => c.role === role).map(c => c.value)
|
||||||
return creator ? creator.value : null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchTagString(metadata, tag) {
|
function fetchTagString(metadata, tag) {
|
||||||
@@ -80,11 +79,11 @@ function fetchVolumeNumber(metadataMeta) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fetchNarrators(creators, metadata) {
|
function fetchNarrators(creators, metadata) {
|
||||||
var roleNrt = fetchCreator(creators, 'nrt')
|
var narrators = fetchCreators(creators, 'nrt')
|
||||||
if (typeof metadata.meta == "undefined" || roleNrt != null) return roleNrt
|
if (typeof metadata.meta == "undefined" || narrators.length) return narrators
|
||||||
try {
|
try {
|
||||||
var narratorsJSON = JSON.parse(fetchTagString(metadata.meta, "calibre:user_metadata:#narrators").replace(/"/g, '"'))
|
var narratorsJSON = JSON.parse(fetchTagString(metadata.meta, "calibre:user_metadata:#narrators").replace(/"/g, '"'))
|
||||||
return narratorsJSON["#value#"].join(", ")
|
return narratorsJSON["#value#"]
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -128,11 +127,13 @@ module.exports.parseOpfMetadataXML = async (xml) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var creators = parseCreators(metadata)
|
const creators = parseCreators(metadata)
|
||||||
var data = {
|
const authors = (fetchCreators(creators, 'aut') || []).filter(au => au && au.trim())
|
||||||
|
const narrators = (fetchNarrators(creators, metadata) || []).filter(nrt => nrt && nrt.trim())
|
||||||
|
const data = {
|
||||||
title: fetchTitle(metadata),
|
title: fetchTitle(metadata),
|
||||||
author: fetchCreator(creators, 'aut'),
|
authors,
|
||||||
narrator: fetchNarrators(creators, metadata),
|
narrators,
|
||||||
publishedYear: fetchDate(metadata),
|
publishedYear: fetchDate(metadata),
|
||||||
publisher: fetchPublisher(metadata),
|
publisher: fetchPublisher(metadata),
|
||||||
isbn: fetchISBN(metadata),
|
isbn: fetchISBN(metadata),
|
||||||
|
|||||||
Reference in New Issue
Block a user