mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-02-01 00:18:14 +01:00
commit
6be741045f
120
client/components/modals/EditSeriesInputInnerModal.vue
Normal file
120
client/components/modals/EditSeriesInputInnerModal.vue
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="wrapper" class="hidden absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 rounded-lg items-center justify-center" style="z-index: 51" @click="clickClose">
|
||||||
|
<div class="absolute top-5 right-5 h-12 w-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300">
|
||||||
|
<span class="material-icons text-4xl">close</span>
|
||||||
|
</div>
|
||||||
|
<div ref="content" class="text-white">
|
||||||
|
<form v-if="selectedSeries" @submit.prevent="submitSeriesForm">
|
||||||
|
<div class="bg-bg rounded-lg p-8" @click.stop>
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-grow p-1 min-w-80">
|
||||||
|
<ui-input-dropdown ref="newSeriesSelect" v-model="selectedSeries.name" :items="existingSeriesNames" :disabled="!selectedSeries.id.startsWith('new')" label="Series Name" />
|
||||||
|
</div>
|
||||||
|
<div class="w-40 p-1">
|
||||||
|
<ui-text-input-with-label v-model="selectedSeries.sequence" label="Sequence" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end mt-2 p-1">
|
||||||
|
<ui-btn type="submit">Save</ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: Boolean,
|
||||||
|
selectedSeries: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
existingSeriesNames: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
el: null,
|
||||||
|
content: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.$nextTick(this.setShow)
|
||||||
|
} else {
|
||||||
|
this.setHide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
show: {
|
||||||
|
get() {
|
||||||
|
return this.value
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
submitSeriesForm() {
|
||||||
|
if (this.$refs.newSeriesSelect) {
|
||||||
|
this.$refs.newSeriesSelect.blur()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('submit')
|
||||||
|
},
|
||||||
|
clickClose() {
|
||||||
|
this.show = false
|
||||||
|
},
|
||||||
|
hotkey(action) {
|
||||||
|
if (action === this.$hotkeys.Modal.CLOSE) {
|
||||||
|
this.show = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setShow() {
|
||||||
|
if (!this.el || !this.content) {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
if (!this.el || !this.content) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(this.el)
|
||||||
|
setTimeout(() => {
|
||||||
|
this.content.style.transform = 'scale(1)'
|
||||||
|
}, 10)
|
||||||
|
document.documentElement.classList.add('modal-open')
|
||||||
|
|
||||||
|
this.$store.commit('setInnerModalOpen', true)
|
||||||
|
this.$eventBus.$on('modal-hotkey', this.hotkey)
|
||||||
|
},
|
||||||
|
setHide() {
|
||||||
|
if (this.content) this.content.style.transform = 'scale(0)'
|
||||||
|
if (this.el) this.el.remove()
|
||||||
|
document.documentElement.classList.remove('modal-open')
|
||||||
|
|
||||||
|
this.$store.commit('setInnerModalOpen', false)
|
||||||
|
this.$eventBus.$off('modal-hotkey', this.hotkey)
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.el = this.$refs.wrapper
|
||||||
|
this.content = this.$refs.content
|
||||||
|
if (this.content && this.el) {
|
||||||
|
this.el.classList.remove('hidden')
|
||||||
|
this.el.classList.add('flex')
|
||||||
|
this.content.style.transform = 'scale(0)'
|
||||||
|
this.content.style.transition = 'transform 0.25s cubic-bezier(0.16, 1, 0.3, 1)'
|
||||||
|
this.el.style.opacity = 1
|
||||||
|
this.el.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
@ -104,6 +104,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
hotkey(action) {
|
hotkey(action) {
|
||||||
|
if (this.$store.state.innerModalOpen) return
|
||||||
if (action === this.$hotkeys.Modal.CLOSE) {
|
if (action === this.$hotkeys.Modal.CLOSE) {
|
||||||
this.show = false
|
this.show = false
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full overflow-hidden px-4 py-6 relative">
|
<div id="match-wrapper" class="w-full h-full overflow-hidden px-4 py-6 relative">
|
||||||
<form @submit.prevent="submitSearch">
|
<form @submit.prevent="submitSearch">
|
||||||
<div class="flex items-center justify-start -mx-1 h-20">
|
<div class="flex items-center justify-start -mx-1 h-20">
|
||||||
<div class="w-40 px-1">
|
<div class="w-40 px-1">
|
||||||
@ -87,7 +87,7 @@
|
|||||||
<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" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.series" :disabled="!selectedMatchUsage.series" label="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>
|
||||||
@ -95,6 +95,27 @@
|
|||||||
<ui-checkbox v-model="selectedMatchUsage.volumeNumber" />
|
<ui-checkbox v-model="selectedMatchUsage.volumeNumber" />
|
||||||
<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">
|
||||||
|
<ui-checkbox v-model="selectedMatchUsage.genres" />
|
||||||
|
<div class="flex-grow ml-4">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedMatch.tags" class="flex items-center py-2">
|
||||||
|
<ui-checkbox v-model="selectedMatchUsage.tags" />
|
||||||
|
<div class="flex-grow ml-4">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedMatch.language" class="flex items-center py-2">
|
||||||
|
<ui-checkbox v-model="selectedMatchUsage.language" />
|
||||||
|
<div class="flex-grow ml-4">
|
||||||
|
<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>
|
||||||
|
</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" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
@ -177,6 +198,10 @@ export default {
|
|||||||
publishedYear: true,
|
publishedYear: true,
|
||||||
series: true,
|
series: true,
|
||||||
volumeNumber: true,
|
volumeNumber: true,
|
||||||
|
genres: true,
|
||||||
|
tags: true,
|
||||||
|
language: true,
|
||||||
|
explicit: true,
|
||||||
asin: true,
|
asin: true,
|
||||||
isbn: true,
|
isbn: true,
|
||||||
// Podcast specific
|
// Podcast specific
|
||||||
@ -204,6 +229,22 @@ export default {
|
|||||||
this.$emit('update:processing', val)
|
this.$emit('update:processing', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
seriesItems: {
|
||||||
|
get() {
|
||||||
|
return this.selectedMatch.series.map((se) => {
|
||||||
|
return {
|
||||||
|
id: `new-${Math.floor(Math.random() * 10000)}`,
|
||||||
|
displayName: se.volumeNumber ? `${se.series} #${se.volumeNumber}` : se.series,
|
||||||
|
name: se.series,
|
||||||
|
sequence: se.volumeNumber || ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
console.log('set series items', val)
|
||||||
|
this.selectedMatch.series = val
|
||||||
|
}
|
||||||
|
},
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
@ -294,6 +335,10 @@ export default {
|
|||||||
publishedYear: true,
|
publishedYear: true,
|
||||||
series: true,
|
series: true,
|
||||||
volumeNumber: true,
|
volumeNumber: true,
|
||||||
|
genres: true,
|
||||||
|
tags: true,
|
||||||
|
language: true,
|
||||||
|
explicit: true,
|
||||||
asin: true,
|
asin: true,
|
||||||
isbn: true,
|
isbn: true,
|
||||||
// Podcast specific
|
// Podcast specific
|
||||||
@ -320,36 +365,69 @@ export default {
|
|||||||
else this.provider = localStorage.getItem('book-provider') || 'google'
|
else this.provider = localStorage.getItem('book-provider') || 'google'
|
||||||
},
|
},
|
||||||
selectMatch(match) {
|
selectMatch(match) {
|
||||||
|
if (match && match.series) {
|
||||||
|
match.series = match.series.map((se) => {
|
||||||
|
return {
|
||||||
|
id: `new-${Math.floor(Math.random() * 10000)}`,
|
||||||
|
displayName: se.volumeNumber ? `${se.series} #${se.volumeNumber}` : se.series,
|
||||||
|
name: se.series,
|
||||||
|
sequence: se.volumeNumber || ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
this.selectedMatch = match
|
this.selectedMatch = match
|
||||||
},
|
},
|
||||||
buildMatchUpdatePayload() {
|
buildMatchUpdatePayload() {
|
||||||
var updatePayload = {}
|
var updatePayload = {}
|
||||||
|
updatePayload.metadata = {}
|
||||||
|
|
||||||
var volumeNumber = this.selectedMatchUsage.volumeNumber ? this.selectedMatch.volumeNumber || null : null
|
var volumeNumber = this.selectedMatchUsage.volumeNumber ? this.selectedMatch.volumeNumber || null : null
|
||||||
for (const key in this.selectedMatchUsage) {
|
for (const key in this.selectedMatchUsage) {
|
||||||
if (this.selectedMatchUsage[key] && this.selectedMatch[key]) {
|
if (this.selectedMatchUsage[key] && this.selectedMatch[key]) {
|
||||||
if (key === 'series') {
|
if (key === 'series') {
|
||||||
var seriesItem = {
|
var seriesPayload = []
|
||||||
id: `new-${Math.floor(Math.random() * 10000)}`,
|
if (!Array.isArray(this.selectedMatch[key])) {
|
||||||
name: this.selectedMatch[key],
|
seriesPayload.push({
|
||||||
sequence: volumeNumber
|
id: `new-${Math.floor(Math.random() * 10000)}`,
|
||||||
|
name: this.selectedMatch[key],
|
||||||
|
sequence: volumeNumber
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.selectedMatch[key].forEach((seriesItem) =>
|
||||||
|
seriesPayload.push({
|
||||||
|
id: seriesItem.id,
|
||||||
|
name: seriesItem.name,
|
||||||
|
sequence: seriesItem.sequence
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
updatePayload.series = [seriesItem]
|
|
||||||
|
updatePayload.metadata.series = seriesPayload
|
||||||
} else if (key === 'author' && !this.isPodcast) {
|
} else if (key === 'author' && !this.isPodcast) {
|
||||||
var authorItem = {
|
if (!Array.isArray(this.selectedMatch[key])) this.selectedMatch[key] = [this.selectedMatch[key]]
|
||||||
id: `new-${Math.floor(Math.random() * 10000)}`,
|
var authorPayload = []
|
||||||
name: this.selectedMatch[key]
|
this.selectedMatch[key].forEach((authorName) =>
|
||||||
}
|
authorPayload.push({
|
||||||
updatePayload.authors = [authorItem]
|
id: `new-${Math.floor(Math.random() * 10000)}`,
|
||||||
|
name: authorName
|
||||||
|
})
|
||||||
|
)
|
||||||
|
updatePayload.metadata.authors = authorPayload
|
||||||
} else if (key === 'narrator') {
|
} else if (key === 'narrator') {
|
||||||
updatePayload.narrators = [this.selectedMatch[key]]
|
updatePayload.metadata.narrators = [this.selectedMatch[key]]
|
||||||
|
} else if (key === 'genres') {
|
||||||
|
updatePayload.metadata.genres = this.selectedMatch[key].split(',')
|
||||||
|
} else if (key === 'tags') {
|
||||||
|
updatePayload.tags = this.selectedMatch[key].split(',')
|
||||||
} else if (key === 'itunesId') {
|
} else if (key === 'itunesId') {
|
||||||
updatePayload.itunesId = Number(this.selectedMatch[key])
|
updatePayload.metadata.itunesId = Number(this.selectedMatch[key])
|
||||||
} else if (key !== 'volumeNumber') {
|
} else {
|
||||||
updatePayload[key] = this.selectedMatch[key]
|
updatePayload.metadata[key] = this.selectedMatch[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return updatePayload
|
return updatePayload
|
||||||
},
|
},
|
||||||
async submitMatchUpdate() {
|
async submitMatchUpdate() {
|
||||||
@ -361,7 +439,7 @@ export default {
|
|||||||
|
|
||||||
if (updatePayload.cover) {
|
if (updatePayload.cover) {
|
||||||
var coverPayload = {
|
var coverPayload = {
|
||||||
url: updatePayload.cover
|
url: updatePayload.metadata.cover
|
||||||
}
|
}
|
||||||
var success = await this.$axios.$post(`/api/items/${this.libraryItemId}/cover`, coverPayload).catch((error) => {
|
var success = await this.$axios.$post(`/api/items/${this.libraryItemId}/cover`, coverPayload).catch((error) => {
|
||||||
console.error('Failed to update', error)
|
console.error('Failed to update', error)
|
||||||
@ -373,13 +451,11 @@ export default {
|
|||||||
this.$toast.error('Item Cover Failed to Update')
|
this.$toast.error('Item Cover Failed to Update')
|
||||||
}
|
}
|
||||||
console.log('Updated cover')
|
console.log('Updated cover')
|
||||||
delete updatePayload.cover
|
delete updatePayload.metadata.cover
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(updatePayload).length) {
|
if (Object.keys(updatePayload).length) {
|
||||||
var mediaUpdatePayload = {
|
var mediaUpdatePayload = updatePayload
|
||||||
metadata: updatePayload
|
|
||||||
}
|
|
||||||
var updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, mediaUpdatePayload).catch((error) => {
|
var updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, mediaUpdatePayload).catch((error) => {
|
||||||
console.error('Failed to update', error)
|
console.error('Failed to update', error)
|
||||||
return false
|
return false
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
<div class="flex mt-2 -mx-1">
|
<div class="flex mt-2 -mx-1">
|
||||||
<div class="flex-grow px-1">
|
<div class="flex-grow px-1">
|
||||||
<ui-multi-select-query-input ref="seriesSelect" v-model="seriesItems" text-key="displayName" label="Series" readonly show-edit @edit="editSeriesItem" @add="addNewSeries" />
|
<widgets-series-input-widget v-model="details.series" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -63,27 +63,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div v-if="showSeriesForm" class="absolute top-0 left-0 z-20 w-full h-full bg-black bg-opacity-50 rounded-lg flex items-center justify-center" @click="cancelSeriesForm">
|
|
||||||
<div class="absolute top-0 right-0 p-4">
|
|
||||||
<span class="material-icons text-gray-200 hover:text-white text-4xl cursor-pointer">close</span>
|
|
||||||
</div>
|
|
||||||
<form @submit.prevent="submitSeriesForm">
|
|
||||||
<div class="bg-bg rounded-lg p-8" @click.stop>
|
|
||||||
<div class="flex">
|
|
||||||
<div class="flex-grow p-1 min-w-80">
|
|
||||||
<ui-input-dropdown ref="newSeriesSelect" v-model="selectedSeries.name" :items="existingSeriesNames" :disabled="!selectedSeries.id.startsWith('new')" label="Series Name" />
|
|
||||||
</div>
|
|
||||||
<div class="w-40 p-1">
|
|
||||||
<ui-text-input-with-label v-model="selectedSeries.sequence" label="Sequence" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-end mt-2 p-1">
|
|
||||||
<ui-btn type="submit">Save</ui-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -97,8 +76,6 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
selectedSeries: {},
|
|
||||||
showSeriesForm: false,
|
|
||||||
details: {
|
details: {
|
||||||
title: null,
|
title: null,
|
||||||
subtitle: null,
|
subtitle: null,
|
||||||
@ -146,24 +123,6 @@ export default {
|
|||||||
},
|
},
|
||||||
filterData() {
|
filterData() {
|
||||||
return this.$store.state.libraries.filterData || {}
|
return this.$store.state.libraries.filterData || {}
|
||||||
},
|
|
||||||
existingSeriesNames() {
|
|
||||||
// Only show series names not already selected
|
|
||||||
var alreadySelectedSeriesIds = this.details.series.map((se) => se.id)
|
|
||||||
return this.series.filter((se) => !alreadySelectedSeriesIds.includes(se.id)).map((se) => se.name)
|
|
||||||
},
|
|
||||||
seriesItems: {
|
|
||||||
get() {
|
|
||||||
return this.details.series.map((se) => {
|
|
||||||
return {
|
|
||||||
displayName: se.sequence ? `${se.name} #${se.sequence}` : se.name,
|
|
||||||
...se
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
this.details.series = val
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -214,50 +173,6 @@ export default {
|
|||||||
this.$refs.tagsSelect.forceBlur()
|
this.$refs.tagsSelect.forceBlur()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cancelSeriesForm() {
|
|
||||||
this.showSeriesForm = false
|
|
||||||
},
|
|
||||||
editSeriesItem(series) {
|
|
||||||
var _series = this.details.series.find((se) => se.id === series.id)
|
|
||||||
if (!_series) return
|
|
||||||
this.selectedSeries = {
|
|
||||||
..._series
|
|
||||||
}
|
|
||||||
this.showSeriesForm = true
|
|
||||||
},
|
|
||||||
addNewSeries() {
|
|
||||||
this.selectedSeries = {
|
|
||||||
id: `new-${Date.now()}`,
|
|
||||||
name: '',
|
|
||||||
sequence: ''
|
|
||||||
}
|
|
||||||
this.showSeriesForm = true
|
|
||||||
},
|
|
||||||
submitSeriesForm() {
|
|
||||||
if (!this.selectedSeries.name) {
|
|
||||||
this.$toast.error('Must enter a series')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (this.$refs.newSeriesSelect) {
|
|
||||||
this.$refs.newSeriesSelect.blur()
|
|
||||||
}
|
|
||||||
var existingSeriesIndex = this.details.series.findIndex((se) => se.id === this.selectedSeries.id)
|
|
||||||
|
|
||||||
var seriesSameName = this.series.find((se) => se.name.toLowerCase() === this.selectedSeries.name.toLowerCase())
|
|
||||||
if (existingSeriesIndex < 0 && seriesSameName) {
|
|
||||||
this.selectedSeries.id = seriesSameName.id
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingSeriesIndex >= 0) {
|
|
||||||
this.details.series.splice(existingSeriesIndex, 1, { ...this.selectedSeries })
|
|
||||||
} else {
|
|
||||||
this.details.series.push({
|
|
||||||
...this.selectedSeries
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.showSeriesForm = false
|
|
||||||
},
|
|
||||||
stringArrayEqual(array1, array2) {
|
stringArrayEqual(array1, array2) {
|
||||||
// return false if different
|
// return false if different
|
||||||
if (array1.length !== array2.length) return false
|
if (array1.length !== array2.length) return false
|
||||||
|
111
client/components/widgets/SeriesInputWidget.vue
Normal file
111
client/components/widgets/SeriesInputWidget.vue
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<ui-multi-select-query-input v-model="seriesItems" text-key="displayName" label="Series" readonly show-edit @edit="editSeriesItem" @add="addNewSeries" />
|
||||||
|
|
||||||
|
<modals-edit-series-input-inner-modal v-model="showSeriesForm" :selected-series="selectedSeries" :existing-series-names="existingSeriesNames" @submit="submitSeriesForm" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedSeries: null,
|
||||||
|
showSeriesForm: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
seriesItems: {
|
||||||
|
get() {
|
||||||
|
return (this.value || []).map((se) => {
|
||||||
|
return {
|
||||||
|
displayName: se.sequence ? `${se.name} #${se.sequence}` : se.name,
|
||||||
|
...se
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series() {
|
||||||
|
return this.filterData.series || []
|
||||||
|
},
|
||||||
|
filterData() {
|
||||||
|
return this.$store.state.libraries.filterData || {}
|
||||||
|
},
|
||||||
|
existingSeriesNames() {
|
||||||
|
// Only show series names not already selected
|
||||||
|
var alreadySelectedSeriesIds = (this.value || []).map((se) => se.id)
|
||||||
|
return this.series.filter((se) => !alreadySelectedSeriesIds.includes(se.id)).map((se) => se.name)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
cancelSeriesForm() {
|
||||||
|
this.showSeriesForm = false
|
||||||
|
},
|
||||||
|
editSeriesItem(series) {
|
||||||
|
var _series = this.seriesItems.find((se) => se.id === series.id)
|
||||||
|
if (!_series) return
|
||||||
|
|
||||||
|
this.selectedSeries = {
|
||||||
|
..._series
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Selected series', this.selectedSeries)
|
||||||
|
this.showSeriesForm = true
|
||||||
|
},
|
||||||
|
addNewSeries() {
|
||||||
|
this.selectedSeries = {
|
||||||
|
id: `new-${Date.now()}`,
|
||||||
|
name: '',
|
||||||
|
sequence: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showSeriesForm = true
|
||||||
|
},
|
||||||
|
submitSeriesForm() {
|
||||||
|
console.log('submit series form', this.value, this.selectedSeries)
|
||||||
|
|
||||||
|
if (!this.selectedSeries.name) {
|
||||||
|
this.$toast.error('Must enter a series')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingSeriesIndex = this.seriesItems.findIndex((se) => se.id === this.selectedSeries.id)
|
||||||
|
|
||||||
|
var existingSeriesSameName = this.seriesItems.findIndex((se) => se.name.toLowerCase() === this.selectedSeries.name.toLowerCase())
|
||||||
|
if (existingSeriesSameName >= 0 && existingSeriesIndex < 0) {
|
||||||
|
console.error('Attempt to add duplicate series')
|
||||||
|
this.$toast.error('Cannot add two of the same series')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var seriesSameName = this.series.find((se) => se.name.toLowerCase() === this.selectedSeries.name.toLowerCase())
|
||||||
|
if (existingSeriesIndex < 0 && seriesSameName) {
|
||||||
|
this.selectedSeries.id = seriesSameName.id
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedSeriesCopy = { ...this.selectedSeries }
|
||||||
|
selectedSeriesCopy.displayName = selectedSeriesCopy.sequence ? `${selectedSeriesCopy.name} #${selectedSeriesCopy.sequence}` : selectedSeriesCopy.name
|
||||||
|
|
||||||
|
var seriesCopy = this.seriesItems.map((v) => ({ ...v }))
|
||||||
|
if (existingSeriesIndex >= 0) {
|
||||||
|
seriesCopy.splice(existingSeriesIndex, 1, selectedSeriesCopy)
|
||||||
|
this.seriesItems = seriesCopy
|
||||||
|
} else {
|
||||||
|
seriesCopy.push(selectedSeriesCopy)
|
||||||
|
this.seriesItems = seriesCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showSeriesForm = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -113,6 +113,16 @@
|
|||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center py-2">
|
||||||
|
<ui-toggle-switch v-model="newServerSettings.scannerPreferMatchedMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferMatchedMetadata', val)" />
|
||||||
|
<ui-tooltip :text="tooltips.scannerPreferMatchedMetadata">
|
||||||
|
<p class="pl-4 text-lg">
|
||||||
|
Scanner prefer matched metadata
|
||||||
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
|
</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.scannerDisableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', val)" />
|
<ui-toggle-switch v-model="newServerSettings.scannerDisableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', val)" />
|
||||||
<ui-tooltip :text="tooltips.scannerDisableWatcher">
|
<ui-tooltip :text="tooltips.scannerDisableWatcher">
|
||||||
@ -226,6 +236,7 @@ export default {
|
|||||||
experimentalFeatures: 'Features in development that could use your feedback and help testing. Click to open github discussion.',
|
experimentalFeatures: 'Features in development that could use your feedback and help testing. Click to open github discussion.',
|
||||||
scannerDisableWatcher: 'Disables the automatic adding/updating of items when file changes are detected. *Requires server restart',
|
scannerDisableWatcher: 'Disables the automatic adding/updating of items when file changes are detected. *Requires server restart',
|
||||||
scannerPreferOpfMetadata: 'OPF file metadata will be used for book details over folder names',
|
scannerPreferOpfMetadata: 'OPF file metadata will be used for book details over folder names',
|
||||||
|
scannerPreferMatchedMetadata: 'Matched data will overide book details when using Quick Match',
|
||||||
scannerPreferAudioMetadata: 'Audio file ID3 meta tags will be used for book details over folder names',
|
scannerPreferAudioMetadata: 'Audio file ID3 meta tags will be used for book details over folder names',
|
||||||
scannerParseSubtitle: 'Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by " - "<br>i.e. "Book Title - A Subtitle Here" has the subtitle "A Subtitle Here"',
|
scannerParseSubtitle: 'Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by " - "<br>i.e. "Book Title - A Subtitle Here" has the subtitle "A Subtitle Here"',
|
||||||
sortingIgnorePrefix: 'i.e. for prefix "the" book title "The Book Title" would sort as "Book Title, The"',
|
sortingIgnorePrefix: 'i.e. for prefix "the" book title "The Book Title" would sort as "Book Title, The"',
|
||||||
|
@ -20,6 +20,7 @@ export const state = () => ({
|
|||||||
backups: [],
|
backups: [],
|
||||||
bookshelfBookIds: [],
|
bookshelfBookIds: [],
|
||||||
openModal: null,
|
openModal: null,
|
||||||
|
innerModalOpen: false,
|
||||||
selectedBookshelfTexture: '/textures/wood_default.jpg',
|
selectedBookshelfTexture: '/textures/wood_default.jpg',
|
||||||
lastBookshelfScrollData: {}
|
lastBookshelfScrollData: {}
|
||||||
})
|
})
|
||||||
@ -177,6 +178,9 @@ export const mutations = {
|
|||||||
setOpenModal(state, val) {
|
setOpenModal(state, val) {
|
||||||
state.openModal = val
|
state.openModal = val
|
||||||
},
|
},
|
||||||
|
setInnerModalOpen(state, val) {
|
||||||
|
state.innerModalOpen = val
|
||||||
|
},
|
||||||
setBookshelfTexture(state, val) {
|
setBookshelfTexture(state, val) {
|
||||||
state.selectedBookshelfTexture = val
|
state.selectedBookshelfTexture = val
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,6 @@ class LibraryItemController {
|
|||||||
|
|
||||||
var hasUpdates = libraryItem.update(req.body)
|
var hasUpdates = libraryItem.update(req.body)
|
||||||
if (hasUpdates) {
|
if (hasUpdates) {
|
||||||
|
|
||||||
// Turn on podcast auto download cron if not already on
|
// Turn on podcast auto download cron if not already on
|
||||||
if (libraryItem.mediaType == 'podcast' && req.body.media.autoDownloadEpisodes && !this.podcastManager.episodeScheduleTask) {
|
if (libraryItem.mediaType == 'podcast' && req.body.media.autoDownloadEpisodes && !this.podcastManager.episodeScheduleTask) {
|
||||||
this.podcastManager.schedulePodcastEpisodeCron()
|
this.podcastManager.schedulePodcastEpisodeCron()
|
||||||
|
@ -166,14 +166,14 @@ class BookFinder {
|
|||||||
return this.iTunesApi.searchAudiobooks(title)
|
return this.iTunesApi.searchAudiobooks(title)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAudibleResults(title, author) {
|
async getAudibleResults(title, author, asin) {
|
||||||
var books = await this.audible.search(title, author);
|
var books = await this.audible.search(title, author, asin);
|
||||||
if (this.verbose) Logger.debug(`Audible Book Search Results: ${books.length || 0}`)
|
if (this.verbose) Logger.debug(`Audible Book Search Results: ${books.length || 0}`)
|
||||||
if (!books) return []
|
if (!books) return []
|
||||||
return books
|
return books
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(provider, title, author, options = {}) {
|
async search(provider, title, author, isbn, asin, options = {}) {
|
||||||
var books = []
|
var books = []
|
||||||
var maxTitleDistance = !isNaN(options.titleDistance) ? Number(options.titleDistance) : 4
|
var maxTitleDistance = !isNaN(options.titleDistance) ? Number(options.titleDistance) : 4
|
||||||
var maxAuthorDistance = !isNaN(options.authorDistance) ? Number(options.authorDistance) : 4
|
var maxAuthorDistance = !isNaN(options.authorDistance) ? Number(options.authorDistance) : 4
|
||||||
@ -182,7 +182,7 @@ class BookFinder {
|
|||||||
if (provider === 'google') {
|
if (provider === 'google') {
|
||||||
return this.getGoogleBooksResults(title, author)
|
return this.getGoogleBooksResults(title, author)
|
||||||
} else if (provider === 'audible') {
|
} else if (provider === 'audible') {
|
||||||
return this.getAudibleResults(title, author)
|
return this.getAudibleResults(title, author, asin)
|
||||||
} else if (provider === 'itunes') {
|
} else if (provider === 'itunes') {
|
||||||
return this.getiTunesAudiobooksResults(title, author)
|
return this.getiTunesAudiobooksResults(title, author)
|
||||||
} else if (provider === 'libgen') {
|
} else if (provider === 'libgen') {
|
||||||
|
@ -48,7 +48,7 @@ class Series {
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkNameEquals(name) {
|
checkNameEquals(name) {
|
||||||
if (!name) return false
|
if (!name || !this.name) return false
|
||||||
return this.name.toLowerCase() == name.toLowerCase().trim()
|
return this.name.toLowerCase() == name.toLowerCase().trim()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,8 @@ class ServerSettings {
|
|||||||
this.scannerCoverProvider = 'google'
|
this.scannerCoverProvider = 'google'
|
||||||
this.scannerPreferAudioMetadata = false
|
this.scannerPreferAudioMetadata = false
|
||||||
this.scannerPreferOpfMetadata = false
|
this.scannerPreferOpfMetadata = false
|
||||||
this.scannerDisableWatcher = false
|
this.scannerPreferMatchedMetadata = false
|
||||||
|
this.scannerDisableWatcher = false
|
||||||
|
|
||||||
// Metadata - choose to store inside users library item folder
|
// Metadata - choose to store inside users library item folder
|
||||||
this.storeCoverWithItem = false
|
this.storeCoverWithItem = false
|
||||||
@ -62,6 +63,7 @@ class ServerSettings {
|
|||||||
this.scannerParseSubtitle = settings.scannerParseSubtitle
|
this.scannerParseSubtitle = settings.scannerParseSubtitle
|
||||||
this.scannerPreferAudioMetadata = !!settings.scannerPreferAudioMetadata
|
this.scannerPreferAudioMetadata = !!settings.scannerPreferAudioMetadata
|
||||||
this.scannerPreferOpfMetadata = !!settings.scannerPreferOpfMetadata
|
this.scannerPreferOpfMetadata = !!settings.scannerPreferOpfMetadata
|
||||||
|
this.scannerPreferMatchedMetadata = !!settings.scannerPreferMatchedMetadata
|
||||||
this.scannerDisableWatcher = !!settings.scannerDisableWatcher
|
this.scannerDisableWatcher = !!settings.scannerDisableWatcher
|
||||||
|
|
||||||
this.storeCoverWithItem = !!settings.storeCoverWithItem
|
this.storeCoverWithItem = !!settings.storeCoverWithItem
|
||||||
@ -107,6 +109,7 @@ class ServerSettings {
|
|||||||
scannerParseSubtitle: this.scannerParseSubtitle,
|
scannerParseSubtitle: this.scannerParseSubtitle,
|
||||||
scannerPreferAudioMetadata: this.scannerPreferAudioMetadata,
|
scannerPreferAudioMetadata: this.scannerPreferAudioMetadata,
|
||||||
scannerPreferOpfMetadata: this.scannerPreferOpfMetadata,
|
scannerPreferOpfMetadata: this.scannerPreferOpfMetadata,
|
||||||
|
scannerPreferMatchedMetadata: this.scannerPreferMatchedMetadata,
|
||||||
scannerDisableWatcher: this.scannerDisableWatcher,
|
scannerDisableWatcher: this.scannerDisableWatcher,
|
||||||
storeCoverWithItem: this.storeCoverWithItem,
|
storeCoverWithItem: this.storeCoverWithItem,
|
||||||
storeMetadataWithItem: this.storeMetadataWithItem,
|
storeMetadataWithItem: this.storeMetadataWithItem,
|
||||||
|
@ -6,83 +6,79 @@ class Audible {
|
|||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
cleanResult(item) {
|
cleanResult(item) {
|
||||||
var { title, subtitle, asin, authors, narrators, publisher_name, publisher_summary, release_date, series, product_images, publication_name } = item;
|
var { title, subtitle, asin, authors, narrators, publisherName, summary, releaseDate, image, genres, seriesPrimary, seriesSecondary, language } = item
|
||||||
|
|
||||||
var primarySeries = this.getPrimarySeries(series, publication_name);
|
var series = []
|
||||||
|
if (seriesPrimary) series.push(seriesPrimary)
|
||||||
|
if (seriesSecondary) series.push(seriesSecondary)
|
||||||
|
|
||||||
|
var genresFiltered = genres ? genres.filter(g => g.type == "genre") : []
|
||||||
|
var tagsFiltered = genres ? genres.filter(g => g.type == "tag") : []
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
subtitle: subtitle || null,
|
subtitle: subtitle || null,
|
||||||
author: authors ? authors.map(({ name }) => name).join(', ') : null,
|
author: authors ? authors.map(({ name }) => name).join(', ') : null,
|
||||||
narrator: narrators ? narrators.map(({ name }) => name).join(', ') : null,
|
narrator: narrators ? narrators.map(({ name }) => name).join(', ') : null,
|
||||||
publisher: publisher_name,
|
publisher: publisherName,
|
||||||
publishedYear: release_date ? release_date.split('-')[0] : null,
|
publishedYear: releaseDate ? releaseDate.split('-')[0] : null,
|
||||||
description: publisher_summary ? htmlSanitizer.stripAllTags(publisher_summary) : null,
|
description: summary ? htmlSanitizer.stripAllTags(summary) : null,
|
||||||
cover: this.getBestImageLink(product_images),
|
cover: image,
|
||||||
asin,
|
asin,
|
||||||
series: primarySeries ? primarySeries.title : null,
|
genres: genresFiltered.length > 0 ? genresFiltered.map(({ name }) => name).join(', ') : null,
|
||||||
volumeNumber: primarySeries ? primarySeries.sequence : null
|
tags: tagsFiltered.length > 0 ? tagsFiltered.map(({ name }) => name).join(', ') : null,
|
||||||
|
series: series != [] ? series.map(({ name, position }) => ({ series: name, volumeNumber: position })) : null,
|
||||||
|
language: language ? language.charAt(0).toUpperCase() + language.slice(1) : null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getBestImageLink(images) {
|
|
||||||
if (!images) return null
|
|
||||||
var keys = Object.keys(images)
|
|
||||||
if (!keys.length) return null
|
|
||||||
return images[keys[keys.length - 1]]
|
|
||||||
}
|
|
||||||
|
|
||||||
getPrimarySeries(series, publication_name) {
|
|
||||||
return (series && series.length > 0) ? series.find((s) => s.title == publication_name) || series[0] : null
|
|
||||||
}
|
|
||||||
|
|
||||||
isProbablyAsin(title) {
|
isProbablyAsin(title) {
|
||||||
return /^[0-9A-Z]{10}$/.test(title)
|
return /^[0-9A-Z]{10}$/.test(title)
|
||||||
}
|
}
|
||||||
|
|
||||||
asinSearch(asin) {
|
asinSearch(asin) {
|
||||||
var queryObj = {
|
|
||||||
response_groups: 'rating,series,contributors,product_desc,media,product_extended_attrs',
|
|
||||||
image_sizes: '500,1024,2000'
|
|
||||||
};
|
|
||||||
var queryString = (new URLSearchParams(queryObj)).toString();
|
|
||||||
asin = encodeURIComponent(asin);
|
asin = encodeURIComponent(asin);
|
||||||
var url = `https://api.audible.com/1.0/catalog/products/${asin}?${queryString}`
|
var url = `https://api.audnex.us/books/${asin}`
|
||||||
Logger.debug(`[Audible] ASIN url: ${url}`)
|
Logger.debug(`[Audible] ASIN url: ${url}`)
|
||||||
return axios.get(url).then((res) => {
|
return axios.get(url).then((res) => {
|
||||||
if (!res || !res.data || !res.data.product || !res.data.product.authors) return []
|
if (!res || !res.data || !res.data.asin) return null
|
||||||
return [res.data.product]
|
return res.data
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
Logger.error('[Audible] search error', error)
|
Logger.error('[Audible] ASIN search error', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(title, author) {
|
async search(title, author, asin) {
|
||||||
if (this.isProbablyAsin(title)) {
|
var items
|
||||||
var items = await this.asinSearch(title)
|
if (asin) {
|
||||||
if (items.length > 0) return items.map(item => this.cleanResult(item))
|
items = [await this.asinSearch(asin)]
|
||||||
}
|
}
|
||||||
|
|
||||||
var queryObj = {
|
if (!items && this.isProbablyAsin(title)) {
|
||||||
response_groups: 'rating,series,contributors,product_desc,media,product_extended_attrs',
|
items = [await this.asinSearch(title)]
|
||||||
image_sizes: '500,1024,2000',
|
}
|
||||||
num_results: '25',
|
|
||||||
products_sort_by: 'Relevance',
|
if (!items) {
|
||||||
title: title
|
var queryObj = {
|
||||||
};
|
num_results: '10',
|
||||||
if (author) queryObj.author = author
|
products_sort_by: 'Relevance',
|
||||||
var queryString = (new URLSearchParams(queryObj)).toString();
|
title: title
|
||||||
var url = `https://api.audible.com/1.0/catalog/products?${queryString}`
|
};
|
||||||
Logger.debug(`[Audible] Search url: ${url}`)
|
if (author) queryObj.author = author
|
||||||
var items = await axios.get(url).then((res) => {
|
var queryString = (new URLSearchParams(queryObj)).toString();
|
||||||
if (!res || !res.data || !res.data.products) return []
|
var url = `https://api.audible.com/1.0/catalog/products?${queryString}`
|
||||||
return res.data.products
|
Logger.debug(`[Audible] Search url: ${url}`)
|
||||||
}).catch(error => {
|
items = await axios.get(url).then((res) => {
|
||||||
Logger.error('[Audible] search error', error)
|
if (!res || !res.data || !res.data.products) return null
|
||||||
return []
|
return Promise.all(res.data.products.map(result => this.asinSearch(result.asin)))
|
||||||
})
|
}).catch(error => {
|
||||||
return items.map(item => this.cleanResult(item))
|
Logger.error('[Audible] query search error', error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return items ? items.map(item => this.cleanResult(item)) : []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ class ScanOptions {
|
|||||||
this.storeCoverWithItem = false
|
this.storeCoverWithItem = false
|
||||||
this.preferAudioMetadata = false
|
this.preferAudioMetadata = false
|
||||||
this.preferOpfMetadata = false
|
this.preferOpfMetadata = false
|
||||||
|
this.preferMatchedMetadata = false
|
||||||
|
|
||||||
if (options) {
|
if (options) {
|
||||||
this.construct(options)
|
this.construct(options)
|
||||||
@ -32,7 +33,8 @@ class ScanOptions {
|
|||||||
findCovers: this.findCovers,
|
findCovers: this.findCovers,
|
||||||
storeCoverWithItem: this.storeCoverWithItem,
|
storeCoverWithItem: this.storeCoverWithItem,
|
||||||
preferAudioMetadata: this.preferAudioMetadata,
|
preferAudioMetadata: this.preferAudioMetadata,
|
||||||
preferOpfMetadata: this.preferOpfMetadata
|
preferOpfMetadata: this.preferOpfMetadata,
|
||||||
|
preferMatchedMetadata: this.preferMatchedMetadata
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,6 +46,7 @@ class ScanOptions {
|
|||||||
this.storeCoverWithItem = serverSettings.storeCoverWithItem
|
this.storeCoverWithItem = serverSettings.storeCoverWithItem
|
||||||
this.preferAudioMetadata = serverSettings.scannerPreferAudioMetadata
|
this.preferAudioMetadata = serverSettings.scannerPreferAudioMetadata
|
||||||
this.preferOpfMetadata = serverSettings.scannerPreferOpfMetadata
|
this.preferOpfMetadata = serverSettings.scannerPreferOpfMetadata
|
||||||
|
this.scannerPreferMatchedMetadata = serverSettings.scannerPreferMatchedMetadata
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = ScanOptions
|
module.exports = ScanOptions
|
@ -640,8 +640,10 @@ class Scanner {
|
|||||||
var provider = options.provider || 'google'
|
var provider = options.provider || 'google'
|
||||||
var searchTitle = options.title || libraryItem.media.metadata.title
|
var searchTitle = options.title || libraryItem.media.metadata.title
|
||||||
var searchAuthor = options.author || libraryItem.media.metadata.authorName
|
var searchAuthor = options.author || libraryItem.media.metadata.authorName
|
||||||
|
var searchISBN = options.isbn || libraryItem.media.metadata.isbn
|
||||||
|
var searchASIN = options.asin || libraryItem.media.metadata.asin
|
||||||
|
|
||||||
var results = await this.bookFinder.search(provider, searchTitle, searchAuthor)
|
var results = await this.bookFinder.search(provider, searchTitle, searchAuthor, searchISBN, searchASIN)
|
||||||
if (!results.length) {
|
if (!results.length) {
|
||||||
return {
|
return {
|
||||||
warning: `No ${provider} match found`
|
warning: `No ${provider} match found`
|
||||||
@ -649,6 +651,12 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
var matchData = results[0]
|
var matchData = results[0]
|
||||||
|
|
||||||
|
// Set to override existing metadata if scannerPreferMatchedMetadata setting is true
|
||||||
|
if(this.db.serverSettings.scannerPreferMatchedMetadata) {
|
||||||
|
options.overrideCover = true
|
||||||
|
options.overrideDetails = true
|
||||||
|
}
|
||||||
|
|
||||||
// Update cover if not set OR overrideCover flag
|
// Update cover if not set OR overrideCover flag
|
||||||
var hasUpdated = false
|
var hasUpdated = false
|
||||||
if (matchData.cover && (!libraryItem.media.coverPath || options.overrideCover)) {
|
if (matchData.cover && (!libraryItem.media.coverPath || options.overrideCover)) {
|
||||||
@ -662,47 +670,68 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update media metadata if not set OR overrideDetails flag
|
// Update media metadata if not set OR overrideDetails flag
|
||||||
const detailKeysToUpdate = ['title', 'subtitle', 'description', 'narrator', 'publisher', 'publishedYear', 'asin', 'isbn']
|
const detailKeysToUpdate = ['title', 'subtitle', 'description', 'narrator', 'publisher', 'publishedYear', 'genres', 'tags', 'language', 'explicit', 'asin', 'isbn']
|
||||||
const updatePayload = {}
|
const updatePayload = {}
|
||||||
|
updatePayload.metadata = {}
|
||||||
for (const key in matchData) {
|
for (const key in matchData) {
|
||||||
if (matchData[key] && detailKeysToUpdate.includes(key)) {
|
if (matchData[key] && detailKeysToUpdate.includes(key)) {
|
||||||
if (key === 'narrator') {
|
if (key === 'narrator') {
|
||||||
if ((!libraryItem.media.metadata.narratorName || options.overrideDetails)) {
|
if ((!libraryItem.media.metadata.narratorName || options.overrideDetails)) {
|
||||||
updatePayload.narrators = [matchData[key]]
|
updatePayload.metadata.narrators = matchData[key].split(',')
|
||||||
|
}
|
||||||
|
} else if (key === 'genres') {
|
||||||
|
if ((!libraryItem.media.metadata.genres || options.overrideDetails)) {
|
||||||
|
updatePayload.metadata[key] = matchData[key].split(',')
|
||||||
|
}
|
||||||
|
} else if (key === 'tags') {
|
||||||
|
if ((!libraryItem.media.tags || options.overrideDetails)) {
|
||||||
|
updatePayload[key] = matchData[key].split(',')
|
||||||
}
|
}
|
||||||
} else if ((!libraryItem.media.metadata[key] || options.overrideDetails)) {
|
} else if ((!libraryItem.media.metadata[key] || options.overrideDetails)) {
|
||||||
updatePayload[key] = matchData[key]
|
updatePayload.metadata[key] = matchData[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add or set author if not set
|
// Add or set author if not set
|
||||||
if (matchData.author && !libraryItem.media.metadata.authorName) {
|
if (matchData.author && !libraryItem.media.metadata.authorName || options.overrideDetails) {
|
||||||
var author = this.db.authors.find(au => au.checkNameEquals(matchData.author))
|
if(!Array.isArray(matchData.author)) matchData.author = [matchData.author]
|
||||||
if (!author) {
|
const authorPayload = []
|
||||||
author = new Author()
|
for (let index = 0; index < matchData.author.length; index++) {
|
||||||
author.setData({ name: matchData.author })
|
const authorName = matchData.author[index]
|
||||||
await this.db.insertEntity('author', author)
|
var author = this.db.authors.find(au => au.checkNameEquals(authorName))
|
||||||
this.emitter('author_added', author)
|
if (!author) {
|
||||||
|
author = new Author()
|
||||||
|
author.setData({ name: authorName })
|
||||||
|
await this.db.insertEntity('author', author)
|
||||||
|
this.emitter('author_added', author)
|
||||||
|
}
|
||||||
|
authorPayload.push(author.toJSONMinimal())
|
||||||
}
|
}
|
||||||
updatePayload.authors = [author.toJSONMinimal()]
|
updatePayload.metadata.authors = authorPayload
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add or set series if not set
|
// Add or set series if not set
|
||||||
if (matchData.series && !libraryItem.media.metadata.seriesName) {
|
if (matchData.series && !libraryItem.media.metadata.seriesName || options.overrideDetails) {
|
||||||
var seriesItem = this.db.series.find(au => au.checkNameEquals(matchData.series))
|
if(!Array.isArray(matchData.series)) matchData.series = [{ series: matchData.series, volumeNumber: matchData.volumeNumber }]
|
||||||
if (!seriesItem) {
|
const seriesPayload = []
|
||||||
seriesItem = new Series()
|
for (let index = 0; index < matchData.series.length; index++) {
|
||||||
seriesItem.setData({ name: matchData.series })
|
const seriesMatchItem = matchData.series[index]
|
||||||
await this.db.insertEntity('series', seriesItem)
|
var seriesItem = this.db.series.find(au => au.checkNameEquals(seriesMatchItem.series))
|
||||||
this.emitter('series_added', seriesItem)
|
if (!seriesItem) {
|
||||||
|
seriesItem = new Series()
|
||||||
|
seriesItem.setData({ name: seriesMatchItem.series })
|
||||||
|
await this.db.insertEntity('series', seriesItem)
|
||||||
|
this.emitter('series_added', seriesItem)
|
||||||
|
}
|
||||||
|
seriesPayload.push(seriesItem.toJSONMinimal(seriesMatchItem.volumeNumber))
|
||||||
}
|
}
|
||||||
updatePayload.series = [seriesItem.toJSONMinimal(matchData.volumeNumber)]
|
updatePayload.metadata.series = seriesPayload
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(updatePayload).length) {
|
if (Object.keys(updatePayload).length) {
|
||||||
Logger.debug('[Scanner] Updating details', updatePayload)
|
Logger.debug('[Scanner] Updating details', updatePayload)
|
||||||
if (libraryItem.media.update({ metadata: updatePayload })) {
|
if (libraryItem.media.update(updatePayload)) {
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user