Add global search, add reset all audiobooks

This commit is contained in:
advplyr 2021-08-21 16:23:35 -05:00
parent fb0a6f4ec2
commit f70e1beca1
18 changed files with 323 additions and 33 deletions

View File

@ -8,9 +8,9 @@
</a> </a>
<h1 class="text-2xl font-book">AudioBookshelf</h1> <h1 class="text-2xl font-book">AudioBookshelf</h1>
<controls-global-search />
<div class="flex-grow" /> <div class="flex-grow" />
<!-- <button class="px-4 py-2 bg-blue-500 rounded-xs" @click="scan">Scan</button> -->
<nuxt-link to="/config" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center"> <nuxt-link to="/config" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center">
<span class="material-icons">settings</span> <span class="material-icons">settings</span>
</nuxt-link> </nuxt-link>

View File

@ -0,0 +1,46 @@
<template>
<div class="flex h-full px-1 overflow-hidden">
<cards-book-cover :audiobook="audiobook" :width="40" />
<div class="flex-grow px-2 searchCardContent h-full">
<p class="truncate text-sm">{{ title }}</p>
<p class="text-xs text-gray-200 truncate">by {{ author }}</p>
</div>
</div>
</template>
<script>
export default {
props: {
audiobook: {
type: Object,
default: () => {}
}
},
data() {
return {}
},
computed: {
book() {
return this.audiobook ? this.audiobook.book || {} : {}
},
title() {
return this.book ? this.book.title : 'No Title'
},
author() {
return this.book ? this.book.author : 'Unknown'
}
},
methods: {},
mounted() {}
}
</script>
<style>
.searchCardContent {
width: calc(100% - 80px);
height: calc(40px * 1.5);
display: flex;
flex-direction: column;
justify-content: center;
}
</style>

View File

@ -1,14 +1,17 @@
<template> <template>
<div ref="wrapper" class="relative" v-click-outside="clickOutside"> <div ref="wrapper" class="relative" v-click-outside="clickOutside">
<button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-300 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu"> <button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
<span class="flex items-center justify-between"> <span class="flex items-center justify-between">
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span> <span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
</span> </span>
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none"> <span v-if="selected === 'all'" class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> <svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg> </svg>
</span> </span>
<div v-else class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer text-gray-400 hover:text-gray-300" @mousedown.stop @mouseup.stop @click.stop.prevent="clearSelected">
<span class="material-icons" style="font-size: 1.1rem">close</span>
</div>
</button> </button>
<div v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-80 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"> <div v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-80 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
@ -118,6 +121,11 @@ export default {
} }
}, },
methods: { methods: {
clearSelected() {
this.selected = 'all'
this.showMenu = false
this.$nextTick(() => this.$emit('change', 'all'))
},
snakeToNormal(kebab) { snakeToNormal(kebab) {
if (!kebab) { if (!kebab) {
return 'err' return 'err'

View File

@ -0,0 +1,112 @@
<template>
<div class="w-64 ml-8 relative">
<ui-text-input v-model="search" placeholder="Search.." @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
<div class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear">
<span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span>
<span v-else class="material-icons" style="font-size: 1.2rem">close</span>
</div>
<div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-10 -mt-px w-full bg-bg border border-black-200 shadow-lg max-h-80 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<li v-if="isTyping" class="py-2 px-2">
<p>Typing...</p>
</li>
<li v-else-if="isFetching" class="py-2 px-2">
<p>Fetching...</p>
</li>
<li v-else-if="!items.length" class="py-2 px-2">
<p>No Results</p>
</li>
<template v-else>
<template v-for="item in items">
<li :key="item.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400" role="option" @click="clickedOption(item)">
<template v-if="item.type === 'audiobook'">
<cards-audiobook-search-card :audiobook="item.data" />
</template>
</li>
</template>
</template>
</ul>
</div>
</div>
</template>
<script>
export default {
data() {
return {
showMenu: false,
isFocused: false,
focusTimeout: null,
isTyping: false,
isFetching: false,
search: null,
items: [],
searchTimeout: null,
lastSearch: null
}
},
computed: {
audiobooks() {
return this.$store.state.audiobooks.audiobooks
}
},
methods: {
focussed() {
this.isFocused = true
this.showMenu = true
},
blurred() {
this.isFocused = false
clearTimeout(this.focusTimeout)
this.focusTimeout = setTimeout(() => {
this.showMenu = false
}, 200)
},
async runSearch(value) {
this.lastSearch = value
if (!this.lastSearch) {
return
}
this.isFetching = true
var results = await this.$axios.$get(`/api/audiobooks?q=${value}`).catch((error) => {
console.error('Search error', error)
return []
})
this.isFetching = false
this.items = results.map((res) => {
return {
id: res.id,
data: res,
type: 'audiobook'
}
})
},
inputUpdate(val) {
clearTimeout(this.searchTimeout)
if (!val) {
this.lastSearch = ''
this.isTyping = false
return
}
this.isTyping = true
this.searchTimeout = setTimeout(() => {
this.isTyping = false
this.runSearch(val)
}, 1000)
},
clickedOption(option) {
if (option.type === 'audiobook') {
this.$router.push(`/audiobook/${option.data.id}`)
}
},
clickClear() {
if (this.search) {
this.search = null
this.items = []
this.showMenu = false
}
}
},
mounted() {}
}
</script>

View File

@ -1,15 +1,10 @@
<template> <template>
<div ref="wrapper" class="relative" v-click-outside="clickOutside"> <div ref="wrapper" class="relative" v-click-outside="clickOutside">
<button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-300 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu"> <button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
<span class="flex items-center justify-between"> <span class="flex items-center justify-between">
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span> <span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
<span class="material-icons text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span> <span class="material-icons text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span>
</span> </span>
<!-- <span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</span> -->
</button> </button>
<ul v-show="showMenu" class="absolute z-10 mt-1 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 v-show="showMenu" class="absolute z-10 mt-1 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">

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="w-full h-full"> <div class="w-full h-full overflow-hidden overflow-y-auto px-1">
<div class="flex"> <div class="flex">
<cards-book-cover :audiobook="audiobook" /> <cards-book-cover :audiobook="audiobook" />
<div class="flex-grow pl-6 pr-2"> <div class="flex-grow pl-6 pr-2">
@ -10,6 +10,23 @@
</div> </div>
</form> </form>
<div v-if="localCovers.length" class="mb-4 mt-6 border-t border-b border-primary">
<div class="flex items-center justify-center py-2">
<p>{{ localCovers.length }} local image(s)</p>
<div class="flex-grow" />
<ui-btn small @click="showLocalCovers = !showLocalCovers">{{ showLocalCovers ? 'Hide' : 'Show' }}</ui-btn>
</div>
<div v-if="showLocalCovers" class="flex items-center justify-center">
<template v-for="cover in localCovers">
<!-- <img :src="`/local/${cover.path}`" :key="cover.path" class="w-20 h-32 object-cover m-0.5" /> -->
<div :key="cover.path" class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.localPath === imageUrl ? 'border-yellow-300' : ''" @click="setCover(cover.localPath)">
<img :src="cover.localPath" class="h-24 object-cover" style="width: 60px" />
</div>
</template>
</div>
</div>
<form @submit.prevent="submitSearchForm"> <form @submit.prevent="submitSearchForm">
<div class="flex items-center justify-start -mx-1 py-2 mt-2"> <div class="flex items-center justify-start -mx-1 py-2 mt-2">
<div class="flex-grow px-1"> <div class="flex-grow px-1">
@ -23,7 +40,7 @@
</div> </div>
</div> </div>
</form> </form>
<div v-if="hasSearched" class="flex items-center flex-wrap justify-center max-h-72 overflow-y-scroll mt-2 max-w-full"> <div v-if="hasSearched" class="flex items-center flex-wrap justify-center max-h-60 overflow-y-scroll mt-2 max-w-full">
<p v-if="!coversFound.length">No Covers Found</p> <p v-if="!coversFound.length">No Covers Found</p>
<template v-for="cover in coversFound"> <template v-for="cover in coversFound">
<div :key="cover" class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === imageUrl ? 'border-yellow-300' : ''" @click="setCover(cover)"> <div :key="cover" class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === imageUrl ? 'border-yellow-300' : ''" @click="setCover(cover)">
@ -37,6 +54,8 @@
</template> </template>
<script> <script>
import Path from 'path'
export default { export default {
props: { props: {
processing: Boolean, processing: Boolean,
@ -51,7 +70,8 @@ export default {
searchAuthor: null, searchAuthor: null,
imageUrl: null, imageUrl: null,
coversFound: [], coversFound: [],
hasSearched: false hasSearched: false,
showLocalCovers: false
} }
}, },
watch: { watch: {
@ -73,10 +93,23 @@ export default {
}, },
book() { book() {
return this.audiobook ? this.audiobook.book || {} : {} return this.audiobook ? this.audiobook.book || {} : {}
},
otherFiles() {
return this.audiobook ? this.audiobook.otherFiles || [] : []
},
localCovers() {
return this.otherFiles
.filter((f) => f.filetype === 'image')
.map((file) => {
var _file = { ...file }
_file.localPath = Path.join('local', _file.path)
return _file
})
} }
}, },
methods: { methods: {
init() { init() {
this.showLocalCovers = false
if (this.coversFound.length && (this.searchTitle !== this.book.title || this.searchAuthor !== this.book.author)) { if (this.coversFound.length && (this.searchTitle !== this.book.title || this.searchAuthor !== this.book.author)) {
this.coversFound = [] this.coversFound = []
this.hasSearched = false this.hasSearched = false

View File

@ -15,7 +15,7 @@
<div v-show="processing" class="flex h-full items-center justify-center"> <div v-show="processing" class="flex h-full items-center justify-center">
<p>Loading...</p> <p>Loading...</p>
</div> </div>
<div v-show="!processing && !searchResults.length" class="flex h-full items-center justify-center"> <div v-show="!processing && !searchResults.length && hasSearched" class="flex h-full items-center justify-center">
<p>No Results</p> <p>No Results</p>
</div> </div>
<div v-show="!processing" class="w-full max-h-full overflow-y-auto overflow-x-hidden matchListWrapper"> <div v-show="!processing" class="w-full max-h-full overflow-y-auto overflow-x-hidden matchListWrapper">
@ -37,11 +37,13 @@ export default {
}, },
data() { data() {
return { return {
audiobookId: null,
searchTitle: null, searchTitle: null,
searchAuthor: null, searchAuthor: null,
lastSearch: null, lastSearch: null,
provider: 'best', provider: 'best',
searchResults: [] searchResults: [],
hasSearched: false
} }
}, },
watch: { watch: {
@ -90,15 +92,22 @@ export default {
}) })
this.searchResults = results this.searchResults = results
this.isProcessing = false this.isProcessing = false
this.hasSearched = true
}, },
init() { init() {
if (this.audiobook.id !== this.audiobookId) {
this.searchResults = []
this.hasSearched = false
this.audiobookId = this.audiobook.id
}
if (!this.audiobook.book || !this.audiobook.book.title) { if (!this.audiobook.book || !this.audiobook.book.title) {
this.searchTitle = null this.searchTitle = null
this.searchAuthor = null
return return
} }
this.searchTitle = this.audiobook.book.title this.searchTitle = this.audiobook.book.title
this.searchAuthor = this.audiobook.book.author || '' this.searchAuthor = this.audiobook.book.author || ''
this.runSearch()
}, },
async selectMatch(match) { async selectMatch(match) {
this.isProcessing = true this.isProcessing = true

View File

@ -1,6 +1,12 @@
<template> <template>
<button class="btn outline-none rounded-md shadow-md relative border border-gray-600" :type="type" :class="classList" @click="click"> <button class="btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="loading" :type="type" :class="classList" @click="click">
<slot /> <slot />
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
<!-- <span class="material-icons animate-spin">refresh</span> -->
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
</div>
</button> </button>
</template> </template>
@ -16,7 +22,8 @@ export default {
default: '' default: ''
}, },
paddingX: Number, paddingX: Number,
small: Boolean small: Boolean,
loading: Boolean
}, },
data() { data() {
return {} return {}
@ -24,6 +31,7 @@ export default {
computed: { computed: {
classList() { classList() {
var list = [] var list = []
if (this.loading) list.push('text-opacity-0')
list.push('text-white') list.push('text-white')
list.push(`bg-${this.color}`) list.push(`bg-${this.color}`)
if (this.small) { if (this.small) {
@ -61,7 +69,10 @@ button.btn::before {
background-color: rgba(255, 255, 255, 0); background-color: rgba(255, 255, 255, 0);
transition: all 0.1s ease-in-out; transition: all 0.1s ease-in-out;
} }
button.btn:hover::before { button.btn:hover:not(:disabled)::before {
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
} }
button:disabled::before {
background-color: rgba(0, 0, 0, 0.2);
}
</style> </style>

View File

@ -1,5 +1,5 @@
<template> <template>
<input v-model="inputValue" :type="type" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="py-2 px-3 rounded bg-primary text-gray-200 focus:border-gray-500 focus:outline-none" :class="transparent ? '' : 'border border-gray-600'" @change="change" /> <input v-model="inputValue" :type="type" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="py-2 px-3 rounded bg-primary text-gray-200 focus:border-gray-500 focus:outline-none" :class="transparent ? '' : 'border border-gray-600'" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" />
</template> </template>
<script> <script>
@ -29,8 +29,17 @@ export default {
} }
}, },
methods: { methods: {
focused() {
this.$emit('focus')
},
blurred() {
this.$emit('blur')
},
change(e) { change(e) {
this.$emit('change', e.target.value) this.$emit('change', e.target.value)
},
keyup(e) {
this.$emit('keyup', e)
} }
}, },
mounted() {} mounted() {}

View File

@ -136,11 +136,6 @@ export default {
this.socket.on('scan_start', this.scanStart) this.socket.on('scan_start', this.scanStart)
this.socket.on('scan_complete', this.scanComplete) this.socket.on('scan_complete', this.scanComplete)
this.socket.on('scan_progress', this.scanProgress) this.socket.on('scan_progress', this.scanProgress)
},
checkVersion() {
this.$axios.$get('http://github.com/advplyr/audiobookshelf/raw/master/package.json').then((data) => {
console.log('GOT DATA', data)
})
} }
}, },
beforeMount() { beforeMount() {
@ -150,7 +145,6 @@ export default {
}, },
mounted() { mounted() {
this.initializeSocket() this.initializeSocket()
this.checkVersion()
} }
} }
</script> </script>

View File

@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "0.9.61-beta", "version": "0.9.62-beta",
"description": "Audiobook manager and player", "description": "Audiobook manager and player",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -12,7 +12,15 @@
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn color="success" @click="scan">Scan</ui-btn> <ui-btn color="success" @click="scan">Scan</ui-btn>
</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">
<ui-btn color="error" small :padding-x="4" :loading="isResettingAudiobooks" @click="resetAudiobooks">Reset All Audiobooks</ui-btn>
</div>
<div class="h-0.5 bg-primary bg-opacity-50 w-full" />
<div class="flex items-center py-4"> <div class="flex items-center py-4">
<p class="font-mono">v{{ $config.version }}</p> <p class="font-mono">v{{ $config.version }}</p>
<div class="flex-grow" /> <div class="flex-grow" />
@ -32,7 +40,9 @@
<script> <script>
export default { export default {
data() { data() {
return {} return {
isResettingAudiobooks: false
}
}, },
computed: { computed: {
streamAudiobook() { streamAudiobook() {
@ -42,6 +52,22 @@ export default {
methods: { methods: {
scan() { scan() {
this.$root.socket.emit('scan') this.$root.socket.emit('scan')
},
resetAudiobooks() {
if (confirm('WARNING! This action will remove all audiobooks from the database including any updates or matches you have made. This does not do anything to your actual files. Shall we continue?')) {
this.isResettingAudiobooks = true
this.$axios
.$delete('/api/audiobooks')
.then(() => {
this.isResettingAudiobooks = false
this.$toast.success('Successfully reset audiobooks')
})
.catch((error) => {
console.error('failed to reset audiobooks', error)
this.isResettingAudiobooks = false
this.$toast.error('Failed to reset audiobooks - stop docker and manually remove appdata')
})
}
} }
}, },
mounted() {} mounted() {}

View File

@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "0.9.61-beta", "version": "0.9.62-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": {

View File

@ -17,8 +17,9 @@ class ApiController {
this.router.get('/find/covers', this.findCovers.bind(this)) this.router.get('/find/covers', this.findCovers.bind(this))
this.router.get('/find/:method', this.find.bind(this)) this.router.get('/find/:method', this.find.bind(this))
this.router.get('/audiobooks', this.getAudiobooks.bind(this)) this.router.get('/audiobooks', this.getAudiobooks.bind(this))
this.router.delete('/audiobooks', this.deleteAllAudiobooks.bind(this))
this.router.get('/audiobook/:id', this.getAudiobook.bind(this)) this.router.get('/audiobook/:id', this.getAudiobook.bind(this))
this.router.delete('/audiobook/:id', this.deleteAudiobook.bind(this)) this.router.delete('/audiobook/:id', this.deleteAudiobook.bind(this))
this.router.patch('/audiobook/:id/tracks', this.updateAudiobookTracks.bind(this)) this.router.patch('/audiobook/:id/tracks', this.updateAudiobookTracks.bind(this))
@ -57,9 +58,22 @@ class ApiController {
} }
getAudiobooks(req, res) { getAudiobooks(req, res) {
Logger.info('Get Audiobooks') var audiobooks = []
var audiobooksMinified = this.db.audiobooks.map(ab => ab.toJSONMinified()) if (req.query.q) {
res.json(audiobooksMinified) audiobooks = this.db.audiobooks.filter(ab => {
return ab.isSearchMatch(req.query.q)
}).map(ab => ab.toJSONMinified())
} else {
audiobooks = this.db.audiobooks.map(ab => ab.toJSONMinified())
}
res.json(audiobooks)
}
async deleteAllAudiobooks(req, res) {
Logger.info('Removing all Audiobooks')
var success = await this.db.recreateAudiobookDb()
if (success) res.sendStatus(200)
else res.sendStatus(500)
} }
getAudiobook(req, res) { getAudiobook(req, res) {

View File

@ -207,5 +207,9 @@ class Audiobook {
this.addTrack(file) this.addTrack(file)
}) })
} }
isSearchMatch(search) {
return this.book.isSearchMatch(search.toLowerCase().trim())
}
} }
module.exports = Audiobook module.exports = Audiobook

View File

@ -16,6 +16,10 @@ class Book {
} }
} }
get _title() { return this.title || '' }
get _author() { return this.author || '' }
get _series() { return this.series || '' }
construct(book) { construct(book) {
this.olid = book.olid this.olid = book.olid
this.title = book.title this.title = book.title
@ -81,5 +85,9 @@ class Book {
} }
return true return true
} }
isSearchMatch(search) {
return this._title.toLowerCase().includes(search) || this._author.toLowerCase().includes(search) || this._series.toLowerCase().includes(search)
}
} }
module.exports = Book module.exports = Book

View File

@ -28,6 +28,12 @@ class Db {
return this.settingsDb return this.settingsDb
} }
getEntityDbKey(entityName) {
if (entityName === 'user') return 'usersDb'
else if (entityName === 'audiobook') return 'audiobooksDb'
return 'settingsDb'
}
getEntityArrayKey(entityName) { getEntityArrayKey(entityName) {
if (entityName === 'user') return 'users' if (entityName === 'user') return 'users'
else if (entityName === 'audiobook') return 'audiobooks' else if (entityName === 'audiobook') return 'audiobooks'
@ -155,6 +161,18 @@ class Db {
}) })
} }
recreateAudiobookDb() {
return this.audiobooksDb.drop().then((results) => {
Logger.info(`[DB] Dropped audiobook db`, results)
this.audiobooksDb = new njodb.Database(this.AudiobooksPath)
this.audiobooks = []
return true
}).catch((error) => {
Logger.error(`[DB] Failed to drop audiobook db`, error)
return false
})
}
getGenres() { getGenres() {
var allGenres = [] var allGenres = []
this.db.audiobooks.forEach((audiobook) => { this.db.audiobooks.forEach((audiobook) => {

View File

@ -108,8 +108,11 @@ class Server {
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
const distPath = Path.join(global.appRoot, '/client/dist') const distPath = Path.join(global.appRoot, '/client/dist')
app.use(express.static(distPath)) app.use(express.static(distPath))
app.use('/local', express.static(this.AudiobookPath))
} else {
app.use(express.static(this.AudiobookPath))
} }
app.use(express.static(this.AudiobookPath))
app.use(express.static(this.MetadataPath)) app.use(express.static(this.MetadataPath))
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
app.use(express.json()) app.use(express.json())