logLevel as server setting, logger config page, re-scan audiobook option, fix embedded cover extraction, flac and mobi support, fix series bookshelf not wrapping

This commit is contained in:
Mark Cooper 2021-09-30 18:52:32 -05:00
parent dc18eb408e
commit d6cab8e591
28 changed files with 684 additions and 113 deletions

View file

@ -440,7 +440,7 @@ export default {
})
this.hlsInstance.on(Hls.Events.ERROR, (e, data) => {
console.error('[HLS] Error', data.type, data.details)
console.error('[HLS] Error', data.type, data.details, data)
if (data.details === Hls.ErrorDetails.BUFFER_STALLED_ERROR) {
console.error('[HLS] BUFFER STALLED ERROR')
}

View file

@ -102,7 +102,8 @@ export default {
return 16 * this.sizeMultiplier
},
bookWidth() {
return this.bookCoverWidth + this.paddingX * 2
var _width = this.bookCoverWidth + this.paddingX * 2
return this.showGroups ? _width * 1.6 : _width
},
isSelectionMode() {
return this.$store.getters['getNumAudiobooksSelected']
@ -161,6 +162,7 @@ export default {
setBookshelfEntities() {
this.wrapperClientWidth = this.$refs.wrapper.clientWidth
var width = Math.max(0, this.wrapperClientWidth - this.rowPaddingX * 2)
var booksPerRow = Math.floor(width / this.bookWidth)
var entities = this.entities

View file

@ -56,10 +56,14 @@
<div class="flex px-4">
<ui-btn v-if="userCanDelete" color="error" type="button" small @click.stop.prevent="deleteAudiobook">Remove</ui-btn>
<ui-tooltip text="(Root User Only) Save a NFO metadata file in your audiobooks directory" class="mx-4">
<ui-tooltip text="(Root User Only) Save a NFO metadata file in your audiobooks directory" direction="bottom" class="ml-4">
<ui-btn v-if="isRootUser" :loading="savingMetadata" color="bg" type="button" class="h-full" small @click.stop.prevent="saveMetadata">Save Metadata</ui-btn>
</ui-tooltip>
<ui-tooltip text="(Root User Only) Rescan audiobook including metadata" direction="bottom" class="ml-4">
<ui-btn v-if="isRootUser" :loading="rescanning" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">Re-Scan</ui-btn>
</ui-tooltip>
<div class="flex-grow" />
<ui-btn type="submit">Submit</ui-btn>
</div>
@ -93,7 +97,8 @@ export default {
newTags: [],
resettingProgress: false,
isScrollable: false,
savingMetadata: false
savingMetadata: false,
rescanning: false
}
},
watch: {
@ -136,6 +141,23 @@ export default {
}
},
methods: {
audiobookScanComplete(result) {
this.rescanning = false
if (!result) {
this.$toast.error(`Re-Scan Failed for "${this.title}"`)
} else if (result === 'UPDATED') {
this.$toast.success(`Re-Scan complete audiobook was updated`)
} else if (result === 'UPTODATE') {
this.$toast.success(`Re-Scan complete audiobook was up to date`)
} else if (result === 'REMOVED') {
this.$toast.error(`Re-Scan complete audiobook was removed`)
}
},
rescan() {
this.rescanning = true
this.$root.socket.once('audiobook_scan_complete', this.audiobookScanComplete)
this.$root.socket.emit('scan_audiobook', this.audiobookId)
},
saveMetadataComplete(result) {
this.savingMetadata = false
if (result.error) {

View file

@ -0,0 +1,72 @@
<template>
<div class="relative w-44" v-click-outside="clickOutside">
<p class="text-sm text-opacity-75 mb-1">{{ label }}</p>
<button type="button" class="relative w-full bg-fg border border-gray-500 rounded shadow-sm pl-3 pr-10 py-2 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="showMenu = !showMenu">
<span class="flex items-center">
<span class="block truncate">{{ selectedText }}</span>
</span>
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<span class="material-icons text-gray-100">chevron_down</span>
</span>
</button>
<transition name="menu">
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox" aria-activedescendant="listbox-option-3">
<template v-for="item in items">
<li :key="item.value" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="clickedOption(item.value)">
<div class="flex items-center">
<span class="font-normal ml-3 block truncate font-sans">{{ item.text }}</span>
</div>
</li>
</template>
</ul>
</transition>
</div>
</template>
<script>
export default {
props: {
value: [String, Number],
label: {
type: String,
default: ''
},
items: {
type: Array,
default: () => []
}
},
data() {
return {
showMenu: false
}
},
computed: {
selected: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
selectedItem() {
return this.items.find((i) => i.value === this.selected)
},
selectedText() {
return this.selectedItem ? this.selectedItem.text : ''
}
},
methods: {
clickOutside() {
this.showMenu = false
},
clickedOption(itemValue) {
this.selected = itemValue
this.showMenu = false
}
},
mounted() {}
}
</script>

View file

@ -127,21 +127,6 @@ export default {
this.$store.commit('setScanProgress', progress)
}
},
saveMetadataComplete(result) {
if (result.error) {
this.$toast.error(result.error)
} else if (result.audiobookId) {
var { savedPath } = result
if (!savedPath) {
this.$toast.error(`Failed to save metadata file (${result.audiobookId})`)
} else {
this.$toast.success(`Metadata file saved (${result.audiobookId})`)
}
} else {
var { success, failed } = result
this.$toast.success(`Metadata save complete\n${success} Succeeded\n${failed} Failed`)
}
},
userUpdated(user) {
if (this.$store.state.user.user.id === user.id) {
this.$store.commit('user/setUser', user)
@ -205,6 +190,9 @@ export default {
download.status = this.$constants.DownloadStatus.EXPIRED
this.$store.commit('downloads/addUpdateDownload', download)
},
logEvtReceived(payload) {
this.$store.commit('logs/logEvt', payload)
},
initializeSocket() {
this.socket = this.$nuxtSocket({
name: process.env.NODE_ENV === 'development' ? 'dev' : 'prod',
@ -245,7 +233,6 @@ export default {
this.socket.on('scan_start', this.scanStart)
this.socket.on('scan_complete', this.scanComplete)
this.socket.on('scan_progress', this.scanProgress)
// this.socket.on('save_metadata_complete', this.saveMetadataComplete)
// Download Listeners
this.socket.on('download_started', this.downloadStarted)
@ -253,6 +240,8 @@ export default {
this.socket.on('download_failed', this.downloadFailed)
this.socket.on('download_killed', this.downloadKilled)
this.socket.on('download_expired', this.downloadExpired)
this.socket.on('log', this.logEvtReceived)
},
showUpdateToast(versionData) {
var ignoreVersion = localStorage.getItem('ignoreVersion')

View file

@ -6,8 +6,13 @@ export default function (context) {
if (route.name === 'login' || from.name === 'login') return
if (route.name === 'config' || route.name === 'upload' || route.name === 'account' || route.name.startsWith('audiobook-id')) {
if (from.name !== route.name && from.name !== 'audiobook-id-edit' && from.name !== 'config' && from.name !== 'upload' && from.name !== 'account') {
if (!route.name) {
console.warn('No Route name', route)
return
}
if (route.name.startsWith('config') || route.name === 'upload' || route.name === 'account' || route.name.startsWith('audiobook-id')) {
if (from.name !== route.name && from.name !== 'audiobook-id-edit' && from.name !== 'config' && from.name !== 'config-log' && from.name !== 'upload' && from.name !== 'account') {
var _history = [...store.state.routeHistory]
if (!_history.length || _history[_history.length - 1] !== from.fullPath) {
_history.push(from.fullPath)

View file

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

View file

@ -34,8 +34,10 @@
</tr>
</table>
</div>
<div class="h-0.5 bg-primary bg-opacity-50 w-full" />
<div class="py-4 mb-8">
<div class="py-4 mb-4">
<p class="text-2xl">Scanner</p>
<div class="flex items-start py-2">
<div class="py-2">
@ -65,6 +67,8 @@
<div class="flex items-center py-4">
<ui-btn color="bg" small :padding-x="4" :loading="isResettingAudiobooks" @click="resetAudiobooks">Reset All Audiobooks</ui-btn>
<div class="flex-grow" />
<ui-btn to="/config/log">View Logger</ui-btn>
</div>
<div class="h-0.5 bg-primary bg-opacity-50 w-full" />
@ -134,6 +138,9 @@ export default {
var payload = {
scannerParseSubtitle: val
}
this.updateServerSettings(payload)
},
updateServerSettings(payload) {
this.$store
.dispatch('updateServerSettings', payload)
.then((success) => {
@ -175,6 +182,7 @@ export default {
.then(() => {
this.isResettingAudiobooks = false
this.$toast.success('Successfully reset audiobooks')
location.reload()
})
.catch((error) => {
console.error('failed to reset audiobooks', error)

136
client/pages/config/log.vue Normal file
View file

@ -0,0 +1,136 @@
<template>
<div id="page-wrapper" class="page p-6 overflow-y-auto" :class="streamAudiobook ? 'streaming' : ''">
<div class="w-full max-w-4xl mx-auto">
<div class="mb-4 flex items-center justify-between">
<p class="text-2xl">Logger</p>
<ui-dropdown v-model="newServerSettings.logLevel" label="Server Log Level" :items="logLevelItems" @input="logLevelUpdated" />
</div>
<div class="relative">
<div ref="container" class="relative w-full h-full bg-primary border-bg overflow-x-hidden overflow-y-auto text-red shadow-inner rounded-md" style="max-height: 550px; min-height: 550px">
<template v-for="(log, index) in logs">
<div :key="index" class="flex flex-nowrap px-2 py-1 items-start text-sm bg-opacity-10" :class="`bg-${logColors[log.level]}`">
<p class="text-gray-400 w-40 font-mono">{{ log.timestamp.split('.')[0].split('T').join(' ') }}</p>
<p class="font-semibold w-12 text-right" :class="`text-${logColors[log.level]}`">{{ log.levelName }}</p>
<p class="px-4 logmessage">{{ log.message }}</p>
</div>
</template>
</div>
<div v-if="!logs.length" class="absolute top-0 left-0 w-full h-full flex flex-col items-center justify-center text-center">
<p class="text-xl text-gray-200 mb-2">No Logs</p>
<p class="text-base text-gray-400">Log listening starts when you login</p>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
asyncData({ store, redirect }) {
if (!store.getters['user/getIsRoot']) {
redirect('/?error=unauthorized')
}
},
data() {
return {
newServerSettings: {},
logColors: ['yellow-200', 'gray-400', 'info', 'warning', 'error', 'red-800', 'blue-400'],
logLevels: [
{
value: 1,
text: 'Debug'
},
{
value: 2,
text: 'Info'
},
{
value: 3,
text: 'Warn'
}
]
}
},
watch: {
serverSettings(newVal, oldVal) {
if (newVal && !oldVal) {
this.newServerSettings = { ...this.serverSettings }
}
},
logs() {
this.updateScroll()
}
},
computed: {
logLevelItems() {
if (process.env.NODE_ENV === 'production') return this.logLevels
this.logLevels.unshift({ text: 'Trace', value: 0 })
return this.logLevels
},
logs() {
return this.$store.state.logs.logs.filter((log) => {
return log.level >= this.newServerSettings.logLevel
})
},
serverSettings() {
return this.$store.state.serverSettings
},
streamAudiobook() {
return this.$store.state.streamAudiobook
}
},
methods: {
updateScroll() {
if (this.$refs.container) {
this.$refs.container.scrollTop = this.$refs.container.scrollHeight - this.$refs.container.clientHeight
}
},
logLevelUpdated(val) {
var payload = {
logLevel: Number(val)
}
this.updateServerSettings(payload)
this.$store.dispatch('logs/setLogListener', this.newServerSettings.logLevel)
this.$nextTick(this.updateScroll)
},
updateServerSettings(payload) {
this.$store
.dispatch('updateServerSettings', payload)
.then((success) => {
console.log('Updated Server Settings', success)
})
.catch((error) => {
console.error('Failed to update server settings', error)
})
},
init(attempts = 0) {
if (!this.$root.socket) {
if (attempts > 10) {
return console.error('Failed to setup socket listeners')
}
setTimeout(() => {
this.init(++attempts)
}, 250)
return
}
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
}
},
updated() {
this.$nextTick(this.updateScroll)
},
mounted() {
this.init()
}
}
</script>
<style scoped>
.logmessage {
width: calc(100% - 208px);
}
</style>

31
client/store/logs.js Normal file
View file

@ -0,0 +1,31 @@
export const state = () => ({
isListening: false,
logs: []
})
export const getters = {
}
export const actions = {
setLogListener({ state, commit, dispatch }) {
dispatch('$nuxtSocket/emit', {
label: 'main',
evt: 'set_log_listener',
msg: 0
}, { root: true })
commit('setIsListening', true)
}
}
export const mutations = {
setIsListening(state, val) {
state.isListening = val
},
logEvt(state, payload) {
state.logs.push(payload)
if (state.logs.length > 500) {
state.logs = state.logs.slice(50)
}
}
}