Add Subtitle and Narrarator fields, add server settings object, scanner to parse out subtitles

This commit is contained in:
advplyr 2021-09-04 19:58:39 -05:00
parent af0365c81f
commit a66a84bd2d
20 changed files with 213 additions and 31 deletions

View File

@ -1,5 +1,5 @@
<template>
<modals-modal v-model="show" :width="800" :height="500" :processing="processing">
<modals-modal v-model="show" :width="800" :height="height" :processing="processing" :content-margin-top="75">
<template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
@ -79,6 +79,10 @@ export default {
this.$store.commit('setShowEditModal', val)
}
},
height() {
var maxHeightAllowed = window.innerHeight - 150
return Math.min(maxHeightAllowed, 650)
},
tabName() {
var _tab = this.tabs.find((t) => t.id === this.selectedTab)
return _tab ? _tab.component : ''

View File

@ -6,7 +6,7 @@
<span class="material-icons text-4xl">close</span>
</div>
<slot name="outer" />
<div ref="content" style="min-width: 400px; min-height: 200px" class="relative text-white" :style="{ height: modalHeight, width: modalWidth }" v-click-outside="clickBg">
<div ref="content" style="min-width: 400px; min-height: 200px" class="relative text-white" :style="{ height: modalHeight, width: modalWidth, marginTop: contentMarginTop + 'px' }" v-click-outside="clickBg">
<slot />
<div v-if="processing" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-black bg-opacity-60 rounded-lg flex items-center justify-center">
<ui-loading-indicator />
@ -31,6 +31,10 @@ export default {
height: {
type: [String, Number],
default: 'unset'
},
contentMarginTop: {
type: Number,
default: 50
}
},
data() {

View File

@ -12,6 +12,8 @@
<form @submit.prevent="submitForm">
<ui-text-input-with-label v-model="details.title" label="Title" />
<ui-text-input-with-label v-model="details.subtitle" label="Subtitle" class="mt-2" />
<div class="flex mt-2 -mx-1">
<div class="w-3/4 px-1">
<ui-text-input-with-label v-model="details.author" label="Author" />
@ -41,7 +43,13 @@
</div>
</div>
<div class="flex py-4">
<div class="flex mt-2 -mx-1">
<div class="w-1/2 px-1">
<ui-text-input-with-label v-model="details.narrarator" label="Narrarator" />
</div>
</div>
<div class="flex py-4 mt-2">
<ui-btn color="error" type="button" small @click.stop.prevent="deleteAudiobook">Remove</ui-btn>
<div class="flex-grow" />
<ui-btn type="submit">Submit</ui-btn>
@ -63,8 +71,10 @@ export default {
return {
details: {
title: null,
subtitle: null,
description: null,
author: null,
narrarator: null,
series: null,
volumeNumber: null,
publishYear: null,
@ -136,8 +146,10 @@ export default {
},
init() {
this.details.title = this.book.title
this.details.subtitle = this.book.subtitle
this.details.description = this.book.description
this.details.author = this.book.author
this.details.narrarator = this.book.narrarator
this.details.genres = this.book.genres || []
this.details.series = this.book.series
this.details.volumeNumber = this.book.volumeNumber

View File

@ -39,7 +39,7 @@ export default {
tooltip.style.top = top + 'px'
tooltip.style.left = left + 'px'
tooltip.style.zIndex = 100
tooltip.innerText = this.text
tooltip.innerHTML = this.text
this.tooltip = tooltip
},
showTooltip() {

View File

@ -56,6 +56,9 @@ export default {
this.$store.commit('user/setUser', payload.user)
this.$store.commit('user/setSettings', payload.user.settings)
}
if (payload.serverSettings) {
this.$store.commit('setServerSettings', payload.serverSettings)
}
},
streamOpen(stream) {
if (this.$refs.streamContainer) this.$refs.streamContainer.streamOpen(stream)

View File

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

View File

@ -9,6 +9,8 @@
<div class="flex-grow pl-4">
<ui-text-input-with-label v-model="audiobook.book.title" label="Title" />
<ui-text-input-with-label v-model="audiobook.book.subtitle" label="Subtitle" class="mt-2" />
<div class="flex mt-2 -mx-1">
<div class="w-3/4 px-1">
<ui-text-input-with-label v-model="audiobook.book.author" label="Author" />
@ -37,6 +39,12 @@
<ui-multi-select v-model="audiobook.tags" label="Tags" :items="tags" />
</div>
</div>
<div class="flex mt-2 -mx-1">
<div class="w-1/2 px-1">
<ui-text-input-with-label v-model="audiobook.book.narrarator" label="Narrarator" />
</div>
</div>
</div>
</div>
</template>

View File

@ -35,8 +35,16 @@
</div>
<div class="h-0.5 bg-primary bg-opacity-50 w-full" />
<div class="py-4 mb-8">
<p class="text-2xl">Scanner</p>
<div class="flex items-start py-2">
<p class="text-2xl">Scanner</p>
<div class="py-2">
<div class="flex items-center">
<ui-toggle-switch v-model="newServerSettings.scannerParseSubtitle" @input="updateScannerParseSubtitle" />
<ui-tooltip :text="parseSubtitleTooltip">
<p class="pl-4 text-lg">Parse Subtitles <span class="material-icons icon-text">info_outlined</span></p>
</ui-tooltip>
</div>
</div>
<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>
@ -84,10 +92,24 @@ export default {
isResettingAudiobooks: false,
users: [],
showAccountModal: false,
isDeletingUser: false
isDeletingUser: false,
newServerSettings: {}
}
},
watch: {
serverSettings(newVal, oldVal) {
if (newVal && !oldVal) {
this.newServerSettings = { ...this.serverSettings }
}
}
},
computed: {
parseSubtitleTooltip() {
return 'Extract subtitles from audiobook directory names.<br>Subtitle must be seperated by " - "<br>i.e. "Book Title - A Subtitle Here" has the subtitle "A Subtitle Here"'
},
serverSettings() {
return this.$store.state.serverSettings
},
streamAudiobook() {
return this.$store.state.streamAudiobook
},
@ -99,6 +121,19 @@ export default {
}
},
methods: {
updateScannerParseSubtitle(val) {
var payload = {
scannerParseSubtitle: val
}
this.$store
.dispatch('updateServerSettings', payload)
.then((success) => {
console.log('Updated Server Settings', success)
})
.catch((error) => {
console.error('Failed to update server settings', error)
})
},
setDeveloperMode() {
var value = !this.$store.state.developerMode
this.$store.commit('setDeveloperMode', value)
@ -186,6 +221,8 @@ export default {
this.$root.socket.on('user_added', this.addUpdateUser)
this.$root.socket.on('user_updated', this.addUpdateUser)
this.$root.socket.on('user_removed', this.userRemoved)
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
}
},
mounted() {

View File

@ -1,11 +1,10 @@
export default function ({ $axios, store }) {
$axios.onRequest(config => {
console.log('Making request to ' + config.url)
// console.log('Making request to ' + config.url)
if (config.url.startsWith('http:') || config.url.startsWith('https:')) {
return
}
var bearerToken = store.state.user.user ? store.state.user.user.token : null
// console.log('Bearer token', bearerToken)
if (bearerToken) {
config.headers.common['Authorization'] = `Bearer ${bearerToken}`
}

View File

@ -1,6 +1,7 @@
import Vue from 'vue'
export const state = () => ({
serverSettings: null,
streamAudiobook: null,
showEditModal: false,
selectedAudiobook: null,
@ -21,9 +22,29 @@ export const getters = {
getNumAudiobooksSelected: state => state.selectedAudiobooks.length
}
export const actions = {}
export const actions = {
updateServerSettings({ commit }, payload) {
var updatePayload = {
...payload
}
return this.$axios.$patch('/api/serverSettings', updatePayload).then((result) => {
if (result.success) {
commit('setServerSettings', result.settings)
return true
} else {
return false
}
}).catch((error) => {
console.error('Failed to update server settings', error)
return false
})
}
}
export const mutations = {
setServerSettings(state, settings) {
state.serverSettings = settings
},
setStreamAudiobook(state, audiobook) {
state.playOnLoad = true
state.streamAudiobook = audiobook

View File

@ -35,7 +35,6 @@ export const actions = {
return this.$axios.$patch('/api/user/settings', updatePayload).then((result) => {
if (result.success) {
commit('setSettings', result.settings)
console.log('Settings updated', result.settings)
return true
} else {
return false

View File

@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
"version": "1.0.1",
"version": "1.0.2",
"description": "Self-hosted audiobook server for managing and playing audiobooks.",
"main": "index.js",
"scripts": {

View File

@ -17,7 +17,12 @@ Android app is in beta, try it out on the [Google Play Store](https://play.googl
/Author/Series/Title/...
Title can start with the publish year like so:
/1989 - Awesome Book/...
/1989 - Book Title/...
(Optional Setting) Subtitle can be seperated to its own field:
/Book Title - With a Subtitle/...
/1989 - Book Title - With a Subtitle/...
will store "With a Subtitle" as the subtitle
```

View File

@ -41,6 +41,8 @@ class ApiController {
this.router.patch('/user/password', this.userChangePassword.bind(this))
this.router.patch('/user/settings', this.userUpdateSettings.bind(this))
this.router.patch('/serverSettings', this.updateServerSettings.bind(this))
this.router.post('/authorize', this.authorize.bind(this))
this.router.get('/genres', this.getGenres.bind(this))
@ -308,6 +310,21 @@ class ApiController {
})
}
async updateServerSettings(req, res) {
var settingsUpdate = req.body
if (!settingsUpdate || !isObject(settingsUpdate)) {
return res.sendStatus(500)
}
var madeUpdates = this.db.serverSettings.update(settingsUpdate)
if (madeUpdates) {
await this.db.updateEntity('settings', this.db.serverSettings)
}
return res.json({
success: true,
serverSettings: this.db.serverSettings
})
}
async download(req, res) {
var downloadId = req.params.id
Logger.info('Download Request', downloadId)

View File

@ -4,6 +4,7 @@ const jwt = require('jsonwebtoken')
const Logger = require('./Logger')
const Audiobook = require('./objects/Audiobook')
const User = require('./objects/User')
const ServerSettings = require('./objects/ServerSettings')
class Db {
constructor(CONFIG_PATH) {
@ -19,6 +20,8 @@ class Db {
this.users = []
this.audiobooks = []
this.settings = []
this.serverSettings = null
}
getEntityDb(entityName) {
@ -39,15 +42,6 @@ class Db {
return 'settings'
}
getDefaultSettings() {
return {
config: {
version: 1,
cardSize: 'md'
}
}
}
getDefaultUser(token) {
return new User({
id: 'root',
@ -71,6 +65,11 @@ class Db {
Logger.debug('Generated default token', token)
await this.insertUser(this.getDefaultUser(token))
}
if (!this.serverSettings) {
this.serverSettings = new ServerSettings()
await this.insertSettings(this.serverSettings)
}
}
async load() {
@ -83,11 +82,26 @@ class Db {
Logger.info(`Users Loaded ${this.users.length}`)
})
var p3 = this.settingsDb.select(() => true).then((results) => {
this.settings = results
if (results.data && results.data.length) {
this.settings = results.data
var serverSettings = this.settings.find(s => s.id === 'server-settings')
if (serverSettings) {
this.serverSettings = new ServerSettings(serverSettings)
}
}
})
await Promise.all([p1, p2, p3])
}
insertSettings(settings) {
return this.settingsDb.insert(settings).then((results) => {
Logger.debug(`[DB] Inserted ${results.inserted} settings`)
this.settings = this.settings.concat(settings)
}).catch((error) => {
Logger.error(`[DB] Insert settings Failed ${error}`)
})
}
insertAudiobook(audiobook) {
return this.insertAudiobooks([audiobook])
}

View File

@ -80,7 +80,7 @@ class Scanner {
}
const scanStart = Date.now()
var audiobookDataFound = await getAllAudiobookFiles(this.AudiobookPath)
var audiobookDataFound = await getAllAudiobookFiles(this.AudiobookPath, this.db.serverSettings)
// Set ino for each ab data as a string
audiobookDataFound = await this.setAudiobookDataInos(audiobookDataFound)

View File

@ -50,8 +50,8 @@ class Server {
get audiobooks() {
return this.db.audiobooks
}
get settings() {
return this.db.settings
get serverSettings() {
return this.db.serverSettings
}
emitter(ev, data) {
@ -239,7 +239,7 @@ class Server {
}
const initialPayload = {
settings: this.settings,
serverSettings: this.serverSettings.toJSON(),
isScanning: this.isScanning,
isInitialized: this.isInitialized,
audiobookPath: this.AudiobookPath,

View File

@ -6,9 +6,11 @@ class Book {
constructor(book = null) {
this.olid = null
this.title = null
this.subtitle = null
this.author = null
this.authorFL = null
this.authorLF = null
this.narrarator = null
this.series = null
this.volumeNumber = null
this.publishYear = null
@ -23,15 +25,19 @@ class Book {
}
get _title() { return this.title || '' }
get _subtitle() { return this.subtitle || '' }
get _narrarator() { return this.narrarator || '' }
get _author() { return this.author || '' }
get _series() { return this.series || '' }
construct(book) {
this.olid = book.olid
this.title = book.title
this.subtitle = book.subtitle || null
this.author = book.author
this.authorFL = book.authorFL || null
this.authorLF = book.authorLF || null
this.narrarator = book.narrarator || null
this.series = book.series
this.volumeNumber = book.volumeNumber || null
this.publishYear = book.publishYear
@ -45,9 +51,11 @@ class Book {
return {
olid: this.olid,
title: this.title,
subtitle: this.subtitle,
author: this.author,
authorFL: this.authorFL,
authorLF: this.authorLF,
narrarator: this.narrarator,
series: this.series,
volumeNumber: this.volumeNumber,
publishYear: this.publishYear,
@ -80,7 +88,9 @@ class Book {
setData(data) {
this.olid = data.olid || null
this.title = data.title || null
this.subtitle = data.subtitle || null
this.author = data.author || null
this.narrarator = data.narrarator || null
this.series = data.series || null
this.volumeNumber = data.volumeNumber || null
this.publishYear = data.publishYear || null
@ -151,7 +161,7 @@ class Book {
}
isSearchMatch(search) {
return this._title.toLowerCase().includes(search) || this._author.toLowerCase().includes(search) || this._series.toLowerCase().includes(search)
return this._title.toLowerCase().includes(search) || this._subtitle.toLowerCase().includes(search) || this._author.toLowerCase().includes(search) || this._series.toLowerCase().includes(search)
}
}
module.exports = Book

View File

@ -0,0 +1,39 @@
class ServerSettings {
constructor(settings) {
this.id = 'server-settings'
this.autoTagNew = false
this.newTagExpireDays = 15
this.scannerParseSubtitle = false
if (settings) {
this.construct(settings)
}
}
construct(settings) {
this.autoTagNew = settings.autoTagNew
this.newTagExpireDays = settings.newTagExpireDays
this.scannerParseSubtitle = settings.scannerParseSubtitle
}
toJSON() {
return {
id: this.id,
autoTagNew: this.autoTagNew,
newTagExpireDays: this.newTagExpireDays,
scannerParseSubtitle: this.scannerParseSubtitle
}
}
update(payload) {
var hasUpdates = false
for (const key in payload) {
if (this[key] !== payload[key]) {
this[key] = payload[key]
hasUpdates = true
}
}
return hasUpdates
}
}
module.exports = ServerSettings

View File

@ -30,7 +30,9 @@ function getFileType(ext) {
return 'unknown'
}
async function getAllAudiobookFiles(abRootPath) {
async function getAllAudiobookFiles(abRootPath, serverSettings = {}) {
var parseSubtitle = !!serverSettings.scannerParseSubtitle
var paths = await getPaths(abRootPath)
var audiobooks = {}
@ -53,6 +55,7 @@ async function getAllAudiobookFiles(abRootPath) {
var title = splitDir.shift()
var publishYear = null
var subtitle = null
// If Title is of format 1999 - Title, then use 1999 as publish year
var publishYearMatch = title.match(/^([0-9]{4}) - (.+)/)
@ -63,10 +66,17 @@ async function getAllAudiobookFiles(abRootPath) {
}
}
if (parseSubtitle && title.includes(' - ')) {
var splitOnSubtitle = title.split(' - ')
title = splitOnSubtitle.shift()
subtitle = splitOnSubtitle.join(' - ')
}
if (!audiobooks[path]) {
audiobooks[path] = {
author: author,
title: title,
author,
title,
subtitle,
series: cleanString(series),
publishYear: publishYear,
path: path,