Update:No longer creating initial root user and initial library, add init root user page, web app works with no libraries

This commit is contained in:
advplyr 2022-05-14 17:23:22 -05:00
parent 63a8e2433e
commit c962090c3a
20 changed files with 287 additions and 149 deletions

View File

@ -3,7 +3,6 @@ set -e
set -o pipefail set -o pipefail
FFMPEG_INSTALL_DIR="/usr/lib/audiobookshelf-ffmpeg/" FFMPEG_INSTALL_DIR="/usr/lib/audiobookshelf-ffmpeg/"
DEFAULT_AUDIOBOOK_PATH="/usr/share/audiobookshelf/audiobooks"
DEFAULT_DATA_PATH="/usr/share/audiobookshelf" DEFAULT_DATA_PATH="/usr/share/audiobookshelf"
DEFAULT_PORT=7331 DEFAULT_PORT=7331
DEFAULT_HOST="0.0.0.0" DEFAULT_HOST="0.0.0.0"
@ -54,14 +53,6 @@ setup_config_interactive() {
if should_build_config; then if should_build_config; then
echo "Okay, let's setup a new config." echo "Okay, let's setup a new config."
AUDIOBOOK_PATH=""
read -p "
Enter path for your audiobooks [Default: $DEFAULT_AUDIOBOOK_PATH]:" AUDIOBOOK_PATH
if [[ -z "$AUDIOBOOK_PATH" ]]; then
AUDIOBOOK_PATH="$DEFAULT_AUDIOBOOK_PATH"
fi
DATA_PATH="" DATA_PATH=""
read -p " read -p "
Enter path for data files, i.e. streams, downloads, database [Default: $DEFAULT_DATA_PATH]:" DATA_PATH Enter path for data files, i.e. streams, downloads, database [Default: $DEFAULT_DATA_PATH]:" DATA_PATH
@ -78,8 +69,7 @@ setup_config_interactive() {
PORT="$DEFAULT_PORT" PORT="$DEFAULT_PORT"
fi fi
config_text="AUDIOBOOK_PATH=$AUDIOBOOK_PATH config_text="METADATA_PATH=$DATA_PATH/metadata
METADATA_PATH=$DATA_PATH/metadata
CONFIG_PATH=$DATA_PATH/config CONFIG_PATH=$DATA_PATH/config
FFMPEG_PATH=/usr/lib/audiobookshelf-ffmpeg/ffmpeg FFMPEG_PATH=/usr/lib/audiobookshelf-ffmpeg/ffmpeg
FFPROBE_PATH=/usr/lib/audiobookshelf-ffmpeg/ffprobe FFPROBE_PATH=/usr/lib/audiobookshelf-ffmpeg/ffprobe
@ -102,8 +92,7 @@ setup_config() {
else else
echo "Creating default config." echo "Creating default config."
config_text="AUDIOBOOK_PATH=$DEFAULT_AUDIOBOOK_PATH config_text="METADATA_PATH=$DEFAULT_DATA_PATH/metadata
METADATA_PATH=$DEFAULT_DATA_PATH/metadata
CONFIG_PATH=$DEFAULT_DATA_PATH/config CONFIG_PATH=$DEFAULT_DATA_PATH/config
FFMPEG_PATH=/usr/lib/audiobookshelf-ffmpeg/ffmpeg FFMPEG_PATH=/usr/lib/audiobookshelf-ffmpeg/ffmpeg
FFPROBE_PATH=/usr/lib/audiobookshelf-ffmpeg/ffprobe FFPROBE_PATH=/usr/lib/audiobookshelf-ffmpeg/ffprobe

View File

@ -12,7 +12,7 @@
<ui-libraries-dropdown /> <ui-libraries-dropdown />
<controls-global-search class="hidden md:block" /> <controls-global-search v-if="currentLibrary" class="hidden md:block" />
<div class="flex-grow" /> <div class="flex-grow" />
<span v-if="showExperimentalFeatures" class="material-icons text-4xl text-warning pr-0 sm:pr-2 md:pr-4">logo_dev</span> <span v-if="showExperimentalFeatures" class="material-icons text-4xl text-warning pr-0 sm:pr-2 md:pr-4">logo_dev</span>
@ -24,11 +24,11 @@
<google-cast-launcher></google-cast-launcher> <google-cast-launcher></google-cast-launcher>
</div> </div>
<nuxt-link to="/config/stats" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1"> <nuxt-link v-if="currentLibrary" to="/config/stats" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
<span class="material-icons" aria-label="User Stats" role="button">equalizer</span> <span class="material-icons" aria-label="User Stats" role="button">equalizer</span>
</nuxt-link> </nuxt-link>
<nuxt-link v-if="userCanUpload" to="/upload" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1"> <nuxt-link v-if="userCanUpload && currentLibrary" to="/upload" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
<span class="material-icons" aria-label="Upload Media" role="button">upload</span> <span class="material-icons" aria-label="Upload Media" role="button">upload</span>
</nuxt-link> </nuxt-link>

View File

@ -25,6 +25,9 @@ export default {
return {} return {}
}, },
computed: { computed: {
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
},
userIsAdminOrUp() { userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp'] return this.$store.getters['user/getIsAdminOrUp']
}, },
@ -38,7 +41,7 @@ export default {
} }
] ]
} }
return [ const configRoutes = [
{ {
id: 'config', id: 'config',
title: 'Settings', title: 'Settings',
@ -63,18 +66,23 @@ export default {
id: 'config-log', id: 'config-log',
title: 'Log', title: 'Log',
path: '/config/log' path: '/config/log'
}, }
{ ]
if (this.currentLibraryId) {
configRoutes.push({
id: 'config-library-stats', id: 'config-library-stats',
title: 'Library Stats', title: 'Library Stats',
path: '/config/library-stats' path: '/config/library-stats'
}, })
{ configRoutes.push({
id: 'config-stats', id: 'config-stats',
title: 'Your Stats', title: 'Your Stats',
path: '/config/stats' path: '/config/stats'
} })
] }
return configRoutes
}, },
wrapperClass() { wrapperClass() {
var classes = [] var classes = []

View File

@ -95,7 +95,7 @@ export default {
settings: { settings: {
disableWatcher: false, disableWatcher: false,
skipMatchingMediaWithAsin: false, skipMatchingMediaWithAsin: false,
skipMatchingMediaWithIsbn: false, skipMatchingMediaWithIsbn: false
} }
} }
}, },
@ -193,6 +193,11 @@ export default {
this.processing = false this.processing = false
this.show = false this.show = false
this.$toast.success(`Library "${res.name}" created successfully`) this.$toast.success(`Library "${res.name}" created successfully`)
if (!this.$store.state.libraries.currentLibraryId) {
console.log('Setting initially library id', res.id)
// First library added
this.$store.dispatch('libraries/fetch', res.id)
}
}) })
.catch((error) => { .catch((error) => {
console.error(error) console.error(error)

View File

@ -6,18 +6,20 @@
<span class="material-icons" style="font-size: 1.4rem">add</span> <span class="material-icons" style="font-size: 1.4rem">add</span>
</div> </div>
</div> </div>
<draggable :list="libraryCopies" v-bind="dragOptions" class="list-group" draggable=".item" tag="div" @start="startDrag" @end="endDrag"> <draggable v-if="libraryCopies.length" :list="libraryCopies" v-bind="dragOptions" class="list-group" draggable=".item" tag="div" @start="startDrag" @end="endDrag">
<template v-for="library in libraryCopies"> <template v-for="library in libraryCopies">
<div :key="library.id" class="item"> <div :key="library.id" class="item">
<tables-library-item :library="library" :selected="currentLibraryId === library.id" :show-edit="true" :dragging="drag" @edit="editLibrary" @click="setLibrary" /> <tables-library-item :library="library" :selected="currentLibraryId === library.id" :show-edit="true" :dragging="drag" @edit="editLibrary" @click="setLibrary" />
</div> </div>
</template> </template>
</draggable> </draggable>
<modals-libraries-edit-modal v-model="showLibraryModal" :library="selectedLibrary" /> <div v-if="!libraries.length" class="pb-4">
<ui-btn @click="clickAddLibrary">Add your first library</ui-btn>
</div>
<p class="text-xs mt-4 text-gray-200">*<strong>Force Re-Scan</strong> will scan all files again like a fresh scan. Audio file ID3 tags, OPF files, and text files will be probed/parsed and used for book details.</p> <p v-if="libraries.length" class="text-xs mt-4 text-gray-200">*<strong>Force Re-Scan</strong> will scan all files again like a fresh scan. Audio file ID3 tags, OPF files, and text files will be probed/parsed and used for book details.</p>
<p class="text-xs mt-4 text-gray-200">**<strong>Match Books</strong> will attempt to match books in library with a book from the selected search provider and fill in empty details and cover art. Does not overwrite details.</p> <p v-if="libraries.length && libraries.some((li) => li.mediaType === 'book')" class="text-xs mt-4 text-gray-200">**<strong>Match Books</strong> will attempt to match books in library with a book from the selected search provider and fill in empty details and cover art. Does not overwrite details.</p>
</div> </div>
</template> </template>
@ -32,8 +34,6 @@ export default {
return { return {
libraryCopies: [], libraryCopies: [],
currentOrder: [], currentOrder: [],
showLibraryModal: false,
selectedLibrary: null,
drag: false, drag: false,
dragOptions: { dragOptions: {
animation: 200, animation: 200,
@ -97,12 +97,10 @@ export default {
this.$router.push(`/library/${library.id}`) this.$router.push(`/library/${library.id}`)
}, },
clickAddLibrary() { clickAddLibrary() {
this.selectedLibrary = null this.$emit('showLibraryModal', null)
this.showLibraryModal = true
}, },
editLibrary(library) { editLibrary(library) {
this.selectedLibrary = library this.$emit('showLibraryModal', library)
this.showLibraryModal = true
}, },
init() { init() {
this.libraryCopies = this.libraries.map((lib) => { this.libraryCopies = this.libraries.map((lib) => {

View File

@ -1,9 +1,12 @@
<template> <template>
<div class="relative"> <div ref="wrapper" class="relative">
<input ref="input" v-model="inputValue" :type="type" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="rounded bg-primary text-gray-200 focus:border-gray-300 focus:bg-bg focus:outline-none border border-gray-600 h-full w-full" :class="classList" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" /> <input ref="input" v-model="inputValue" :type="actualType" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="rounded bg-primary text-gray-200 focus:border-gray-300 focus:bg-bg focus:outline-none border border-gray-600 h-full w-full" :class="classList" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" />
<div v-if="clearable && inputValue" class="absolute top-0 right-0 h-full px-2 flex items-center justify-center"> <div v-if="clearable && inputValue" class="absolute top-0 right-0 h-full px-2 flex items-center justify-center">
<span class="material-icons text-gray-300 cursor-pointer" style="font-size: 1.1rem" @click.stop.prevent="clear">close</span> <span class="material-icons text-gray-300 cursor-pointer" style="font-size: 1.1rem" @click.stop.prevent="clear">close</span>
</div> </div>
<div v-if="type === 'password' && isHovering" class="absolute top-0 right-0 h-full px-4 flex items-center justify-center">
<span class="material-icons-outlined text-gray-400 cursor-pointer text-lg" @click.stop.prevent="showPassword = !showPassword">{{ !showPassword ? 'visibility' : 'visibility_off' }}</span>
</div>
</div> </div>
</template> </template>
@ -31,7 +34,10 @@ export default {
clearable: Boolean clearable: Boolean
}, },
data() { data() {
return {} return {
showPassword: false,
isHovering: false
}
}, },
computed: { computed: {
inputValue: { inputValue: {
@ -49,6 +55,10 @@ export default {
if (this.noSpinner) _list.push('no-spinner') if (this.noSpinner) _list.push('no-spinner')
if (this.textCenter) _list.push('text-center') if (this.textCenter) _list.push('text-center')
return _list.join(' ') return _list.join(' ')
},
actualType() {
if (this.type === 'password' && this.showPassword) return 'text'
return this.type
} }
}, },
methods: { methods: {
@ -69,9 +79,20 @@ export default {
}, },
blur() { blur() {
if (this.$refs.input) this.$refs.input.blur() if (this.$refs.input) this.$refs.input.blur()
},
mouseover() {
this.isHovering = true
},
mouseleave() {
this.isHovering = false
} }
}, },
mounted() {} mounted() {
if (this.type === 'password' && this.$refs.wrapper) {
this.$refs.wrapper.addEventListener('mouseover', this.mouseover)
this.$refs.wrapper.addEventListener('mouseleave', this.mouseleave)
}
}
} }
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="w-full"> <div class="w-full">
<p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''"> <p class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }">
{{ label }}<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em> {{ label }}<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em>
</p> </p>
<ui-text-input ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" class="w-full" @blur="inputBlurred" /> <ui-text-input ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" class="w-full" @blur="inputBlurred" />

View File

@ -51,7 +51,7 @@ export default {
}, },
isShowingSideRail() { isShowingSideRail() {
if (!this.$route.name) return false if (!this.$route.name) return false
return !this.$route.name.startsWith('config') return !this.$route.name.startsWith('config') && this.$store.state.libraries.currentLibraryId
}, },
appContentMarginLeft() { appContentMarginLeft() {
return this.isShowingSideRail ? 80 : 0 return this.isShowingSideRail ? 80 : 0
@ -173,6 +173,7 @@ export default {
this.$store.commit('libraries/addUpdate', library) this.$store.commit('libraries/addUpdate', library)
}, },
async libraryRemoved(library) { async libraryRemoved(library) {
console.log('Library removed', library)
this.$store.commit('libraries/remove', library) this.$store.commit('libraries/remove', library)
// When removed currently selected library then set next accessible library // When removed currently selected library then set next accessible library
@ -191,7 +192,8 @@ export default {
this.$router.push(`/library/${nextLibrary.id}`) this.$router.push(`/library/${nextLibrary.id}`)
} }
} else { } else {
console.error('User has no accessible libraries') console.error('User has no more accessible libraries')
this.$store.commit('libraries/setCurrentLibrary', null)
} }
} }
}, },

View File

@ -1,16 +1,26 @@
<template> <template>
<div> <div>
<tables-library-libraries-table /> <tables-library-libraries-table @showLibraryModal="setShowLibraryModal" />
<modals-libraries-edit-modal v-model="showLibraryModal" :library="selectedLibrary" />
</div> </div>
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return {} return {
showLibraryModal: false,
selectedLibrary: null
}
}, },
computed: {}, computed: {},
methods: {}, methods: {
setShowLibraryModal(selectedLibrary) {
this.selectedLibrary = selectedLibrary
this.showLibraryModal = true
}
},
mounted() {} mounted() {}
} }
</script> </script>

View File

@ -67,6 +67,12 @@
<script> <script>
export default { export default {
asyncData({ redirect, store }) {
if (!store.state.libraries.currentLibraryId) {
return redirect('/config')
}
return {}
},
data() { data() {
return { return {
libraryStats: null libraryStats: null

View File

@ -5,6 +5,9 @@
<script> <script>
export default { export default {
asyncData({ redirect, store }) { asyncData({ redirect, store }) {
if (!store.state.libraries.currentLibraryId) {
return redirect('/oops?message=No libraries')
}
redirect(`/library/${store.state.libraries.currentLibraryId}`) redirect(`/library/${store.state.libraries.currentLibraryId}`)
}, },
data() { data() {

View File

@ -1,7 +1,29 @@
<template> <template>
<div class="w-full h-screen bg-bg"> <div class="w-full h-screen bg-bg">
<div class="w-full flex h-1/2 items-center justify-center"> <div class="w-full flex h-full items-center justify-center">
<div class="w-full max-w-md border border-opacity-0 rounded-xl px-8 pb-8 pt-4"> <div v-if="criticalError" class="w-full max-w-md rounded border border-error border-opacity-25 bg-error bg-opacity-10 p-4">
<p class="text-center text-lg font-semibold">Server could not be reached</p>
</div>
<div v-else-if="showInitScreen" class="w-full max-w-lg px-4 md:px-8 pb-8 pt-4">
<p class="text-3xl text-white text-center mb-4">Initial Server Setup</p>
<div class="w-full h-px bg-white bg-opacity-10 my-4" />
<form @submit.prevent="submitServerSetup">
<p class="text-lg font-semibold mb-2 pl-1 text-center">Create Root User</p>
<ui-text-input-with-label v-model="newRoot.username" label="Username" :disabled="processing" class="w-full mb-3 text-sm" />
<ui-text-input-with-label v-model="newRoot.password" label="Password" type="password" :disabled="processing" class="w-full mb-3 text-sm" />
<ui-text-input-with-label v-model="confirmPassword" label="Confirm Password" type="password" :disabled="processing" class="w-full mb-3 text-sm" />
<p class="text-lg font-semibold mt-6 mb-2 pl-1 text-center">Directory Paths</p>
<ui-text-input-with-label v-model="ConfigPath" label="Config Path" disabled class="w-full mb-3 text-sm" />
<ui-text-input-with-label v-model="MetadataPath" label="Metadata Path" disabled class="w-full mb-3 text-sm" />
<div class="w-full flex justify-end py-3">
<ui-btn type="submit" :disabled="processing" color="primary" class="leading-none">{{ processing ? 'Initializing...' : 'Submit' }}</ui-btn>
</div>
</form>
</div>
<div v-else-if="isInit" class="w-full max-w-md px-8 pb-8 pt-4 -mt-40">
<p class="text-3xl text-white text-center mb-4">Login</p> <p class="text-3xl text-white text-center mb-4">Login</p>
<div class="w-full h-px bg-white bg-opacity-10 my-4" /> <div class="w-full h-px bg-white bg-opacity-10 my-4" />
<p v-if="error" class="text-error text-center py-2">{{ error }}</p> <p v-if="error" class="text-error text-center py-2">{{ error }}</p>
@ -11,8 +33,8 @@
<label class="text-xs text-gray-300 uppercase">Password</label> <label class="text-xs text-gray-300 uppercase">Password</label>
<ui-text-input v-model="password" type="password" :disabled="processing" class="w-full mb-3" /> <ui-text-input v-model="password" type="password" :disabled="processing" class="w-full mb-3" />
<div class="w-full flex justify-end"> <div class="w-full flex justify-end py-3">
<button type="submit" :disabled="processing" class="bg-blue-600 hover:bg-blue-800 px-8 py-1 mt-3 rounded-md text-white text-center transition duration-300 ease-in-out focus:outline-none">{{ processing ? 'Checking...' : 'Submit' }}</button> <ui-btn type="submit" :disabled="processing" color="primary" class="leading-none">{{ processing ? 'Checking...' : 'Submit' }}</ui-btn>
</div> </div>
</form> </form>
</div> </div>
@ -26,15 +48,33 @@ export default {
data() { data() {
return { return {
error: null, error: null,
criticalError: null,
processing: false, processing: false,
username: '', username: '',
password: null password: null,
showInitScreen: false,
isInit: false,
newRoot: {
username: 'root',
password: ''
},
confirmPassword: '',
ConfigPath: '',
MetadataPath: ''
} }
}, },
watch: { watch: {
user(newVal) { user(newVal) {
if (newVal) { if (newVal) {
if (this.$route.query.redirect) { if (!this.$store.state.libraries.currentLibraryId) {
// No libraries available to this user
if (this.$store.getters['user/getIsRoot']) {
// If root user go to config/libraries
this.$router.replace('/config/libraries')
} else {
this.$router.replace('/oops?message=No libraries available')
}
} else if (this.$route.query.redirect) {
this.$router.replace(this.$route.query.redirect) this.$router.replace(this.$route.query.redirect)
} else { } else {
this.$router.replace(`/library/${this.$store.state.libraries.currentLibraryId}`) this.$router.replace(`/library/${this.$store.state.libraries.currentLibraryId}`)
@ -48,6 +88,42 @@ export default {
} }
}, },
methods: { methods: {
async submitServerSetup() {
if (!this.newRoot.username || !this.newRoot.username.trim()) {
this.$toast.error('Must enter a root username')
return
}
if (this.newRoot.password !== this.confirmPassword) {
this.$toast.error('Password mismatch')
return
}
if (!this.newRoot.password) {
if (!confirm('Are you sure you want to create the root user with no password?')) {
return
}
}
this.processing = true
const payload = {
newRoot: { ...this.newRoot }
}
var success = await this.$axios
.$post('/init', payload)
.then(() => true)
.catch((error) => {
console.error('Failed', error.response)
const errorMsg = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error'
this.$toast.error(errorMsg)
return false
})
if (!success) {
this.processing = false
return
}
location.reload()
},
setUser({ user, userDefaultLibraryId, serverSettings }) { setUser({ user, userDefaultLibraryId, serverSettings }) {
this.$store.commit('setServerSettings', serverSettings) this.$store.commit('setServerSettings', serverSettings)
@ -81,32 +157,54 @@ export default {
this.processing = false this.processing = false
}, },
checkAuth() { checkAuth() {
if (localStorage.getItem('token')) { var token = localStorage.getItem('token')
var token = localStorage.getItem('token') if (!token) return false
if (token) { this.processing = true
this.processing = true
this.$axios return this.$axios
.$post('/api/authorize', null, { .$post('/api/authorize', null, {
headers: { headers: {
Authorization: `Bearer ${token}` Authorization: `Bearer ${token}`
} }
}) })
.then((res) => { .then((res) => {
this.setUser(res) this.setUser(res)
this.processing = false this.processing = false
}) return true
.catch((error) => { })
console.error('Authorize error', error) .catch((error) => {
this.processing = false console.error('Authorize error', error)
}) this.processing = false
} return false
} })
},
checkStatus() {
this.processing = true
this.$axios
.$get('/status')
.then((res) => {
this.processing = false
this.isInit = res.isInit
this.showInitScreen = !res.isInit
if (this.showInitScreen) {
this.ConfigPath = res.ConfigPath || ''
this.MetadataPath = res.MetadataPath || ''
}
})
.catch((error) => {
console.error('Status check failed', error)
this.processing = false
this.criticalError = 'Status check failed'
})
} }
}, },
mounted() { async mounted() {
this.checkAuth() if (localStorage.getItem('token')) {
var userfound = await this.checkAuth()
if (userfound) return // if valid user no need to check status
}
this.checkStatus()
} }
} }
</script> </script>

View File

@ -2,7 +2,7 @@ export const state = () => ({
libraries: [], libraries: [],
lastLoad: 0, lastLoad: 0,
listeners: [], listeners: [],
currentLibraryId: 'main', currentLibraryId: null,
folders: [], folders: [],
issues: 0, issues: 0,
folderLastUpdate: 0, folderLastUpdate: 0,

View File

@ -1,4 +1,4 @@
if(process.env.TOKEN_SECRET == null) process.env.TOKEN_SECRET = '09f26e402586e2faa8da4c98a35f1b20d6b033c6097befa8be3486a829587fe2f90a832bd3ff9d42710a4da095a2ce285b009f0c3730cd9b8e1af3eb84df6611' if (process.env.TOKEN_SECRET == null) process.env.TOKEN_SECRET = '09f26e402586e2faa8da4c98a35f1b20d6b033c6097befa8be3486a829587fe2f90a832bd3ff9d42710a4da095a2ce285b009f0c3730cd9b8e1af3eb84df6611'
const server = require('./server/Server') const server = require('./server/Server')
global.appRoot = __dirname global.appRoot = __dirname
@ -9,20 +9,20 @@ if (isDev) {
process.env.PORT = devEnv.Port process.env.PORT = devEnv.Port
process.env.CONFIG_PATH = devEnv.ConfigPath process.env.CONFIG_PATH = devEnv.ConfigPath
process.env.METADATA_PATH = devEnv.MetadataPath process.env.METADATA_PATH = devEnv.MetadataPath
process.env.AUDIOBOOK_PATH = devEnv.AudiobookPath
process.env.FFMPEG_PATH = devEnv.FFmpegPath process.env.FFMPEG_PATH = devEnv.FFmpegPath
process.env.FFPROBE_PATH = devEnv.FFProbePath process.env.FFPROBE_PATH = devEnv.FFProbePath
process.env.SOURCE = 'local'
} }
const PORT = process.env.PORT || 80 const PORT = process.env.PORT || 80
const HOST = process.env.HOST || '0.0.0.0' const HOST = process.env.HOST || '0.0.0.0'
const CONFIG_PATH = process.env.CONFIG_PATH || '/config' const CONFIG_PATH = process.env.CONFIG_PATH || '/config'
const AUDIOBOOK_PATH = process.env.AUDIOBOOK_PATH || '/audiobooks'
const METADATA_PATH = process.env.METADATA_PATH || '/metadata' const METADATA_PATH = process.env.METADATA_PATH || '/metadata'
const UID = process.env.AUDIOBOOKSHELF_UID || 99 const UID = process.env.AUDIOBOOKSHELF_UID || 99
const GID = process.env.AUDIOBOOKSHELF_GID || 100 const GID = process.env.AUDIOBOOKSHELF_GID || 100
const SOURCE = process.env.SOURCE || 'docker'
console.log('Config', CONFIG_PATH, METADATA_PATH, AUDIOBOOK_PATH) console.log('Config', CONFIG_PATH, METADATA_PATH)
const Server = new server(PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, AUDIOBOOK_PATH) const Server = new server(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH)
Server.start() Server.start()

13
prod.js
View File

@ -1,16 +1,16 @@
const optionDefinitions = [ const optionDefinitions = [
{ name: 'config', alias: 'c', type: String }, { name: 'config', alias: 'c', type: String },
{ name: 'audiobooks', alias: 'a', type: String },
{ name: 'metadata', alias: 'm', type: String }, { name: 'metadata', alias: 'm', type: String },
{ name: 'port', alias: 'p', type: String }, { name: 'port', alias: 'p', type: String },
{ name: 'host', alias: 'h', type: String } { name: 'host', alias: 'h', type: String },
{ name: 'source', alias: 's', type: String }
] ]
const commandLineArgs = require('command-line-args') const commandLineArgs = require('command-line-args')
const options = commandLineArgs(optionDefinitions) const options = commandLineArgs(optionDefinitions)
const Path = require('path') const Path = require('path')
if(process.env.TOKEN_SECRET == null) process.env.TOKEN_SECRET = '09f26e402586e2faa8da4c98a35f1b20d6b033c6097befa8be3486a829587fe2f90a832bd3ff9d42710a4da095a2ce285b009f0c3730cd9b8e1af3eb84df6611' if (process.env.TOKEN_SECRET == null) process.env.TOKEN_SECRET = '09f26e402586e2faa8da4c98a35f1b20d6b033c6097befa8be3486a829587fe2f90a832bd3ff9d42710a4da095a2ce285b009f0c3730cd9b8e1af3eb84df6611'
process.env.NODE_ENV = 'production' process.env.NODE_ENV = 'production'
const server = require('./server/Server') const server = require('./server/Server')
@ -18,18 +18,17 @@ const server = require('./server/Server')
global.appRoot = __dirname global.appRoot = __dirname
var inputConfig = options.config ? Path.resolve(options.config) : null var inputConfig = options.config ? Path.resolve(options.config) : null
var inputAudiobook = options.audiobooks ? Path.resolve(options.audiobooks) : null
var inputMetadata = options.metadata ? Path.resolve(options.metadata) : null var inputMetadata = options.metadata ? Path.resolve(options.metadata) : null
const PORT = options.port || process.env.PORT || 3333 const PORT = options.port || process.env.PORT || 3333
const HOST = options.host || process.env.HOST || "0.0.0.0" const HOST = options.host || process.env.HOST || "0.0.0.0"
const CONFIG_PATH = inputConfig || process.env.CONFIG_PATH || Path.resolve('config') const CONFIG_PATH = inputConfig || process.env.CONFIG_PATH || Path.resolve('config')
const AUDIOBOOK_PATH = inputAudiobook || process.env.AUDIOBOOK_PATH || Path.resolve('audiobooks')
const METADATA_PATH = inputMetadata || process.env.METADATA_PATH || Path.resolve('metadata') const METADATA_PATH = inputMetadata || process.env.METADATA_PATH || Path.resolve('metadata')
const UID = 99 const UID = 99
const GID = 100 const GID = 100
const SOURCE = options.source || 'debian'
console.log(process.env.NODE_ENV, 'Config', CONFIG_PATH, METADATA_PATH, AUDIOBOOK_PATH) console.log(process.env.NODE_ENV, 'Config', CONFIG_PATH, METADATA_PATH)
const Server = new server(PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, AUDIOBOOK_PATH) const Server = new server(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH)
Server.start() Server.start()

View File

@ -17,14 +17,6 @@ class Auth {
return this.db.users return this.db.users
} }
init() {
var root = this.users.find(u => u.type === 'root')
if (!root) {
Logger.fatal('No Root User', this.users)
throw new Error('No Root User')
}
}
cors(req, res, next) { cors(req, res, next) {
res.header('Access-Control-Allow-Origin', '*') res.header('Access-Control-Allow-Origin', '*')
res.header("Access-Control-Allow-Methods", 'GET, POST, PATCH, PUT, DELETE, OPTIONS') res.header("Access-Control-Allow-Methods", 'GET, POST, PATCH, PUT, DELETE, OPTIONS')

View File

@ -46,6 +46,10 @@ class Db {
this.previousVersion = null this.previousVersion = null
} }
get hasRootUser() {
return this.users.some(u => u.id === 'root')
}
getEntityDb(entityName) { getEntityDb(entityName) {
if (entityName === 'user') return this.usersDb if (entityName === 'user') return this.usersDb
else if (entityName === 'session') return this.sessionsDb else if (entityName === 'session') return this.sessionsDb
@ -70,33 +74,6 @@ class Db {
return null return null
} }
getDefaultUser(token) {
return new User({
id: 'root',
type: 'root',
username: 'root',
pash: '',
stream: null,
token,
isActive: true,
createdAt: Date.now()
})
}
getDefaultLibrary() {
var defaultLibrary = new Library()
defaultLibrary.setData({
id: 'main',
name: 'Main',
folder: { // Generates default folder
id: 'audiobooks',
fullPath: global.AudiobookPath,
libraryId: 'main'
}
})
return defaultLibrary
}
reinit() { reinit() {
this.libraryItemsDb = new njodb.Database(this.LibraryItemsPath) this.libraryItemsDb = new njodb.Database(this.LibraryItemsPath)
this.usersDb = new njodb.Database(this.UsersPath) this.usersDb = new njodb.Database(this.UsersPath)
@ -123,23 +100,36 @@ class Db {
}) })
} }
createRootUser(username, pash, token) {
const newRoot = new User({
id: 'root',
type: 'root',
username,
pash,
token,
isActive: true,
createdAt: Date.now()
})
return this.insertEntity('user', newRoot)
}
async init() { async init() {
await this.load() await this.load()
// Insert Defaults // Insert Defaults
var rootUser = this.users.find(u => u.type === 'root') // var rootUser = this.users.find(u => u.type === 'root')
if (!rootUser) { // if (!rootUser) {
var token = await jwt.sign({ userId: 'root' }, process.env.TOKEN_SECRET) // var token = await jwt.sign({ userId: 'root' }, process.env.TOKEN_SECRET)
Logger.debug('Generated default token', token) // Logger.debug('Generated default token', token)
Logger.info('[Db] Root user created') // Logger.info('[Db] Root user created')
await this.insertEntity('user', this.getDefaultUser(token)) // await this.insertEntity('user', this.getDefaultUser(token))
} else { // } else {
Logger.info(`[Db] Root user exists, pw: ${rootUser.hasPw}`) // Logger.info(`[Db] Root user exists, pw: ${rootUser.hasPw}`)
} // }
if (!this.libraries.length) { // if (!this.libraries.length) {
await this.insertEntity('library', this.getDefaultLibrary()) // await this.insertEntity('library', this.getDefaultLibrary())
} // }
if (!this.serverSettings) { if (!this.serverSettings) {
this.serverSettings = new ServerSettings() this.serverSettings = new ServerSettings()

View File

@ -34,18 +34,18 @@ const AudioMetadataMangaer = require('./managers/AudioMetadataManager')
const RssFeedManager = require('./managers/RssFeedManager') const RssFeedManager = require('./managers/RssFeedManager')
class Server { class Server {
constructor(PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, AUDIOBOOK_PATH) { constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH) {
this.Source = SOURCE
this.Port = PORT this.Port = PORT
this.Host = HOST this.Host = HOST
global.Uid = isNaN(UID) ? 0 : Number(UID) global.Uid = isNaN(UID) ? 0 : Number(UID)
global.Gid = isNaN(GID) ? 0 : Number(GID) global.Gid = isNaN(GID) ? 0 : Number(GID)
global.ConfigPath = Path.normalize(CONFIG_PATH) global.ConfigPath = Path.normalize(CONFIG_PATH)
global.AudiobookPath = Path.normalize(AUDIOBOOK_PATH)
global.MetadataPath = Path.normalize(METADATA_PATH) global.MetadataPath = Path.normalize(METADATA_PATH)
// Fix backslash if not on Windows // Fix backslash if not on Windows
if (process.platform !== 'win32') { if (process.platform !== 'win32') {
global.ConfigPath = global.ConfigPath.replace(/\\/g, '/') global.ConfigPath = global.ConfigPath.replace(/\\/g, '/')
global.AudiobookPath = global.AudiobookPath.replace(/\\/g, '/')
global.MetadataPath = global.MetadataPath.replace(/\\/g, '/') global.MetadataPath = global.MetadataPath.replace(/\\/g, '/')
} }
@ -57,10 +57,6 @@ class Server {
fs.mkdirSync(global.MetadataPath) fs.mkdirSync(global.MetadataPath)
filePerms.setDefaultDirSync(global.MetadataPath, false) filePerms.setDefaultDirSync(global.MetadataPath, false)
} }
if (!fs.pathExistsSync(global.AudiobookPath)) {
fs.mkdirSync(global.AudiobookPath)
filePerms.setDefaultDirSync(global.AudiobookPath, false)
}
this.db = new Db() this.db = new Db()
this.watcher = new Watcher() this.watcher = new Watcher()
@ -140,8 +136,6 @@ class Server {
await this.db.init() await this.db.init()
} }
this.auth.init()
await this.checkUserMediaProgress() // Remove invalid user item progress await this.checkUserMediaProgress() // Remove invalid user item progress
await this.purgeMetadata() // Remove metadata folders without library item await this.purgeMetadata() // Remove metadata folders without library item
@ -231,6 +225,25 @@ class Server {
app.post('/login', this.getLoginRateLimiter(), (req, res) => this.auth.login(req, res)) app.post('/login', this.getLoginRateLimiter(), (req, res) => this.auth.login(req, res))
app.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this)) app.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this))
app.post('/init', (req, res) => {
if (this.db.hasRootUser) {
Logger.error(`[Server] attempt to init server when server already has a root user`)
return res.sendStatus(500)
}
this.initializeServer(req, res)
})
app.get('/status', (req, res) => {
// status check for client to see if server has been initialized
// server has been initialized if a root user exists
const payload = {
isInit: this.db.hasRootUser
}
if (!payload.isInit) {
payload.ConfigPath = global.ConfigPath
payload.MetadataPath = global.MetadataPath
}
res.json(payload)
})
app.get('/ping', (req, res) => { app.get('/ping', (req, res) => {
Logger.info('Recieved ping') Logger.info('Recieved ping')
res.json({ success: true }) res.json({ success: true })
@ -293,6 +306,17 @@ class Server {
}) })
} }
async initializeServer(req, res) {
Logger.info(`[Server] Initializing new server`)
const newRoot = req.body.newRoot
let rootPash = newRoot.password ? await this.auth.hashPass(newRoot.password) : ''
if (!rootPash) Logger.warn(`[Server] Creating root user with no password`)
let rootToken = await this.auth.generateAccessToken({ userId: 'root' })
await this.db.createRootUser(newRoot.username, rootPash, rootToken)
res.sendStatus(200)
}
async filesChanged(fileUpdates) { async filesChanged(fileUpdates) {
Logger.info('[Server]', fileUpdates.length, 'Files Changed') Logger.info('[Server]', fileUpdates.length, 'Files Changed')
await this.scanner.scanFilesChanged(fileUpdates) await this.scanner.scanFilesChanged(fileUpdates)
@ -433,7 +457,6 @@ class Server {
const initialPayload = { const initialPayload = {
// TODO: this is sent with user auth now, update mobile app to use that then remove this // TODO: this is sent with user auth now, update mobile app to use that then remove this
serverSettings: this.db.serverSettings.toJSON(), serverSettings: this.db.serverSettings.toJSON(),
audiobookPath: global.AudiobookPath,
metadataPath: global.MetadataPath, metadataPath: global.MetadataPath,
configPath: global.ConfigPath, configPath: global.ConfigPath,
user: client.user.toJSONForBrowser(), user: client.user.toJSONForBrowser(),

View File

@ -42,6 +42,7 @@ class LibraryController {
newLibraryPayload.displayOrder = this.db.libraries.length + 1 newLibraryPayload.displayOrder = this.db.libraries.length + 1
library.setData(newLibraryPayload) library.setData(newLibraryPayload)
await this.db.insertEntity('library', library) await this.db.insertEntity('library', library)
// TODO: Only emit to users that have access
this.emitter('library_added', library.toJSON()) this.emitter('library_added', library.toJSON())
// Add library watcher // Add library watcher

View File

@ -242,13 +242,6 @@ class DownloadManager {
if (shouldIncludeCover) { if (shouldIncludeCover) {
var _cover = audiobook.book.coverFullPath.replace(/\\/g, '/') var _cover = audiobook.book.coverFullPath.replace(/\\/g, '/')
// Supporting old local file prefix
var bookCoverPath = audiobook.book.cover ? audiobook.book.cover.replace(/\\/g, '/') : null
if (!_cover && bookCoverPath && bookCoverPath.startsWith('/local')) {
_cover = Path.posix.join(global.AudiobookPath, _cover.replace('/local', ''))
Logger.debug('Local cover url', _cover)
}
ffmpegInputs.push({ ffmpegInputs.push({
input: _cover, input: _cover,
options: ['-f image2pipe'] options: ['-f image2pipe']