mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-20 19:06:06 +01:00
Add Subtitle and Narrarator fields, add server settings object, scanner to parse out subtitles
This commit is contained in:
parent
af0365c81f
commit
a66a84bd2d
@ -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 : ''
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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)
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"description": "Audiobook manager and player",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
@ -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>
|
||||
|
@ -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() {
|
||||
|
@ -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}`
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
```
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
34
server/Db.js
34
server/Db.js
@ -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])
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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
|
39
server/objects/ServerSettings.js
Normal file
39
server/objects/ServerSettings.js
Normal 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
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user