mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-30 18:48:55 +01:00
Clean and parse author name from directory, sort by author last name, scan for covers
This commit is contained in:
parent
759be593b6
commit
51357195e2
@ -65,4 +65,8 @@
|
|||||||
|
|
||||||
.icon-text {
|
.icon-text {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ab-page-wrapper {
|
||||||
|
background-image: linear-gradient(to right bottom, #2e2e2e, #303030, #313131, #333333, #353535, #343434, #323232, #313131, #2c2c2c, #282828, #232323, #1f1f1f);
|
||||||
}
|
}
|
@ -3,9 +3,9 @@
|
|||||||
<div id="toolbar" class="absolute top-0 left-0 w-full h-full z-20 flex items-center px-8">
|
<div id="toolbar" class="absolute top-0 left-0 w-full h-full z-20 flex items-center px-8">
|
||||||
<p class="font-book">{{ numShowing }} Audiobooks</p>
|
<p class="font-book">{{ numShowing }} Audiobooks</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<controls-filter-select v-model="settings.filterBy" class="w-40 h-7.5" @change="updateFilter" />
|
<controls-filter-select v-model="settings.filterBy" class="w-48 h-7.5" @change="updateFilter" />
|
||||||
<span class="px-4 text-sm">by</span>
|
<span class="px-4 text-sm">by</span>
|
||||||
<controls-order-select v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-40 h-7.5" @change="updateOrder" />
|
<controls-order-select v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-48 h-7.5" @change="updateOrder" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<nuxt-link :to="`/audiobook/${audiobookId}`" :style="{ height: height + 32 + 'px', width: width + 32 + 'px' }" class="cursor-pointer p-4">
|
<nuxt-link :to="`/audiobook/${audiobookId}`" :style="{ height: height + 32 + 'px', width: width + 32 + 'px' }" class="cursor-pointer p-4">
|
||||||
<div class="rounded-sm h-full overflow-hidden relative bookCard" @mouseover="isHovering = true" @mouseleave="isHovering = false">
|
<div class="rounded-sm h-full overflow-hidden relative bookCard" @mouseover="isHovering = true" @mouseleave="isHovering = false">
|
||||||
<div class="w-full relative" :style="{ height: width * 1.6 + 'px' }">
|
<div class="w-full relative" :style="{ height: width * 1.6 + 'px' }">
|
||||||
<cards-book-cover :audiobook="audiobook" />
|
<cards-book-cover :audiobook="audiobook" :author-override="authorFormat" />
|
||||||
|
|
||||||
<div v-show="isHovering" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-40">
|
<div v-show="isHovering" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-40">
|
||||||
<div class="h-full flex items-center justify-center">
|
<div class="h-full flex items-center justify-center">
|
||||||
@ -14,7 +14,6 @@
|
|||||||
<span class="material-icons" style="font-size: 16px">edit</span>
|
<span class="material-icons" style="font-size: 16px">edit</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute bottom-0 left-0 h-1 bg-yellow-400 shadow-sm" :style="{ width: width * userProgressPercent + 'px' }"></div>
|
<div class="absolute bottom-0 left-0 h-1 bg-yellow-400 shadow-sm" :style="{ width: width * userProgressPercent + 'px' }"></div>
|
||||||
</div>
|
</div>
|
||||||
<ui-tooltip v-if="showError" :text="errorText" class="absolute top-4 left-0">
|
<ui-tooltip v-if="showError" :text="errorText" class="absolute top-4 left-0">
|
||||||
@ -62,6 +61,25 @@ export default {
|
|||||||
author() {
|
author() {
|
||||||
return this.book.author
|
return this.book.author
|
||||||
},
|
},
|
||||||
|
authorFL() {
|
||||||
|
return this.book.authorFL || this.author
|
||||||
|
},
|
||||||
|
authorLF() {
|
||||||
|
return this.book.authorLF || this.author
|
||||||
|
},
|
||||||
|
authorFormat() {
|
||||||
|
if (!this.orderBy || !this.orderBy.startsWith('book.author')) return null
|
||||||
|
return this.orderBy === 'book.authorLF' ? this.authorLF : this.authorFL
|
||||||
|
},
|
||||||
|
volumeNumber() {
|
||||||
|
return this.book.volumeNumber || null
|
||||||
|
},
|
||||||
|
orderBy() {
|
||||||
|
return this.$store.getters['user/getUserSetting']('orderBy')
|
||||||
|
},
|
||||||
|
filterBy() {
|
||||||
|
return this.$store.getters['user/getUserSetting']('filterBy')
|
||||||
|
},
|
||||||
userProgressPercent() {
|
userProgressPercent() {
|
||||||
return this.userProgress ? this.userProgress.progress || 0 : 0
|
return this.userProgress ? this.userProgress.progress || 0 : 0
|
||||||
},
|
},
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
<p class="text-center font-book text-error" :style="{ fontSize: titleFontSize + 'rem' }">Invalid Cover</p>
|
<p class="text-center font-book text-error" :style="{ fontSize: titleFontSize + 'rem' }">Invalid Cover</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-center font-book" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">{{ titleCleaned }}</p>
|
<p class="text-center font-book" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">{{ titleCleaned }}</p>
|
||||||
@ -26,6 +27,7 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
|
authorOverride: String,
|
||||||
width: {
|
width: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 120
|
default: 120
|
||||||
@ -36,6 +38,11 @@ export default {
|
|||||||
imageFailed: false
|
imageFailed: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
cover() {
|
||||||
|
this.imageFailed = false
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
book() {
|
book() {
|
||||||
return this.audiobook.book || {}
|
return this.audiobook.book || {}
|
||||||
@ -50,6 +57,7 @@ export default {
|
|||||||
return this.title
|
return this.title
|
||||||
},
|
},
|
||||||
author() {
|
author() {
|
||||||
|
if (this.authorOverride) return this.authorOverride
|
||||||
return this.book.author || 'Unknown'
|
return this.book.author || 'Unknown'
|
||||||
},
|
},
|
||||||
authorCleaned() {
|
authorCleaned() {
|
||||||
|
@ -115,6 +115,9 @@ export default {
|
|||||||
if (!_sel) return ''
|
if (!_sel) return ''
|
||||||
return _sel.text
|
return _sel.text
|
||||||
},
|
},
|
||||||
|
authors() {
|
||||||
|
return this.$store.getters['audiobooks/getUniqueAuthors']
|
||||||
|
},
|
||||||
genres() {
|
genres() {
|
||||||
return this.$store.state.audiobooks.genres
|
return this.$store.state.audiobooks.genres
|
||||||
},
|
},
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<template v-for="item in items">
|
<template v-for="item in items">
|
||||||
<li :key="item.value" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="item.value === selected ? 'bg-primary bg-opacity-50' : ''" role="option" @click="clickedOption(item.value)">
|
<li :key="item.value" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="item.value === selected ? 'bg-primary bg-opacity-50' : ''" role="option" @click="clickedOption(item.value)">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="font-normal ml-3 block truncate">{{ item.text }}</span>
|
<span class="font-normal ml-3 block truncate text-xs">{{ item.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
|
<span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
|
||||||
<span class="material-icons text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
<span class="material-icons text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||||
@ -37,8 +37,12 @@ export default {
|
|||||||
value: 'book.title'
|
value: 'book.title'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Author',
|
text: 'Author (First Last)',
|
||||||
value: 'book.author'
|
value: 'book.authorFL'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Author (Last, First)',
|
||||||
|
value: 'book.authorLF'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Added At',
|
text: 'Added At',
|
||||||
@ -73,7 +77,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
selectedText() {
|
selectedText() {
|
||||||
var _sel = this.items.find((i) => i.value === this.selected)
|
var _selected = this.selected === 'book.author' ? 'book.authorFL' : this.selected
|
||||||
|
var _sel = this.items.find((i) => i.value === _selected)
|
||||||
if (!_sel) return ''
|
if (!_sel) return ''
|
||||||
return _sel.text
|
return _sel.text
|
||||||
}
|
}
|
||||||
|
@ -21,9 +21,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <ui-text-input-with-label v-model="details.series" label="Series" class="mt-2" /> -->
|
<div class="flex mt-2 -mx-1">
|
||||||
|
<div class="w-3/4 px-1">
|
||||||
<ui-input-dropdown v-model="details.series" label="Series" class="mt-2" :items="series" />
|
<ui-input-dropdown v-model="details.series" label="Series" :items="series" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow px-1">
|
||||||
|
<ui-text-input-with-label v-model="details.volumeNumber" label="Volume #" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ui-textarea-with-label v-model="details.description" :rows="3" label="Description" class="mt-2" />
|
<ui-textarea-with-label v-model="details.description" :rows="3" label="Description" class="mt-2" />
|
||||||
|
|
||||||
@ -61,6 +66,7 @@ export default {
|
|||||||
description: null,
|
description: null,
|
||||||
author: null,
|
author: null,
|
||||||
series: null,
|
series: null,
|
||||||
|
volumeNumber: null,
|
||||||
publishYear: null,
|
publishYear: null,
|
||||||
genres: []
|
genres: []
|
||||||
},
|
},
|
||||||
@ -132,6 +138,7 @@ export default {
|
|||||||
this.details.author = this.book.author
|
this.details.author = this.book.author
|
||||||
this.details.genres = this.book.genres || []
|
this.details.genres = this.book.genres || []
|
||||||
this.details.series = this.book.series
|
this.details.series = this.book.series
|
||||||
|
this.details.volumeNumber = this.book.volumeNumber
|
||||||
this.details.publishYear = this.book.publishYear
|
this.details.publishYear = this.book.publishYear
|
||||||
|
|
||||||
this.newTags = this.audiobook.tags || []
|
this.newTags = this.audiobook.tags || []
|
||||||
|
@ -3,12 +3,12 @@
|
|||||||
<p class="px-1 text-sm font-semibold">{{ label }}</p>
|
<p class="px-1 text-sm font-semibold">{{ label }}</p>
|
||||||
<div ref="wrapper" class="relative">
|
<div ref="wrapper" class="relative">
|
||||||
<form @submit.prevent="submitForm">
|
<form @submit.prevent="submitForm">
|
||||||
<div ref="inputWrapper" style="min-height: 40px" class="flex-wrap relative w-full shadow-sm flex items-center bg-primary border border-gray-600 rounded-md px-2 py-1 cursor-text">
|
<div ref="inputWrapper" class="flex-wrap relative w-full shadow-sm flex items-center bg-primary border border-gray-600 rounded px-2 py-2">
|
||||||
<input ref="input" v-model="textInput" class="h-full w-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" />
|
<input ref="input" v-model="textInput" class="h-full w-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<ul ref="menu" v-show="isFocused && items.length && (itemsToShow.length || currentSearch)" class="absolute z-50 mt-0 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
|
<ul ref="menu" v-show="isFocused && items.length && (itemsToShow.length || currentSearch)" class="absolute z-50 mt-0 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
|
||||||
<template v-for="item in itemsToShow">
|
<template v-for="item in itemsToShow">
|
||||||
<li :key="item" class="text-gray-50 select-none relative py-2 pr-3 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
|
<li :key="item" class="text-gray-50 select-none relative py-2 pr-3 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
|
@ -4,7 +4,12 @@
|
|||||||
<div ref="wrapper" class="relative">
|
<div ref="wrapper" class="relative">
|
||||||
<form @submit.prevent="submitForm">
|
<form @submit.prevent="submitForm">
|
||||||
<div ref="inputWrapper" style="min-height: 40px" class="flex-wrap relative w-full shadow-sm flex items-center bg-primary border border-gray-600 rounded-md px-2 py-1 cursor-text" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
|
<div ref="inputWrapper" style="min-height: 40px" class="flex-wrap relative w-full shadow-sm flex items-center bg-primary border border-gray-600 rounded-md px-2 py-1 cursor-text" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
|
||||||
<div v-for="item in selected" :key="item" class="rounded-full px-2 py-1 ma-0.5 text-xs bg-bg flex flex-nowrap whitespace-nowrap items-center">{{ $snakeToNormal(item) }}</div>
|
<div v-for="item in selected" :key="item" class="rounded-full px-2 py-1 ma-0.5 text-xs bg-bg flex flex-nowrap whitespace-nowrap items-center relative">
|
||||||
|
<div class="w-full h-full rounded-full absolute top-0 left-0 opacity-0 hover:opacity-100 px-1 bg-bg bg-opacity-75 flex items-center justify-end cursor-pointer">
|
||||||
|
<span class="material-icons text-white hover:text-error" style="font-size: 1.1rem" @click.stop="removeItem(item)">close</span>
|
||||||
|
</div>
|
||||||
|
{{ $snakeToNormal(item) }}
|
||||||
|
</div>
|
||||||
<input ref="input" v-model="textInput" style="min-width: 40px; width: 40px" class="h-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" />
|
<input ref="input" v-model="textInput" style="min-width: 40px; width: 40px" class="h-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@ -156,6 +161,13 @@ export default {
|
|||||||
}
|
}
|
||||||
this.focus()
|
this.focus()
|
||||||
},
|
},
|
||||||
|
removeItem(item) {
|
||||||
|
var remaining = this.selected.filter((i) => i !== item)
|
||||||
|
this.$emit('input', remaining)
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.recalcMenuPos()
|
||||||
|
})
|
||||||
|
},
|
||||||
insertNewItem(item) {
|
insertNewItem(item) {
|
||||||
var kebabItem = this.$normalToSnake(item)
|
var kebabItem = this.$normalToSnake(item)
|
||||||
this.selected.push(kebabItem)
|
this.selected.push(kebabItem)
|
||||||
|
@ -1,25 +1,47 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-show="isScanning" class="fixed bottom-0 left-0 right-0 mx-auto z-20 max-w-lg">
|
<div v-show="isScanning" class="fixed bottom-4 left-0 right-0 mx-auto z-20 max-w-lg">
|
||||||
<div class="w-full my-1 rounded-lg drop-shadow-lg px-4 py-2 flex items-center justify-center text-center transition-all border border-white border-opacity-40 shadow-md bg-warning">
|
<div class="w-full my-1 rounded-lg drop-shadow-lg px-4 py-2 flex items-center justify-center text-center transition-all border border-white border-opacity-40 shadow-md bg-warning">
|
||||||
<p class="text-lg font-sans" v-html="text" />
|
<p class="text-lg font-sans" v-html="text" />
|
||||||
</div>
|
</div>
|
||||||
|
<div v-show="!hasCanceled" class="absolute right-0 top-3 bottom-0 px-2">
|
||||||
|
<ui-btn color="red-600" small :padding-x="1" @click="cancelScan">Cancel</ui-btn>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {
|
||||||
|
hasCanceled: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
isScanning(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.hasCanceled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
text() {
|
text() {
|
||||||
return `Scanning... <span class="font-mono">${this.scanNum}</span> of <span class="font-mono">${this.scanTotal}</span> <strong class='font-mono px-2'>${this.scanPercent}</strong>`
|
var scanText = this.isScanningFiles ? 'Scanning...' : 'Scanning Covers...'
|
||||||
|
return `${scanText} <span class="font-mono">${this.scanNum}</span> of <span class="font-mono">${this.scanTotal}</span> <strong class='font-mono px-2'>${this.scanPercent}</strong>`
|
||||||
},
|
},
|
||||||
isScanning() {
|
isScanning() {
|
||||||
|
return this.isScanningFiles || this.isScanningCovers
|
||||||
|
},
|
||||||
|
isScanningFiles() {
|
||||||
return this.$store.state.isScanning
|
return this.$store.state.isScanning
|
||||||
},
|
},
|
||||||
|
isScanningCovers() {
|
||||||
|
return this.$store.state.isScanningCovers
|
||||||
|
},
|
||||||
|
scanProgressKey() {
|
||||||
|
return this.isScanningFiles ? 'scanProgress' : 'coverScanProgress'
|
||||||
|
},
|
||||||
scanProgress() {
|
scanProgress() {
|
||||||
return this.$store.state.scanProgress
|
return this.$store.state[this.scanProgressKey]
|
||||||
},
|
},
|
||||||
scanPercent() {
|
scanPercent() {
|
||||||
return this.scanProgress ? this.scanProgress.progress + '%' : '0%'
|
return this.scanProgress ? this.scanProgress.progress + '%' : '0%'
|
||||||
@ -31,7 +53,12 @@ export default {
|
|||||||
return this.scanProgress ? this.scanProgress.total : 0
|
return this.scanProgress ? this.scanProgress.total : 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {},
|
methods: {
|
||||||
|
cancelScan() {
|
||||||
|
this.hasCanceled = true
|
||||||
|
this.$root.socket.emit('cancel_scan')
|
||||||
|
}
|
||||||
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
@ -83,21 +83,37 @@ export default {
|
|||||||
}
|
}
|
||||||
this.$store.commit('audiobooks/remove', audiobook)
|
this.$store.commit('audiobooks/remove', audiobook)
|
||||||
},
|
},
|
||||||
scanComplete(results) {
|
scanComplete({ scanType, results }) {
|
||||||
if (!results) results = {}
|
if (scanType === 'covers') {
|
||||||
this.$store.commit('setIsScanning', false)
|
this.$store.commit('setIsScanningCovers', false)
|
||||||
var scanResultMsgs = []
|
if (results) {
|
||||||
if (results.added) scanResultMsgs.push(`${results.added} added`)
|
this.$toast.success(`Scan Finished\nUpdated ${results.found} covers`)
|
||||||
if (results.updated) scanResultMsgs.push(`${results.updated} updated`)
|
}
|
||||||
if (results.removed) scanResultMsgs.push(`${results.removed} removed`)
|
} else {
|
||||||
if (!scanResultMsgs.length) this.$toast.success('Scan Finished\nEverything was up to date')
|
this.$store.commit('setIsScanning', false)
|
||||||
else this.$toast.success('Scan Finished\n' + scanResultMsgs.join('\n'))
|
if (results) {
|
||||||
|
var scanResultMsgs = []
|
||||||
|
if (results.added) scanResultMsgs.push(`${results.added} added`)
|
||||||
|
if (results.updated) scanResultMsgs.push(`${results.updated} updated`)
|
||||||
|
if (results.removed) scanResultMsgs.push(`${results.removed} removed`)
|
||||||
|
if (!scanResultMsgs.length) this.$toast.success('Scan Finished\nEverything was up to date')
|
||||||
|
else this.$toast.success('Scan Finished\n' + scanResultMsgs.join('\n'))
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
scanStart() {
|
scanStart(scanType) {
|
||||||
this.$store.commit('setIsScanning', true)
|
if (scanType === 'covers') {
|
||||||
|
this.$store.commit('setIsScanningCovers', true)
|
||||||
|
} else {
|
||||||
|
this.$store.commit('setIsScanning', true)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
scanProgress(progress) {
|
scanProgress({ scanType, progress }) {
|
||||||
this.$store.commit('setScanProgress', progress)
|
if (scanType === 'covers') {
|
||||||
|
this.$store.commit('setCoverScanProgress', progress)
|
||||||
|
} else {
|
||||||
|
this.$store.commit('setScanProgress', progress)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
userUpdated(user) {
|
userUpdated(user) {
|
||||||
if (this.$store.state.user.user.id === user.id) {
|
if (this.$store.state.user.user.id === user.id) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "0.9.72-beta",
|
"version": "0.9.73-beta",
|
||||||
"description": "Audiobook manager and player",
|
"description": "Audiobook manager and player",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-bg page overflow-hidden relative" :class="streamAudiobook ? 'streaming' : ''">
|
<div id="ab-page-wrapper" class="bg-bg page overflow-hidden relative" :class="streamAudiobook ? 'streaming' : ''">
|
||||||
<div v-show="saving" class="absolute z-20 w-full h-full flex items-center justify-center">
|
<div v-show="saving" class="absolute z-20 w-full h-full flex items-center justify-center">
|
||||||
<ui-loading-indicator />
|
<ui-loading-indicator />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-bg page overflow-hidden" :class="streamAudiobook ? 'streaming' : ''">
|
<div id="ab-page-wrapper" class="bg-bg page overflow-hidden" :class="streamAudiobook ? 'streaming' : ''">
|
||||||
<div class="w-full h-full overflow-y-auto p-8">
|
<div class="w-full h-full overflow-y-auto p-8">
|
||||||
<div class="flex max-w-6xl mx-auto">
|
<div class="flex max-w-6xl mx-auto">
|
||||||
<div class="w-52" style="min-width: 208px">
|
<div class="w-52" style="min-width: 208px">
|
||||||
@ -10,7 +10,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-10">
|
<div class="flex-grow px-10">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<h1 class="text-2xl">{{ title }}</h1>
|
<div class="mb-2">
|
||||||
|
<h1 class="text-2xl font-book leading-7">{{ title }}</h1>
|
||||||
|
<h3 v-if="series" class="font-book text-gray-300 text-lg leading-7">{{ seriesText }}</h3>
|
||||||
|
<p class="text-sm text-gray-100 leading-7">by {{ author }}</p>
|
||||||
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-300 text-sm my-1">
|
<p class="text-gray-300 text-sm my-1">
|
||||||
@ -133,6 +137,17 @@ export default {
|
|||||||
author() {
|
author() {
|
||||||
return this.book.author || 'Unknown'
|
return this.book.author || 'Unknown'
|
||||||
},
|
},
|
||||||
|
series() {
|
||||||
|
return this.book.series || null
|
||||||
|
},
|
||||||
|
volumeNumber() {
|
||||||
|
return this.book.volumeNumber || null
|
||||||
|
},
|
||||||
|
seriesText() {
|
||||||
|
if (!this.series) return ''
|
||||||
|
if (!this.volumeNumber) return this.series
|
||||||
|
return `${this.series} #${this.volumeNumber}`
|
||||||
|
},
|
||||||
durationPretty() {
|
durationPretty() {
|
||||||
return this.audiobook.durationPretty
|
return this.audiobook.durationPretty
|
||||||
},
|
},
|
||||||
|
@ -26,10 +26,15 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-0.5 bg-primary bg-opacity-50 w-full" />
|
<div class="h-0.5 bg-primary bg-opacity-50 w-full" />
|
||||||
<div class="flex items-center py-4 mb-8">
|
<div class="py-4 mb-8">
|
||||||
<p class="text-2xl">Scanner</p>
|
<div class="flex items-start py-2">
|
||||||
<div class="flex-grow" />
|
<p class="text-2xl">Scanner</p>
|
||||||
<ui-btn color="success" @click="scan">Scan</ui-btn>
|
<div class="flex-grow" />
|
||||||
|
<div class="w-40 flex flex-col">
|
||||||
|
<ui-btn color="success" class="mb-4" :loading="isScanning" :disabled="isScanningCovers" @click="scan">Scan</ui-btn>
|
||||||
|
<ui-btn color="primary" small :padding-x="2" :loading="isScanningCovers" :disabled="isScanning" @click="scanCovers">Scan for Covers</ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="h-0.5 bg-primary bg-opacity-50 w-full" />
|
<div class="h-0.5 bg-primary bg-opacity-50 w-full" />
|
||||||
@ -68,6 +73,12 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
streamAudiobook() {
|
streamAudiobook() {
|
||||||
return this.$store.state.streamAudiobook
|
return this.$store.state.streamAudiobook
|
||||||
|
},
|
||||||
|
isScanning() {
|
||||||
|
return this.$store.state.isScanning
|
||||||
|
},
|
||||||
|
isScanningCovers() {
|
||||||
|
return this.$store.state.isScanningCovers
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -79,6 +90,9 @@ export default {
|
|||||||
scan() {
|
scan() {
|
||||||
this.$root.socket.emit('scan')
|
this.$root.socket.emit('scan')
|
||||||
},
|
},
|
||||||
|
scanCovers() {
|
||||||
|
this.$root.socket.emit('scan_covers')
|
||||||
|
},
|
||||||
clickAddUser() {
|
clickAddUser() {
|
||||||
this.$toast.info('Under Construction: User management coming soon.')
|
this.$toast.info('Under Construction: User management coming soon.')
|
||||||
},
|
},
|
||||||
|
@ -109,6 +109,21 @@ Vue.prototype.$codeToString = (code) => {
|
|||||||
return finalform
|
return finalform
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cleanString(str, availableChars) {
|
||||||
|
var _str = str.normalize('NFD').replace(/[\u0300-\u036f]/g, "")
|
||||||
|
var cleaned = ''
|
||||||
|
for (let i = 0; i < _str.length; i++) {
|
||||||
|
cleaned += availableChars.indexOf(str[i]) < 0 ? '' : str[i]
|
||||||
|
}
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cleanFilterString = (str) => {
|
||||||
|
var _str = str.toLowerCase().replace(/ /g, '_')
|
||||||
|
_str = cleanString(_str, "0123456789abcdefghijklmnopqrstuvwxyz")
|
||||||
|
return _str
|
||||||
|
}
|
||||||
|
|
||||||
function loadImageBlob(uri) {
|
function loadImageBlob(uri) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const img = document.createElement('img')
|
const img = document.createElement('img')
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { sort } from '@/assets/fastSort'
|
import { sort } from '@/assets/fastSort'
|
||||||
|
import { cleanFilterString } from '@/plugins/init.client'
|
||||||
|
|
||||||
const STANDARD_GENRES = ['adventure', 'autobiography', 'biography', 'childrens', 'comedy', 'crime', 'dystopian', 'fantasy', 'fiction', 'health', 'history', 'horror', 'mystery', 'new_adult', 'nonfiction', 'philosophy', 'politics', 'religion', 'romance', 'sci-fi', 'self-help', 'short_story', 'technology', 'thriller', 'true_crime', 'western', 'young_adult']
|
const STANDARD_GENRES = ['adventure', 'autobiography', 'biography', 'childrens', 'comedy', 'crime', 'dystopian', 'fantasy', 'fiction', 'health', 'history', 'horror', 'mystery', 'new_adult', 'nonfiction', 'philosophy', 'politics', 'religion', 'romance', 'sci-fi', 'self-help', 'short_story', 'technology', 'thriller', 'true_crime', 'western', 'young_adult']
|
||||||
|
|
||||||
@ -16,13 +17,14 @@ export const getters = {
|
|||||||
var settings = rootState.user.settings || {}
|
var settings = rootState.user.settings || {}
|
||||||
var filterBy = settings.filterBy || ''
|
var filterBy = settings.filterBy || ''
|
||||||
|
|
||||||
var searchGroups = ['genres', 'tags', 'series']
|
var searchGroups = ['genres', 'tags', 'series', 'authors']
|
||||||
var group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
|
var group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
|
||||||
if (group) {
|
if (group) {
|
||||||
var filter = filterBy.replace(`${group}.`, '')
|
var filter = filterBy.replace(`${group}.`, '')
|
||||||
if (group === 'genres') filtered = filtered.filter(ab => ab.book && ab.book.genres.includes(filter))
|
if (group === 'genres') filtered = filtered.filter(ab => ab.book && ab.book.genres.includes(filter))
|
||||||
else if (group === 'tags') filtered = filtered.filter(ab => ab.tags.includes(filter))
|
else if (group === 'tags') filtered = filtered.filter(ab => ab.tags.includes(filter))
|
||||||
else if (group === 'series') filtered = filtered.filter(ab => ab.book && ab.book.series === filter)
|
else if (group === 'series') filtered = filtered.filter(ab => ab.book && ab.book.series === filter)
|
||||||
|
else if (group === 'authors') filtered = filtered.filter(ab => ab.book && ab.book.author === filter)
|
||||||
}
|
}
|
||||||
return filtered
|
return filtered
|
||||||
},
|
},
|
||||||
@ -35,6 +37,10 @@ export const getters = {
|
|||||||
// Supports dot notation strings i.e. "book.title"
|
// Supports dot notation strings i.e. "book.title"
|
||||||
return settings.orderBy.split('.').reduce((a, b) => a[b], ab)
|
return settings.orderBy.split('.').reduce((a, b) => a[b], ab)
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
getUniqueAuthors: (state) => {
|
||||||
|
var _authors = state.audiobooks.filter(ab => !!(ab.book && ab.book.author)).map(ab => ab.book.author)
|
||||||
|
return [...new Set(_authors)]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,9 @@ export const state = () => ({
|
|||||||
selectedAudiobook: null,
|
selectedAudiobook: null,
|
||||||
playOnLoad: false,
|
playOnLoad: false,
|
||||||
isScanning: false,
|
isScanning: false,
|
||||||
|
isScanningCovers: false,
|
||||||
scanProgress: null,
|
scanProgress: null,
|
||||||
|
coverScanProgress: null,
|
||||||
developerMode: false
|
developerMode: false
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -41,9 +43,16 @@ export const mutations = {
|
|||||||
setIsScanning(state, isScanning) {
|
setIsScanning(state, isScanning) {
|
||||||
state.isScanning = isScanning
|
state.isScanning = isScanning
|
||||||
},
|
},
|
||||||
setScanProgress(state, progress) {
|
setScanProgress(state, scanProgress) {
|
||||||
if (progress > 0) state.isScanning = true
|
if (scanProgress && scanProgress.progress > 0) state.isScanning = true
|
||||||
state.scanProgress = progress
|
state.scanProgress = scanProgress
|
||||||
|
},
|
||||||
|
setIsScanningCovers(state, isScanningCovers) {
|
||||||
|
state.isScanningCovers = isScanningCovers
|
||||||
|
},
|
||||||
|
setCoverScanProgress(state, coverScanProgress) {
|
||||||
|
if (coverScanProgress && coverScanProgress.progress > 0) state.isScanningCovers = true
|
||||||
|
state.coverScanProgress = coverScanProgress
|
||||||
},
|
},
|
||||||
setDeveloperMode(state, val) {
|
setDeveloperMode(state, val) {
|
||||||
state.developerMode = val
|
state.developerMode = val
|
||||||
|
@ -16,12 +16,11 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
bg: '#373838',
|
bg: '#373838',
|
||||||
primary: '#262626',
|
primary: '#232323',
|
||||||
accent: '#1ad691',
|
accent: '#1ad691',
|
||||||
error: '#FF5252',
|
error: '#FF5252',
|
||||||
info: '#2196F3',
|
info: '#2196F3',
|
||||||
success: '#4CAF50',
|
success: '#4CAF50',
|
||||||
successDark: '#3b8a3e',
|
|
||||||
warning: '#FB8C00',
|
warning: '#FB8C00',
|
||||||
'black-50': '#bbbbbb',
|
'black-50': '#bbbbbb',
|
||||||
'black-100': '#666666',
|
'black-100': '#666666',
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "0.9.72-beta",
|
"version": "0.9.73-beta",
|
||||||
"description": "Self-hosted audiobook server for managing and playing audiobooks.",
|
"description": "Self-hosted audiobook server for managing and playing audiobooks.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -26,4 +26,4 @@
|
|||||||
"socket.io": "^4.1.3"
|
"socket.io": "^4.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {}
|
"devDependencies": {}
|
||||||
}
|
}
|
@ -62,6 +62,10 @@ class Audiobook {
|
|||||||
return this.book ? this.book.author : 'Unknown'
|
return this.book ? this.book.author : 'Unknown'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get authorLF() {
|
||||||
|
return this.book ? this.book.authorLF : null
|
||||||
|
}
|
||||||
|
|
||||||
get genres() {
|
get genres() {
|
||||||
return this.book ? this.book.genres || [] : []
|
return this.book ? this.book.genres || [] : []
|
||||||
}
|
}
|
||||||
@ -136,9 +140,9 @@ class Audiobook {
|
|||||||
toJSONExpanded() {
|
toJSONExpanded() {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
title: this.title,
|
// title: this.title,
|
||||||
author: this.author,
|
// author: this.author,
|
||||||
cover: this.cover,
|
// cover: this.cover,
|
||||||
path: this.path,
|
path: this.path,
|
||||||
fullPath: this.fullPath,
|
fullPath: this.fullPath,
|
||||||
addedAt: this.addedAt,
|
addedAt: this.addedAt,
|
||||||
@ -306,6 +310,10 @@ class Audiobook {
|
|||||||
return hasUpdates
|
return hasUpdates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
syncAuthorNames(audiobookData) {
|
||||||
|
return this.book.syncAuthorNames(audiobookData.authorFL, audiobookData.authorLF)
|
||||||
|
}
|
||||||
|
|
||||||
isSearchMatch(search) {
|
isSearchMatch(search) {
|
||||||
return this.book.isSearchMatch(search.toLowerCase().trim())
|
return this.book.isSearchMatch(search.toLowerCase().trim())
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,10 @@ class Book {
|
|||||||
this.olid = null
|
this.olid = null
|
||||||
this.title = null
|
this.title = null
|
||||||
this.author = null
|
this.author = null
|
||||||
|
this.authorFL = null
|
||||||
|
this.authorLF = null
|
||||||
this.series = null
|
this.series = null
|
||||||
|
this.volumeNumber = null
|
||||||
this.publishYear = null
|
this.publishYear = null
|
||||||
this.publisher = null
|
this.publisher = null
|
||||||
this.description = null
|
this.description = null
|
||||||
@ -24,7 +27,10 @@ class Book {
|
|||||||
this.olid = book.olid
|
this.olid = book.olid
|
||||||
this.title = book.title
|
this.title = book.title
|
||||||
this.author = book.author
|
this.author = book.author
|
||||||
|
this.authorFL = book.authorFL || null
|
||||||
|
this.authorLF = book.authorLF || null
|
||||||
this.series = book.series
|
this.series = book.series
|
||||||
|
this.volumeNumber = book.volumeNumber || null
|
||||||
this.publishYear = book.publishYear
|
this.publishYear = book.publishYear
|
||||||
this.publisher = book.publisher
|
this.publisher = book.publisher
|
||||||
this.description = book.description
|
this.description = book.description
|
||||||
@ -37,7 +43,10 @@ class Book {
|
|||||||
olid: this.olid,
|
olid: this.olid,
|
||||||
title: this.title,
|
title: this.title,
|
||||||
author: this.author,
|
author: this.author,
|
||||||
|
authorFL: this.authorFL,
|
||||||
|
authorLF: this.authorLF,
|
||||||
series: this.series,
|
series: this.series,
|
||||||
|
volumeNumber: this.volumeNumber,
|
||||||
publishYear: this.publishYear,
|
publishYear: this.publishYear,
|
||||||
publisher: this.publisher,
|
publisher: this.publisher,
|
||||||
description: this.description,
|
description: this.description,
|
||||||
@ -50,7 +59,10 @@ class Book {
|
|||||||
this.olid = data.olid || null
|
this.olid = data.olid || null
|
||||||
this.title = data.title || null
|
this.title = data.title || null
|
||||||
this.author = data.author || null
|
this.author = data.author || null
|
||||||
|
this.authorLF = data.authorLF || null
|
||||||
|
this.authorFL = data.authorFL || null
|
||||||
this.series = data.series || null
|
this.series = data.series || null
|
||||||
|
this.volumeNumber = data.volumeNumber || null
|
||||||
this.publishYear = data.publishYear || null
|
this.publishYear = data.publishYear || null
|
||||||
this.description = data.description || null
|
this.description = data.description || null
|
||||||
this.cover = data.cover || null
|
this.cover = data.cover || null
|
||||||
@ -83,7 +95,20 @@ class Book {
|
|||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return hasUpdates
|
||||||
|
}
|
||||||
|
|
||||||
|
syncAuthorNames(authorFL, authorLF) {
|
||||||
|
var hasUpdates = false
|
||||||
|
if (authorFL !== this.authorFL) {
|
||||||
|
this.authorFL = authorFL
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
if (authorLF !== this.authorLF) {
|
||||||
|
this.authorLF = authorLF
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
return hasUpdates
|
||||||
}
|
}
|
||||||
|
|
||||||
isSearchMatch(search) {
|
isSearchMatch(search) {
|
||||||
|
@ -26,7 +26,17 @@ class BookFinder {
|
|||||||
return title
|
return title
|
||||||
}
|
}
|
||||||
|
|
||||||
|
replaceAccentedChars(str) {
|
||||||
|
try {
|
||||||
|
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, "")
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('[BookFinder] str normalize error', error)
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cleanTitleForCompares(title) {
|
cleanTitleForCompares(title) {
|
||||||
|
if (!title) return ''
|
||||||
// Remove subtitle if there (i.e. "Cool Book: Coolest Ever" becomes "Cool Book")
|
// Remove subtitle if there (i.e. "Cool Book: Coolest Ever" becomes "Cool Book")
|
||||||
var stripped = this.stripSubtitle(title)
|
var stripped = this.stripSubtitle(title)
|
||||||
|
|
||||||
@ -35,16 +45,34 @@ class BookFinder {
|
|||||||
|
|
||||||
// Remove single quotes (i.e. "Ender's Game" becomes "Enders Game")
|
// Remove single quotes (i.e. "Ender's Game" becomes "Enders Game")
|
||||||
cleaned = cleaned.replace(/'/g, '')
|
cleaned = cleaned.replace(/'/g, '')
|
||||||
|
cleaned = this.replaceAccentedChars(cleaned)
|
||||||
|
return cleaned.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanAuthorForCompares(author) {
|
||||||
|
if (!author) return ''
|
||||||
|
var cleaned = this.replaceAccentedChars(author)
|
||||||
return cleaned.toLowerCase()
|
return cleaned.toLowerCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance) {
|
filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance) {
|
||||||
var searchTitle = this.cleanTitleForCompares(title)
|
var searchTitle = this.cleanTitleForCompares(title)
|
||||||
|
var searchAuthor = this.cleanAuthorForCompares(author)
|
||||||
return books.map(b => {
|
return books.map(b => {
|
||||||
b.cleanedTitle = this.cleanTitleForCompares(b.title)
|
b.cleanedTitle = this.cleanTitleForCompares(b.title)
|
||||||
b.titleDistance = levenshteinDistance(b.cleanedTitle, title)
|
b.titleDistance = levenshteinDistance(b.cleanedTitle, title)
|
||||||
if (author) {
|
if (author) {
|
||||||
b.authorDistance = levenshteinDistance(b.author || '', author)
|
if (!b.author) {
|
||||||
|
b.authorDistance = author.length
|
||||||
|
} else {
|
||||||
|
b.cleanedAuthor = this.cleanAuthorForCompares(b.author)
|
||||||
|
|
||||||
|
var cleanedAuthorDistance = levenshteinDistance(b.cleanedAuthor, searchAuthor)
|
||||||
|
var authorDistance = levenshteinDistance(b.author || '', author)
|
||||||
|
// Use best distance
|
||||||
|
if (cleanedAuthorDistance > authorDistance) b.authorDistance = authorDistance
|
||||||
|
else b.authorDistance = cleanedAuthorDistance
|
||||||
|
}
|
||||||
}
|
}
|
||||||
b.totalDistance = b.titleDistance + (b.authorDistance || 0)
|
b.totalDistance = b.titleDistance + (b.authorDistance || 0)
|
||||||
b.totalPossibleDistance = b.title.length
|
b.totalPossibleDistance = b.title.length
|
||||||
@ -142,7 +170,8 @@ class BookFinder {
|
|||||||
|
|
||||||
async findCovers(provider, title, author, options = {}) {
|
async findCovers(provider, title, author, options = {}) {
|
||||||
var searchResults = await this.search(provider, title, author, options)
|
var searchResults = await this.search(provider, title, author, options)
|
||||||
console.log('Find Covers search results', searchResults)
|
Logger.info(`[BookFinder] FindCovers search results: ${searchResults.length}`)
|
||||||
|
|
||||||
var covers = []
|
var covers = []
|
||||||
searchResults.forEach((result) => {
|
searchResults.forEach((result) => {
|
||||||
if (result.covers && result.covers.length) {
|
if (result.covers && result.covers.length) {
|
||||||
|
@ -13,6 +13,8 @@ class Scanner {
|
|||||||
this.db = db
|
this.db = db
|
||||||
this.emitter = emitter
|
this.emitter = emitter
|
||||||
|
|
||||||
|
this.cancelScan = false
|
||||||
|
|
||||||
this.bookFinder = new BookFinder()
|
this.bookFinder = new BookFinder()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,6 +36,11 @@ class Scanner {
|
|||||||
const scanStart = Date.now()
|
const scanStart = Date.now()
|
||||||
var audiobookDataFound = await getAllAudiobookFiles(this.AudiobookPath)
|
var audiobookDataFound = await getAllAudiobookFiles(this.AudiobookPath)
|
||||||
|
|
||||||
|
if (this.cancelScan) {
|
||||||
|
this.cancelScan = false
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
var scanResults = {
|
var scanResults = {
|
||||||
removed: 0,
|
removed: 0,
|
||||||
updated: 0,
|
updated: 0,
|
||||||
@ -54,6 +61,10 @@ class Scanner {
|
|||||||
scanResults.removed++
|
scanResults.removed++
|
||||||
this.emitter('audiobook_removed', this.audiobooks[i].toJSONMinified())
|
this.emitter('audiobook_removed', this.audiobooks[i].toJSONMinified())
|
||||||
}
|
}
|
||||||
|
if (this.cancelScan) {
|
||||||
|
this.cancelScan = false
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < audiobookDataFound.length; i++) {
|
for (let i = 0; i < audiobookDataFound.length; i++) {
|
||||||
@ -109,6 +120,11 @@ class Scanner {
|
|||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (audiobookData.author && existingAudiobook.syncAuthorNames(audiobookData)) {
|
||||||
|
Logger.info(`[Scanner] "${existingAudiobook.title}" author names updated, "${existingAudiobook.authorLF}"`)
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
|
||||||
if (hasUpdates) {
|
if (hasUpdates) {
|
||||||
Logger.info(`[Scanner] "${existingAudiobook.title}" was updated - saving`)
|
Logger.info(`[Scanner] "${existingAudiobook.title}" was updated - saving`)
|
||||||
existingAudiobook.lastUpdate = Date.now()
|
existingAudiobook.lastUpdate = Date.now()
|
||||||
@ -138,10 +154,17 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
var progress = Math.round(100 * (i + 1) / audiobookDataFound.length)
|
var progress = Math.round(100 * (i + 1) / audiobookDataFound.length)
|
||||||
this.emitter('scan_progress', {
|
this.emitter('scan_progress', {
|
||||||
total: audiobookDataFound.length,
|
scanType: 'files',
|
||||||
done: i + 1,
|
progress: {
|
||||||
progress
|
total: audiobookDataFound.length,
|
||||||
|
done: i + 1,
|
||||||
|
progress
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
if (this.cancelScan) {
|
||||||
|
this.cancelScan = false
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const scanElapsed = Math.floor((Date.now() - scanStart) / 1000)
|
const scanElapsed = Math.floor((Date.now() - scanStart) / 1000)
|
||||||
Logger.info(`[Scanned] Finished | ${scanResults.added} added | ${scanResults.updated} updated | ${scanResults.removed} removed | elapsed: ${secondsToTimestamp(scanElapsed)}`)
|
Logger.info(`[Scanned] Finished | ${scanResults.added} added | ${scanResults.updated} updated | ${scanResults.removed} removed | elapsed: ${secondsToTimestamp(scanElapsed)}`)
|
||||||
@ -161,6 +184,47 @@ class Scanner {
|
|||||||
return scanResult
|
return scanResult
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async scanCovers() {
|
||||||
|
var audiobooksNeedingCover = this.audiobooks.filter(ab => !ab.cover && ab.author)
|
||||||
|
var found = 0
|
||||||
|
var notFound = 0
|
||||||
|
for (let i = 0; i < audiobooksNeedingCover.length; i++) {
|
||||||
|
var audiobook = audiobooksNeedingCover[i]
|
||||||
|
var options = {
|
||||||
|
titleDistance: 2,
|
||||||
|
authorDistance: 2
|
||||||
|
}
|
||||||
|
var results = await this.bookFinder.findCovers('openlibrary', audiobook.title, audiobook.author, options)
|
||||||
|
if (results.length) {
|
||||||
|
Logger.info(`[Scanner] Found best cover for "${audiobook.title}"`)
|
||||||
|
audiobook.book.cover = results[0]
|
||||||
|
await this.db.updateAudiobook(audiobook)
|
||||||
|
found++
|
||||||
|
} else {
|
||||||
|
notFound++
|
||||||
|
}
|
||||||
|
|
||||||
|
var progress = Math.round(100 * (i + 1) / audiobooksNeedingCover.length)
|
||||||
|
this.emitter('scan_progress', {
|
||||||
|
scanType: 'covers',
|
||||||
|
progress: {
|
||||||
|
total: audiobooksNeedingCover.length,
|
||||||
|
done: i + 1,
|
||||||
|
progress
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.cancelScan) {
|
||||||
|
this.cancelScan = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
found,
|
||||||
|
notFound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async find(req, res) {
|
async find(req, res) {
|
||||||
var method = req.params.method
|
var method = req.params.method
|
||||||
var query = req.query
|
var query = req.query
|
||||||
|
@ -42,6 +42,7 @@ class Server {
|
|||||||
this.clients = {}
|
this.clients = {}
|
||||||
|
|
||||||
this.isScanning = false
|
this.isScanning = false
|
||||||
|
this.isScanningCovers = false
|
||||||
this.isInitialized = false
|
this.isInitialized = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,13 +65,28 @@ class Server {
|
|||||||
Logger.info('[Server] Starting Scan')
|
Logger.info('[Server] Starting Scan')
|
||||||
this.isScanning = true
|
this.isScanning = true
|
||||||
this.isInitialized = true
|
this.isInitialized = true
|
||||||
this.emitter('scan_start')
|
this.emitter('scan_start', 'files')
|
||||||
var results = await this.scanner.scan()
|
var results = await this.scanner.scan()
|
||||||
this.isScanning = false
|
this.isScanning = false
|
||||||
this.emitter('scan_complete', results)
|
this.emitter('scan_complete', { scanType: 'files', results })
|
||||||
Logger.info('[Server] Scan complete')
|
Logger.info('[Server] Scan complete')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async scanCovers() {
|
||||||
|
Logger.info('[Server] Start cover scan')
|
||||||
|
this.isScanningCovers = true
|
||||||
|
this.emitter('scan_start', 'covers')
|
||||||
|
var results = await this.scanner.scanCovers()
|
||||||
|
this.isScanningCovers = false
|
||||||
|
this.emitter('scan_complete', { scanType: 'covers', results })
|
||||||
|
Logger.info('[Server] Cover scan complete')
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelScan() {
|
||||||
|
if (!this.isScanningCovers && !this.isScanning) return
|
||||||
|
this.scanner.cancelScan = true
|
||||||
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
Logger.info('[Server] Init')
|
Logger.info('[Server] Init')
|
||||||
await this.streamManager.removeOrphanStreams()
|
await this.streamManager.removeOrphanStreams()
|
||||||
@ -149,6 +165,8 @@ class Server {
|
|||||||
|
|
||||||
socket.on('auth', (token) => this.authenticateSocket(socket, token))
|
socket.on('auth', (token) => this.authenticateSocket(socket, token))
|
||||||
socket.on('scan', this.scan.bind(this))
|
socket.on('scan', this.scan.bind(this))
|
||||||
|
socket.on('scan_covers', this.scanCovers.bind(this))
|
||||||
|
socket.on('cancel_scan', this.cancelScan.bind(this))
|
||||||
socket.on('open_stream', (audiobookId) => this.streamManager.openStreamSocketRequest(socket, audiobookId))
|
socket.on('open_stream', (audiobookId) => this.streamManager.openStreamSocketRequest(socket, audiobookId))
|
||||||
socket.on('close_stream', () => this.streamManager.closeStreamRequest(socket))
|
socket.on('close_stream', () => this.streamManager.closeStreamRequest(socket))
|
||||||
socket.on('stream_update', (payload) => this.streamManager.streamUpdate(socket, payload))
|
socket.on('stream_update', (payload) => this.streamManager.streamUpdate(socket, payload))
|
||||||
|
67
server/utils/parseAuthors.js
Normal file
67
server/utils/parseAuthors.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
const parseFullName = require('./parseFullName')
|
||||||
|
|
||||||
|
function parseName(name) {
|
||||||
|
var parts = parseFullName(name)
|
||||||
|
var firstName = parts.first
|
||||||
|
if (firstName && parts.middle) firstName += ' ' + parts.middle
|
||||||
|
|
||||||
|
return {
|
||||||
|
first_name: firstName,
|
||||||
|
last_name: parts.last
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this name segment is of the format "Last, First" or "First Last"
|
||||||
|
// return true is "Last, First"
|
||||||
|
function checkIsALastName(name) {
|
||||||
|
if (!name.includes(' ')) return true // No spaces must be a Last name
|
||||||
|
|
||||||
|
var parsed = parseFullName(name)
|
||||||
|
if (!parsed.first) return true // had spaces but not a first name i.e. "von Mises", must be last name only
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = (author) => {
|
||||||
|
if (!author) return null
|
||||||
|
var splitByComma = author.split(', ')
|
||||||
|
|
||||||
|
var authors = []
|
||||||
|
|
||||||
|
// 1 author FIRST LAST
|
||||||
|
if (splitByComma.length === 1) {
|
||||||
|
authors.push(parseName(author))
|
||||||
|
} else {
|
||||||
|
var firstChunkIsALastName = checkIsALastName(splitByComma[0])
|
||||||
|
var isEvenNum = splitByComma.length % 2 === 0
|
||||||
|
|
||||||
|
if (!isEvenNum && firstChunkIsALastName) {
|
||||||
|
// console.error('Multi-author LAST,FIRST entry has a straggler (could be roman numerals or a suffix), ignore it', splitByComma[splitByComma.length - 1])
|
||||||
|
splitByComma = splitByComma.slice(0, splitByComma.length - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstChunkIsALastName) {
|
||||||
|
var numAuthors = splitByComma.length / 2
|
||||||
|
for (let i = 0; i < numAuthors; i++) {
|
||||||
|
var last = splitByComma.shift()
|
||||||
|
var first = splitByComma.shift()
|
||||||
|
authors.push({
|
||||||
|
first_name: first,
|
||||||
|
last_name: last
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
splitByComma.forEach((segment) => {
|
||||||
|
authors.push(parseName(segment))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var firstLast = authors.length ? authors.map(a => a.first_name ? `${a.first_name} ${a.last_name}` : a.last_name).join(', ') : ''
|
||||||
|
var lastFirst = authors.length ? authors.map(a => a.first_name ? `${a.last_name}, ${a.first_name}` : a.last_name).join(', ') : ''
|
||||||
|
return {
|
||||||
|
authorFL: firstLast,
|
||||||
|
authorLF: lastFirst,
|
||||||
|
authorsParsed: authors
|
||||||
|
}
|
||||||
|
}
|
346
server/utils/parseFullName.js
Normal file
346
server/utils/parseFullName.js
Normal file
@ -0,0 +1,346 @@
|
|||||||
|
|
||||||
|
|
||||||
|
// https://github.com/RateGravity/parse-full-name/blob/master/index.js
|
||||||
|
module.exports = (nameToParse, partToReturn, fixCase, stopOnError, useLongLists) => {
|
||||||
|
|
||||||
|
var i, j, k, l, m, n, part, comma, titleList, suffixList, prefixList, regex,
|
||||||
|
partToCheck, partFound, partsFoundCount, firstComma, remainingCommas,
|
||||||
|
nameParts = [], nameCommas = [null], partsFound = [],
|
||||||
|
conjunctionList = ['&', 'and', 'et', 'e', 'of', 'the', 'und', 'y'],
|
||||||
|
parsedName = {
|
||||||
|
title: '', first: '', middle: '', last: '', nick: '', suffix: '', error: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validate inputs, or set to defaults
|
||||||
|
partToReturn = partToReturn && ['title', 'first', 'middle', 'last', 'nick',
|
||||||
|
'suffix', 'error'].indexOf(partToReturn.toLowerCase()) > -1 ?
|
||||||
|
partToReturn.toLowerCase() : 'all';
|
||||||
|
// 'all' = return object with all parts, others return single part
|
||||||
|
if (fixCase === false) fixCase = 0;
|
||||||
|
if (fixCase === true) fixCase = 1;
|
||||||
|
fixCase = fixCase !== 'undefined' && (fixCase === 0 || fixCase === 1) ?
|
||||||
|
fixCase : -1; // -1 = fix case only if input is all upper or lowercase
|
||||||
|
if (stopOnError === true) stopOnError = 1;
|
||||||
|
stopOnError = stopOnError && stopOnError === 1 ? 1 : 0;
|
||||||
|
// false = output warnings on parse error, but don't stop
|
||||||
|
if (useLongLists === true) useLongLists = 1;
|
||||||
|
useLongLists = useLongLists && useLongLists === 1 ? 1 : 0; // 0 = short lists
|
||||||
|
|
||||||
|
// If stopOnError = 1, throw error, otherwise return error messages in array
|
||||||
|
function handleError(errorMessage) {
|
||||||
|
if (stopOnError) {
|
||||||
|
throw 'Error: ' + errorMessage;
|
||||||
|
} else {
|
||||||
|
parsedName.error.push('Error: ' + errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If fixCase = 1, fix case of parsedName parts before returning
|
||||||
|
function fixParsedNameCase(fixedCaseName, fixCaseNow) {
|
||||||
|
var forceCaseList = ['e', 'y', 'av', 'af', 'da', 'dal', 'de', 'del', 'der', 'di',
|
||||||
|
'la', 'le', 'van', 'der', 'den', 'vel', 'von', 'II', 'III', 'IV', 'J.D.', 'LL.M.',
|
||||||
|
'M.D.', 'D.O.', 'D.C.', 'Ph.D.'];
|
||||||
|
var forceCaseListIndex;
|
||||||
|
var namePartLabels = [];
|
||||||
|
var namePartWords;
|
||||||
|
if (fixCaseNow) {
|
||||||
|
namePartLabels = Object.keys(parsedName)
|
||||||
|
.filter(function (v) { return v !== 'error'; });
|
||||||
|
for (i = 0, l = namePartLabels.length; i < l; i++) {
|
||||||
|
if (fixedCaseName[namePartLabels[i]]) {
|
||||||
|
namePartWords = (fixedCaseName[namePartLabels[i]] + '').split(' ');
|
||||||
|
for (j = 0, m = namePartWords.length; j < m; j++) {
|
||||||
|
forceCaseListIndex = forceCaseList
|
||||||
|
.map(function (v) { return v.toLowerCase(); })
|
||||||
|
.indexOf(namePartWords[j].toLowerCase());
|
||||||
|
if (forceCaseListIndex > -1) { // Set case of words in forceCaseList
|
||||||
|
namePartWords[j] = forceCaseList[forceCaseListIndex];
|
||||||
|
} else if (namePartWords[j].length === 1) { // Uppercase initials
|
||||||
|
namePartWords[j] = namePartWords[j].toUpperCase();
|
||||||
|
} else if (
|
||||||
|
namePartWords[j].length > 2 &&
|
||||||
|
namePartWords[j].slice(0, 1) ===
|
||||||
|
namePartWords[j].slice(0, 1).toUpperCase() &&
|
||||||
|
namePartWords[j].slice(1, 2) ===
|
||||||
|
namePartWords[j].slice(1, 2).toLowerCase() &&
|
||||||
|
namePartWords[j].slice(2) ===
|
||||||
|
namePartWords[j].slice(2).toUpperCase()
|
||||||
|
) { // Detect McCASE and convert to McCase
|
||||||
|
namePartWords[j] = namePartWords[j].slice(0, 3) +
|
||||||
|
namePartWords[j].slice(3).toLowerCase();
|
||||||
|
} else if (
|
||||||
|
namePartLabels[j] === 'suffix' &&
|
||||||
|
nameParts[j].slice(-1) !== '.' &&
|
||||||
|
!suffixList.indexOf(nameParts[j].toLowerCase())
|
||||||
|
) { // Convert suffix abbreviations to UPPER CASE
|
||||||
|
if (namePartWords[j] === namePartWords[j].toLowerCase()) {
|
||||||
|
namePartWords[j] = namePartWords[j].toUpperCase();
|
||||||
|
}
|
||||||
|
} else { // Convert to Title Case
|
||||||
|
namePartWords[j] = namePartWords[j].slice(0, 1).toUpperCase() +
|
||||||
|
namePartWords[j].slice(1).toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fixedCaseName[namePartLabels[i]] = namePartWords.join(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fixedCaseName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no input name, or input name is not a string, abort
|
||||||
|
if (!nameToParse || typeof nameToParse !== 'string') {
|
||||||
|
handleError('No input');
|
||||||
|
parsedName = fixParsedNameCase(parsedName, fixCase);
|
||||||
|
return partToReturn === 'all' ? parsedName : parsedName[partToReturn];
|
||||||
|
} else {
|
||||||
|
nameToParse = nameToParse.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-detect fixCase: fix if nameToParse is all upper or all lowercase
|
||||||
|
if (fixCase === -1) {
|
||||||
|
fixCase = (
|
||||||
|
nameToParse === nameToParse.toUpperCase() ||
|
||||||
|
nameToParse === nameToParse.toLowerCase() ? 1 : 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initilize lists of prefixs, suffixs, and titles to detect
|
||||||
|
// Note: These list entries must be all lowercase
|
||||||
|
if (useLongLists) {
|
||||||
|
suffixList = ['esq', 'esquire', 'jr', 'jnr', 'sr', 'snr', '2', 'ii', 'iii', 'iv',
|
||||||
|
'v', 'clu', 'chfc', 'cfp', 'md', 'phd', 'j.d.', 'll.m.', 'm.d.', 'd.o.', 'd.c.',
|
||||||
|
'p.c.', 'ph.d.'];
|
||||||
|
prefixList = ['a', 'ab', 'antune', 'ap', 'abu', 'al', 'alm', 'alt', 'bab', 'bäck',
|
||||||
|
'bar', 'bath', 'bat', 'beau', 'beck', 'ben', 'berg', 'bet', 'bin', 'bint', 'birch',
|
||||||
|
'björk', 'björn', 'bjur', 'da', 'dahl', 'dal', 'de', 'degli', 'dele', 'del',
|
||||||
|
'della', 'der', 'di', 'dos', 'du', 'e', 'ek', 'el', 'escob', 'esch', 'fleisch',
|
||||||
|
'fitz', 'fors', 'gott', 'griff', 'haj', 'haug', 'holm', 'ibn', 'kauf', 'kil',
|
||||||
|
'koop', 'kvarn', 'la', 'le', 'lind', 'lönn', 'lund', 'mac', 'mhic', 'mic', 'mir',
|
||||||
|
'na', 'naka', 'neder', 'nic', 'ni', 'nin', 'nord', 'norr', 'ny', 'o', 'ua', 'ui\'',
|
||||||
|
'öfver', 'ost', 'över', 'öz', 'papa', 'pour', 'quarn', 'skog', 'skoog', 'sten',
|
||||||
|
'stor', 'ström', 'söder', 'ter', 'ter', 'tre', 'türk', 'van', 'väst', 'väster',
|
||||||
|
'vest', 'von'];
|
||||||
|
titleList = ['mr', 'mrs', 'ms', 'miss', 'dr', 'herr', 'monsieur', 'hr', 'frau',
|
||||||
|
'a v m', 'admiraal', 'admiral', 'air cdre', 'air commodore', 'air marshal',
|
||||||
|
'air vice marshal', 'alderman', 'alhaji', 'ambassador', 'baron', 'barones',
|
||||||
|
'brig', 'brig gen', 'brig general', 'brigadier', 'brigadier general',
|
||||||
|
'brother', 'canon', 'capt', 'captain', 'cardinal', 'cdr', 'chief', 'cik', 'cmdr',
|
||||||
|
'coach', 'col', 'col dr', 'colonel', 'commandant', 'commander', 'commissioner',
|
||||||
|
'commodore', 'comte', 'comtessa', 'congressman', 'conseiller', 'consul',
|
||||||
|
'conte', 'contessa', 'corporal', 'councillor', 'count', 'countess',
|
||||||
|
'crown prince', 'crown princess', 'dame', 'datin', 'dato', 'datuk',
|
||||||
|
'datuk seri', 'deacon', 'deaconess', 'dean', 'dhr', 'dipl ing', 'doctor',
|
||||||
|
'dott', 'dott sa', 'dr', 'dr ing', 'dra', 'drs', 'embajador', 'embajadora', 'en',
|
||||||
|
'encik', 'eng', 'eur ing', 'exma sra', 'exmo sr', 'f o', 'father',
|
||||||
|
'first lieutient', 'first officer', 'flt lieut', 'flying officer', 'fr',
|
||||||
|
'frau', 'fraulein', 'fru', 'gen', 'generaal', 'general', 'governor', 'graaf',
|
||||||
|
'gravin', 'group captain', 'grp capt', 'h e dr', 'h h', 'h m', 'h r h', 'hajah',
|
||||||
|
'haji', 'hajim', 'her highness', 'her majesty', 'herr', 'high chief',
|
||||||
|
'his highness', 'his holiness', 'his majesty', 'hon', 'hr', 'hra', 'ing', 'ir',
|
||||||
|
'jonkheer', 'judge', 'justice', 'khun ying', 'kolonel', 'lady', 'lcda', 'lic',
|
||||||
|
'lieut', 'lieut cdr', 'lieut col', 'lieut gen', 'lord', 'm', 'm l', 'm r',
|
||||||
|
'madame', 'mademoiselle', 'maj gen', 'major', 'master', 'mevrouw', 'miss',
|
||||||
|
'mlle', 'mme', 'monsieur', 'monsignor', 'mr', 'mrs', 'ms', 'mstr', 'nti', 'pastor',
|
||||||
|
'president', 'prince', 'princess', 'princesse', 'prinses', 'prof', 'prof dr',
|
||||||
|
'prof sir', 'professor', 'puan', 'puan sri', 'rabbi', 'rear admiral', 'rev',
|
||||||
|
'rev canon', 'rev dr', 'rev mother', 'reverend', 'rva', 'senator', 'sergeant',
|
||||||
|
'sheikh', 'sheikha', 'sig', 'sig na', 'sig ra', 'sir', 'sister', 'sqn ldr', 'sr',
|
||||||
|
'sr d', 'sra', 'srta', 'sultan', 'tan sri', 'tan sri dato', 'tengku', 'teuku',
|
||||||
|
'than puying', 'the hon dr', 'the hon justice', 'the hon miss', 'the hon mr',
|
||||||
|
'the hon mrs', 'the hon ms', 'the hon sir', 'the very rev', 'toh puan', 'tun',
|
||||||
|
'vice admiral', 'viscount', 'viscountess', 'wg cdr', 'ind', 'misc', 'mx'];
|
||||||
|
} else {
|
||||||
|
suffixList = ['esq', 'esquire', 'jr', 'jnr', 'sr', 'snr', '2', 'ii', 'iii', 'iv',
|
||||||
|
'md', 'phd', 'j.d.', 'll.m.', 'm.d.', 'd.o.', 'd.c.', 'p.c.', 'ph.d.'];
|
||||||
|
prefixList = ['ab', 'bar', 'bin', 'da', 'dal', 'de', 'de la', 'del', 'della', 'der',
|
||||||
|
'di', 'du', 'ibn', 'l\'', 'la', 'le', 'san', 'st', 'st.', 'ste', 'ter', 'van',
|
||||||
|
'van de', 'van der', 'van den', 'vel', 'ver', 'vere', 'von'];
|
||||||
|
titleList = ['dr', 'miss', 'mr', 'mrs', 'ms', 'prof', 'sir', 'frau', 'herr', 'hr',
|
||||||
|
'monsieur', 'captain', 'doctor', 'judge', 'officer', 'professor', 'ind', 'misc',
|
||||||
|
'mx'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nickname: remove and store parts with surrounding punctuation as nicknames
|
||||||
|
regex = /\s(?:[‘’']([^‘’']+)[‘’']|[“”"]([^“”"]+)[“”"]|\[([^\]]+)\]|\(([^\)]+)\)),?\s/g;
|
||||||
|
partFound = (' ' + nameToParse + ' ').match(regex);
|
||||||
|
if (partFound) partsFound = partsFound.concat(partFound);
|
||||||
|
partsFoundCount = partsFound.length;
|
||||||
|
if (partsFoundCount === 1) {
|
||||||
|
parsedName.nick = partsFound[0].slice(2).slice(0, -2);
|
||||||
|
if (parsedName.nick.slice(-1) === ',') {
|
||||||
|
parsedName.nick = parsedName.nick.slice(0, -1);
|
||||||
|
}
|
||||||
|
nameToParse = (' ' + nameToParse + ' ').replace(partsFound[0], ' ').trim();
|
||||||
|
partsFound = [];
|
||||||
|
} else if (partsFoundCount > 1) {
|
||||||
|
handleError(partsFoundCount + ' nicknames found');
|
||||||
|
for (i = 0; i < partsFoundCount; i++) {
|
||||||
|
nameToParse = (' ' + nameToParse + ' ')
|
||||||
|
.replace(partsFound[i], ' ').trim();
|
||||||
|
partsFound[i] = partsFound[i].slice(2).slice(0, -2);
|
||||||
|
if (partsFound[i].slice(-1) === ',') {
|
||||||
|
partsFound[i] = partsFound[i].slice(0, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parsedName.nick = partsFound.join(', ');
|
||||||
|
partsFound = [];
|
||||||
|
}
|
||||||
|
if (!nameToParse.trim().length) {
|
||||||
|
parsedName = fixParsedNameCase(parsedName, fixCase);
|
||||||
|
return partToReturn === 'all' ? parsedName : parsedName[partToReturn];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split remaining nameToParse into parts, remove and store preceding commas
|
||||||
|
for (i = 0, n = nameToParse.split(' '), l = n.length; i < l; i++) {
|
||||||
|
part = n[i];
|
||||||
|
comma = null;
|
||||||
|
if (part.slice(-1) === ',') {
|
||||||
|
comma = ',';
|
||||||
|
part = part.slice(0, -1);
|
||||||
|
}
|
||||||
|
nameParts.push(part);
|
||||||
|
nameCommas.push(comma);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suffix: remove and store matching parts as suffixes
|
||||||
|
for (l = nameParts.length, i = l - 1; i > 0; i--) {
|
||||||
|
partToCheck = (nameParts[i].slice(-1) === '.' ?
|
||||||
|
nameParts[i].slice(0, -1).toLowerCase() : nameParts[i].toLowerCase());
|
||||||
|
if (
|
||||||
|
suffixList.indexOf(partToCheck) > -1 ||
|
||||||
|
suffixList.indexOf(partToCheck + '.') > -1
|
||||||
|
) {
|
||||||
|
partsFound = nameParts.splice(i, 1).concat(partsFound);
|
||||||
|
if (nameCommas[i] === ',') { // Keep comma, either before or after
|
||||||
|
nameCommas.splice(i + 1, 1);
|
||||||
|
} else {
|
||||||
|
nameCommas.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
partsFoundCount = partsFound.length;
|
||||||
|
if (partsFoundCount === 1) {
|
||||||
|
parsedName.suffix = partsFound[0];
|
||||||
|
partsFound = [];
|
||||||
|
} else if (partsFoundCount > 1) {
|
||||||
|
handleError(partsFoundCount + ' suffixes found');
|
||||||
|
parsedName.suffix = partsFound.join(', ');
|
||||||
|
partsFound = [];
|
||||||
|
}
|
||||||
|
if (!nameParts.length) {
|
||||||
|
parsedName = fixParsedNameCase(parsedName, fixCase);
|
||||||
|
return partToReturn === 'all' ? parsedName : parsedName[partToReturn];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title: remove and store matching parts as titles
|
||||||
|
for (l = nameParts.length, i = l - 1; i >= 0; i--) {
|
||||||
|
partToCheck = (nameParts[i].slice(-1) === '.' ?
|
||||||
|
nameParts[i].slice(0, -1).toLowerCase() : nameParts[i].toLowerCase());
|
||||||
|
if (
|
||||||
|
titleList.indexOf(partToCheck) > -1 ||
|
||||||
|
titleList.indexOf(partToCheck + '.') > -1
|
||||||
|
) {
|
||||||
|
partsFound = nameParts.splice(i, 1).concat(partsFound);
|
||||||
|
if (nameCommas[i] === ',') { // Keep comma, either before or after
|
||||||
|
nameCommas.splice(i + 1, 1);
|
||||||
|
} else {
|
||||||
|
nameCommas.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
partsFoundCount = partsFound.length;
|
||||||
|
if (partsFoundCount === 1) {
|
||||||
|
parsedName.title = partsFound[0];
|
||||||
|
partsFound = [];
|
||||||
|
} else if (partsFoundCount > 1) {
|
||||||
|
handleError(partsFoundCount + ' titles found');
|
||||||
|
parsedName.title = partsFound.join(', ');
|
||||||
|
partsFound = [];
|
||||||
|
}
|
||||||
|
if (!nameParts.length) {
|
||||||
|
parsedName = fixParsedNameCase(parsedName, fixCase);
|
||||||
|
return partToReturn === 'all' ? parsedName : parsedName[partToReturn];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join name prefixes to following names
|
||||||
|
if (nameParts.length > 1) {
|
||||||
|
for (i = nameParts.length - 2; i >= 0; i--) {
|
||||||
|
if (prefixList.indexOf(nameParts[i].toLowerCase()) > -1) {
|
||||||
|
nameParts[i] = nameParts[i] + ' ' + nameParts[i + 1];
|
||||||
|
nameParts.splice(i + 1, 1);
|
||||||
|
nameCommas.splice(i + 1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join conjunctions to surrounding names
|
||||||
|
if (nameParts.length > 2) {
|
||||||
|
for (i = nameParts.length - 3; i >= 0; i--) {
|
||||||
|
if (conjunctionList.indexOf(nameParts[i + 1].toLowerCase()) > -1) {
|
||||||
|
nameParts[i] = nameParts[i] + ' ' + nameParts[i + 1] + ' ' + nameParts[i + 2];
|
||||||
|
nameParts.splice(i + 1, 2);
|
||||||
|
nameCommas.splice(i + 1, 2);
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suffix: remove and store items after extra commas as suffixes
|
||||||
|
nameCommas.pop();
|
||||||
|
firstComma = nameCommas.indexOf(',');
|
||||||
|
remainingCommas = nameCommas.filter(function (v) { return v !== null; }).length;
|
||||||
|
if (firstComma > 1 || remainingCommas > 1) {
|
||||||
|
for (i = nameParts.length - 1; i >= 2; i--) {
|
||||||
|
if (nameCommas[i] === ',') {
|
||||||
|
partsFound = nameParts.splice(i, 1).concat(partsFound);
|
||||||
|
nameCommas.splice(i, 1);
|
||||||
|
remainingCommas--;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (partsFound.length) {
|
||||||
|
if (parsedName.suffix) {
|
||||||
|
partsFound = [parsedName.suffix].concat(partsFound);
|
||||||
|
}
|
||||||
|
parsedName.suffix = partsFound.join(', ');
|
||||||
|
partsFound = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last name: remove and store last name
|
||||||
|
if (remainingCommas > 0) {
|
||||||
|
if (remainingCommas > 1) {
|
||||||
|
handleError((remainingCommas - 1) + ' extra commas found');
|
||||||
|
}
|
||||||
|
// Remove and store all parts before first comma as last name
|
||||||
|
if (nameCommas.indexOf(',')) {
|
||||||
|
parsedName.last = nameParts.splice(0, nameCommas.indexOf(',')).join(' ');
|
||||||
|
nameCommas.splice(0, nameCommas.indexOf(','));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove and store last part as last name
|
||||||
|
parsedName.last = nameParts.pop();
|
||||||
|
}
|
||||||
|
if (!nameParts.length) {
|
||||||
|
parsedName = fixParsedNameCase(parsedName, fixCase);
|
||||||
|
return partToReturn === 'all' ? parsedName : parsedName[partToReturn];
|
||||||
|
}
|
||||||
|
|
||||||
|
// First name: remove and store first part as first name
|
||||||
|
parsedName.first = nameParts.shift();
|
||||||
|
if (!nameParts.length) {
|
||||||
|
parsedName = fixParsedNameCase(parsedName, fixCase);
|
||||||
|
return partToReturn === 'all' ? parsedName : parsedName[partToReturn];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middle name: store all remaining parts as middle name
|
||||||
|
if (nameParts.length > 2) {
|
||||||
|
handleError(nameParts.length + ' middle names');
|
||||||
|
}
|
||||||
|
parsedName.middle = nameParts.join(' ');
|
||||||
|
|
||||||
|
parsedName = fixParsedNameCase(parsedName, fixCase);
|
||||||
|
return partToReturn === 'all' ? parsedName : parsedName[partToReturn];
|
||||||
|
};
|
@ -1,6 +1,7 @@
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const dir = require('node-dir')
|
const dir = require('node-dir')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
const parseAuthors = require('./parseAuthors')
|
||||||
const { cleanString } = require('./index')
|
const { cleanString } = require('./index')
|
||||||
|
|
||||||
const AUDIOBOOK_PARTS_FORMATS = ['m4b', 'mp3']
|
const AUDIOBOOK_PARTS_FORMATS = ['m4b', 'mp3']
|
||||||
@ -74,6 +75,14 @@ async function getAllAudiobookFiles(abRootPath) {
|
|||||||
parts: [],
|
parts: [],
|
||||||
otherFiles: []
|
otherFiles: []
|
||||||
}
|
}
|
||||||
|
if (author) {
|
||||||
|
var parsedAuthors = parseAuthors(author)
|
||||||
|
if (parsedAuthors) {
|
||||||
|
var { authorLF, authorFL } = parsedAuthors
|
||||||
|
audiobooks[path].authorLF = authorLF || null
|
||||||
|
audiobooks[path].authorFL = authorFL || null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var filetype = getFileType(pathformat.ext)
|
var filetype = getFileType(pathformat.ext)
|
||||||
|
Loading…
Reference in New Issue
Block a user