Compare commits

...

2 Commits

Author SHA1 Message Date
Mark Cooper fcd664c16e Side rail, book group cards, fix dropdown select 2021-09-24 07:32:38 -05:00
Mark Cooper 94741598af Pkg scripts win/linux 2021-09-22 20:40:35 -05:00
25 changed files with 807 additions and 88 deletions
+8
View File
@@ -102,3 +102,11 @@
.box-shadow-book { .box-shadow-book {
box-shadow: 4px 1px 8px #11111166, -4px 1px 8px #11111166, 1px -4px 8px #11111166; box-shadow: 4px 1px 8px #11111166, -4px 1px 8px #11111166, 1px -4px 8px #11111166;
} }
.box-shadow-book3d {
box-shadow: 4px 1px 8px #11111166, 1px -4px 8px #11111166;
}
.box-shadow-side {
box-shadow: 4px 0px 4px #11111166;
}
+9 -10
View File
@@ -7,15 +7,16 @@
<span class="material-icons text-4xl text-white">arrow_back</span> <span class="material-icons text-4xl text-white">arrow_back</span>
</a> </a>
<h1 class="text-2xl font-book mr-6">AudioBookshelf</h1> <h1 class="text-2xl font-book mr-6">AudioBookshelf</h1>
<!-- <div class="-mb-2">
<h1 class="text-lg font-book leading-3 mr-6 px-1">AudioBookshelf</h1>
<div class="bg-black bg-opacity-20 rounded-sm py-1.5 px-2 mt-1.5 flex items-center justify-between border border-bg">
<p class="text-sm text-gray-400 leading-3">My Library</p>
<span class="material-icons text-sm leading-3 text-gray-400">expand_more</span>
</div>
</div> -->
<controls-global-search /> <controls-global-search />
<div class="flex-grow" /> <div class="flex-grow" />
<!-- <a v-if="isUpdateAvailable" :href="githubTagUrl" target="_blank" class="flex items-center rounded-full bg-warning p-2 text-sm">
<span class="material-icons">notification_important</span>
<span class="pl-2">Update is available! Check release notes for v{{ latestVersion }}</span>
</a> -->
<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"> <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">
<span class="material-icons">upload</span> <span class="material-icons">upload</span>
</nuxt-link> </nuxt-link>
@@ -45,10 +46,8 @@
</ui-tooltip> </ui-tooltip>
<template v-if="userCanUpdate"> <template v-if="userCanUpdate">
<ui-icon-btn v-show="!processingBatchDelete" icon="edit" bg-color="warning" class="mx-1.5" @click="batchEditClick" /> <ui-icon-btn v-show="!processingBatchDelete" icon="edit" bg-color="warning" class="mx-1.5" @click="batchEditClick" />
<!-- <ui-btn v-show="!processingBatchDelete" color="warning" small class="mx-2 w-10 h-10" :padding-y="0" :padding-x="0" @click="batchEditClick"><span class="material-icons text-gray-200 text-base">edit</span></ui-btn> -->
</template> </template>
<ui-icon-btn v-show="userCanDelete" :disabled="processingBatchDelete" icon="delete" bg-color="error" class="mx-1.5" @click="batchDeleteClick" /> <ui-icon-btn v-show="userCanDelete" :disabled="processingBatchDelete" icon="delete" bg-color="error" class="mx-1.5" @click="batchDeleteClick" />
<!-- <ui-btn v-if="userCanDelete" color="error" small class="mx-2" :loading="processingBatchDelete" @click="batchDeleteClick"><span class="material-icons text-gray-200 pt-1">delete</span></ui-btn> -->
<span class="material-icons text-4xl px-4 hover:text-gray-100 cursor-pointer" :class="processingBatchDelete ? 'text-gray-400' : ''" @click="cancelSelectionMode">close</span> <span class="material-icons text-4xl px-4 hover:text-gray-100 cursor-pointer" :class="processingBatchDelete ? 'text-gray-400' : ''" @click="cancelSelectionMode">close</span>
</div> </div>
</div> </div>
@@ -64,7 +63,7 @@ export default {
}, },
computed: { computed: {
showBack() { showBack() {
return this.$route.name !== 'index' return this.$route.name !== 'library-id'
}, },
user() { user() {
return this.$store.state.user.user return this.$store.state.user.user
@@ -115,7 +114,7 @@ export default {
if (this.$route.name === 'audiobook-id-edit') { if (this.$route.name === 'audiobook-id-edit') {
this.$router.push(`/audiobook/${this.$route.params.id}`) this.$router.push(`/audiobook/${this.$route.params.id}`)
} else { } else {
this.$router.push('/') this.$router.push('/library')
} }
}, },
cancelSelectionMode() { cancelSelectionMode() {
+62 -42
View File
@@ -16,20 +16,22 @@
<ui-btn color="success" class="w-52" @click="scan">Scan Audiobooks</ui-btn> <ui-btn color="success" class="w-52" @click="scan">Scan Audiobooks</ui-btn>
</div> </div>
</div> </div>
<div v-else class="w-full flex flex-col items-center"> <div v-else id="bookshelf" class="w-full flex flex-col items-center">
<template v-for="(shelf, index) in groupedBooks"> <template v-for="(shelf, index) in shelves">
<div :key="index" class="w-full bookshelfRow relative"> <div :key="index" class="w-full bookshelfRow relative">
<div class="flex justify-center items-center"> <div class="flex justify-center items-center">
<template v-for="audiobook in shelf"> <template v-for="entity in shelf">
<cards-book-card :ref="`audiobookCard-${audiobook.id}`" :key="audiobook.id" :width="bookCoverWidth" :user-progress="userAudiobooks[audiobook.id]" :audiobook="audiobook" /> <cards-group-card v-if="showGroups" :key="entity.id" :width="bookCoverWidth" :group="entity" @click="clickGroup" />
<!-- <cards-book-3d :key="entity.id" v-else :width="100" :src="$store.getters['audiobooks/getBookCoverSrc'](entity.book)" /> -->
<cards-book-card v-else :key="entity.id" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" />
</template> </template>
</div> </div>
<div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" /> <div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" />
</div> </div>
</template> </template>
<div v-show="!groupedBooks.length" class="w-full py-16 text-center text-xl"> <div v-show="!shelves.length" class="w-full py-16 text-center text-xl">
<div class="py-4">No Audiobooks</div> <div class="py-4">No {{ showGroups ? 'Series' : 'Audiobooks' }}</div>
<ui-btn v-if="filterBy !== 'all' || keywordFilter" @click="clearFilter">Clear Filter</ui-btn> <ui-btn v-if="!showGroups && (filterBy !== 'all' || keywordFilter)" @click="clearFilter">Clear Filter</ui-btn>
</div> </div>
</div> </div>
</div> </div>
@@ -37,11 +39,13 @@
<script> <script>
export default { export default {
props: {
page: String,
selectedSeries: String
},
data() { data() {
return { return {
width: 0, shelves: [],
booksPerRow: 0,
groupedBooks: [],
currFilterOrderKey: null, currFilterOrderKey: null,
availableSizes: [60, 80, 100, 120, 140, 160, 180, 200, 220], availableSizes: [60, 80, 100, 120, 140, 160, 180, 200, 220],
selectedSizeIndex: 3, selectedSizeIndex: 3,
@@ -53,6 +57,11 @@ export default {
watch: { watch: {
keywordFilter() { keywordFilter() {
this.checkKeywordFilter() this.checkKeywordFilter()
},
selectedSeries() {
this.$nextTick(() => {
this.setBookshelfEntities()
})
} }
}, },
computed: { computed: {
@@ -85,9 +94,30 @@ export default {
}, },
filterBy() { filterBy() {
return this.$store.getters['user/getUserSetting']('filterBy') return this.$store.getters['user/getUserSetting']('filterBy')
},
showGroups() {
return this.page !== '' && !this.selectedSeries
},
entities() {
if (this.page === '') {
return this.$store.getters['audiobooks/getFilteredAndSorted']()
} else {
var seriesGroups = this.$store.getters['audiobooks/getSeriesGroups']()
if (this.selectedSeries) {
var group = seriesGroups.find((group) => group.name === this.selectedSeries)
return group.books
}
return seriesGroups
}
} }
}, },
methods: { methods: {
clickGroup(group) {
this.$emit('update:selectedSeries', group.name)
},
changeRotation() {
this.rotation = 'show-right'
},
clearFilter() { clearFilter() {
this.$store.commit('audiobooks/setKeywordFilter', null) this.$store.commit('audiobooks/setKeywordFilter', null)
if (this.filterBy !== 'all') { if (this.filterBy !== 'all') {
@@ -95,13 +125,13 @@ export default {
filterBy: 'all' filterBy: 'all'
}) })
} else { } else {
this.setGroupedBooks() this.setBookshelfEntities()
} }
}, },
checkKeywordFilter() { checkKeywordFilter() {
clearTimeout(this.keywordFilterTimeout) clearTimeout(this.keywordFilterTimeout)
this.keywordFilterTimeout = setTimeout(() => { this.keywordFilterTimeout = setTimeout(() => {
this.setGroupedBooks() this.setBookshelfEntities()
}, 500) }, 500)
}, },
increaseSize() { increaseSize() {
@@ -114,59 +144,51 @@ export default {
this.resize() this.resize()
this.$store.dispatch('user/updateUserSettings', { bookshelfCoverSize: this.bookCoverWidth }) this.$store.dispatch('user/updateUserSettings', { bookshelfCoverSize: this.bookCoverWidth })
}, },
setGroupedBooks() { setBookshelfEntities() {
var width = Math.max(0, this.$refs.wrapper.clientWidth - this.rowPaddingX * 2)
var booksPerRow = Math.floor(width / this.bookWidth)
var entities = this.entities
var groups = [] var groups = []
var currentRow = 0 var currentRow = 0
var currentGroup = [] var currentGroup = []
var audiobooksSorted = this.$store.getters['audiobooks/getFilteredAndSorted']() for (let i = 0; i < entities.length; i++) {
this.currFilterOrderKey = this.filterOrderKey var row = Math.floor(i / booksPerRow)
for (let i = 0; i < audiobooksSorted.length; i++) {
var row = Math.floor(i / this.booksPerRow)
if (row > currentRow) { if (row > currentRow) {
groups.push([...currentGroup]) groups.push([...currentGroup])
currentRow = row currentRow = row
currentGroup = [] currentGroup = []
} }
currentGroup.push(audiobooksSorted[i]) currentGroup.push(entities[i])
} }
if (currentGroup.length) { if (currentGroup.length) {
groups.push([...currentGroup]) groups.push([...currentGroup])
} }
this.groupedBooks = groups this.shelves = groups
}, },
calculateBookshelf() { async init() {
this.width = this.$refs.wrapper.clientWidth
this.width = Math.max(0, this.width - this.rowPaddingX * 2)
var booksPerRow = Math.floor(this.width / this.bookWidth)
this.booksPerRow = booksPerRow
},
getAudiobookCard(id) {
if (this.$refs[`audiobookCard-${id}`] && this.$refs[`audiobookCard-${id}`].length) {
return this.$refs[`audiobookCard-${id}`][0]
}
return null
},
init() {
var bookshelfCoverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize') var bookshelfCoverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
var sizeIndex = this.availableSizes.findIndex((s) => s === bookshelfCoverSize) var sizeIndex = this.availableSizes.findIndex((s) => s === bookshelfCoverSize)
if (!isNaN(sizeIndex)) this.selectedSizeIndex = sizeIndex if (!isNaN(sizeIndex)) this.selectedSizeIndex = sizeIndex
this.calculateBookshelf()
var isLoading = await this.$store.dispatch('audiobooks/load')
if (!isLoading) {
this.setBookshelfEntities()
}
}, },
resize() { resize() {
this.$nextTick(() => { this.$nextTick(() => {
this.calculateBookshelf() this.setBookshelfEntities()
this.setGroupedBooks()
}) })
}, },
audiobooksUpdated() { audiobooksUpdated() {
console.log('[AudioBookshelf] Audiobooks Updated') console.log('[AudioBookshelf] Audiobooks Updated')
this.setGroupedBooks() this.setBookshelfEntities()
}, },
settingsUpdated(settings) { settingsUpdated(settings) {
if (this.currFilterOrderKey !== this.filterOrderKey) { if (this.currFilterOrderKey !== this.filterOrderKey) {
this.setGroupedBooks() this.setBookshelfEntities()
} }
if (settings.bookshelfCoverSize !== this.bookCoverWidth && settings.bookshelfCoverSize !== undefined) { if (settings.bookshelfCoverSize !== this.bookCoverWidth && settings.bookshelfCoverSize !== undefined) {
var index = this.availableSizes.indexOf(settings.bookshelfCoverSize) var index = this.availableSizes.indexOf(settings.bookshelfCoverSize)
@@ -181,17 +203,15 @@ export default {
} }
}, },
mounted() { mounted() {
window.addEventListener('resize', this.resize)
this.$store.commit('audiobooks/addListener', { id: 'bookshelf', meth: this.audiobooksUpdated }) this.$store.commit('audiobooks/addListener', { id: 'bookshelf', meth: this.audiobooksUpdated })
this.$store.commit('user/addSettingsListener', { id: 'bookshelf', meth: this.settingsUpdated }) this.$store.commit('user/addSettingsListener', { id: 'bookshelf', meth: this.settingsUpdated })
this.$store.dispatch('audiobooks/load')
this.init() this.init()
window.addEventListener('resize', this.resize)
}, },
beforeDestroy() { beforeDestroy() {
window.removeEventListener('resize', this.resize)
this.$store.commit('audiobooks/removeListener', 'bookshelf') this.$store.commit('audiobooks/removeListener', 'bookshelf')
this.$store.commit('user/removeSettingsListener', 'bookshelf') this.$store.commit('user/removeSettingsListener', 'bookshelf')
window.removeEventListener('resize', this.resize)
} }
} }
</script> </script>
+41 -7
View File
@@ -1,20 +1,31 @@
<template> <template>
<div class="w-full h-10 relative"> <div class="w-full h-10 relative">
<div id="toolbar" class="absolute top-0 left-0 w-full h-full z-20 flex items-center px-8"> <div id="toolbar" class="absolute top-0 left-0 w-full h-full z-20 flex items-center px-8">
<p class="font-book">{{ numShowing }} Audiobooks</p> <p v-if="!selectedSeries" class="font-book">{{ numShowing }} {{ entityName }}</p>
<div v-else class="flex items-center">
<div @click="seriesBackArrow" class="rounded-full h-10 w-10 flex items-center justify-center hover:bg-white hover:bg-opacity-10 cursor-pointer">
<span class="material-icons text-3xl text-white">west</span>
</div>
<!-- <span class="material-icons text-2xl cursor-pointer" @click="seriesBackArrow">west</span> -->
<p class="pl-4 font-book text-lg">
{{ selectedSeries }} <span class="ml-3 font-mono text-lg bg-black bg-opacity-30 rounded-lg px-1 py-0.5">{{ numShowing }}</span>
</p>
</div>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-text-input v-model="_keywordFilter" placeholder="Keyword Filter" :padding-y="1.5" class="text-xs w-40" /> <ui-text-input v-show="showSortFilters" v-model="_keywordFilter" placeholder="Keyword Filter" :padding-y="1.5" class="text-xs w-40" />
<controls-filter-select v-show="showSortFilters" v-model="settings.filterBy" class="w-48 h-7.5 ml-4" @change="updateFilter" />
<controls-filter-select v-model="settings.filterBy" class="w-48 h-7.5 ml-4" @change="updateFilter" /> <controls-order-select v-show="showSortFilters" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-48 h-7.5 ml-4" @change="updateOrder" />
<controls-order-select v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-48 h-7.5 ml-4" @change="updateOrder" />
</div> </div>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: {
page: String,
selectedSeries: String
},
data() { data() {
return { return {
settings: {}, settings: {},
@@ -22,8 +33,27 @@ export default {
} }
}, },
computed: { computed: {
showSortFilters() {
return this.page === ''
},
numShowing() { numShowing() {
return this.$store.getters['audiobooks/getFiltered']().length if (this.page === '') {
return this.$store.getters['audiobooks/getFiltered']().length
} else {
var groups = this.$store.getters['audiobooks/getSeriesGroups']()
if (this.selectedSeries) {
var group = groups.find((g) => g.name === this.selectedSeries)
if (group) return group.books.length
return 0
}
return groups.length
}
},
entityName() {
if (!this.page) return 'Audiobooks'
if (this.page === 'series') return 'Series'
if (this.page === 'collections') return 'Collections'
return ''
}, },
_keywordFilter: { _keywordFilter: {
get() { get() {
@@ -35,6 +65,10 @@ export default {
} }
}, },
methods: { methods: {
seriesBackArrow() {
this.$router.replace('/library/series')
this.$emit('update:selectedSeries', null)
},
updateOrder() { updateOrder() {
this.saveSettings() this.saveSettings()
}, },
+71
View File
@@ -0,0 +1,71 @@
<template>
<div class="w-20 border-r border-primary bg-bg h-full relative box-shadow-side z-40" style="min-width: 80px">
<nuxt-link to="/library" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === '' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
<p class="font-book pt-1.5" style="font-size: 1rem">Library</p>
<div v-show="paramId === ''" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<nuxt-link to="/library/series" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'series' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
</svg>
<p class="font-book pt-1.5" style="font-size: 1rem">Series</p>
<div v-show="paramId === 'series'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<!-- <nuxt-link to="/library/collections" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
<p class="font-book pt-1.5" style="font-size: 0.8rem">Collections</p>
<div v-show="paramId === 'collections'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link> -->
<!-- <nuxt-link to="/library/tags" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'tags' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
</svg>
<p class="font-book pt-1.5" style="font-size: 0.8rem">Tags</p>
<div v-show="paramId === 'tags'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link> -->
<!-- <nuxt-link to="/library/authors" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'authors' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
<p class="font-book pt-1.5" style="font-size: 0.8rem">Authors</p>
<div v-show="paramId === 'authors'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link> -->
</div>
</template>
<script>
export default {
data() {
return {}
},
computed: {
paramId() {
return this.$route.params ? this.$route.params.id || '' : ''
},
selectedClassName() {
return ''
}
},
methods: {},
mounted() {}
}
</script>
+1 -1
View File
@@ -68,7 +68,7 @@ export default {
methods: { methods: {
filterByAuthor() { filterByAuthor() {
if (this.$route.name !== 'index') { if (this.$route.name !== 'index') {
this.$router.push('/') this.$router.push('/library')
} }
var settingsUpdate = { var settingsUpdate = {
filterBy: `authors.${this.$encode(this.author)}` filterBy: `authors.${this.$encode(this.author)}`
+254
View File
@@ -0,0 +1,254 @@
<template>
<div ref="wrapper" class="relative pointer-events-none" :style="{ width: standardWidth * 0.8 * 1.1 * scale + 'px', height: standardHeight * 1.1 * scale + 'px', marginBottom: 20 + 'px', marginTop: 15 + 'px' }">
<div ref="card" class="wrap absolute origin-center transform duration-200" :style="{ transform: `scale(${scale * scaleMultiplier}) translateY(${hover2 ? '-40%' : '-50%'})` }">
<div class="perspective">
<div class="book-wrap transform duration-100 pointer-events-auto" :class="hover2 ? 'z-80' : 'rotate'" @mouseover="hover = true" @mouseout="hover = false">
<div class="book book-1 box-shadow-book3d" ref="front"></div>
<div class="title book-1 pointer-events-none" ref="left"></div>
<div class="bottom book-1 pointer-events-none" ref="bottom"></div>
<div class="book-back book-1 pointer-events-none">
<div class="text pointer-events-none">
<h3 class="mb-4">Book Back</h3>
<p>
<span>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Sunt earum doloremque aliquam culpa dolor nostrum consequatur quas dicta? Molestias repellendus minima pariatur libero vel, reiciendis optio magnam rerum, labore corporis.</span>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
src: String,
width: {
type: Number,
default: 200
}
},
data() {
return {
hover: false,
hover2: false,
standardWidth: 200,
standardHeight: 320,
isAttached: true,
pageX: 0,
pageY: 0
}
},
watch: {
src(newVal) {
this.setCover()
},
width(newVal) {
this.init()
},
hover(newVal) {
if (newVal) {
this.unattach()
} else {
this.attach()
}
setTimeout(() => {
this.hover2 = newVal
}, 100)
}
},
computed: {
scaleMultiplier() {
return this.hover2 ? 1.25 : 1
},
scale() {
var scale = this.width / this.standardWidth
return scale
}
},
methods: {
unattach() {
if (this.$refs.card && this.isAttached) {
var bookshelf = document.getElementById('bookshelf')
if (bookshelf) {
var pos = this.$refs.wrapper.getBoundingClientRect()
this.pageX = pos.x
this.pageY = pos.y
document.body.appendChild(this.$refs.card)
this.$refs.card.style.left = this.pageX + 'px'
this.$refs.card.style.top = this.pageY + 'px'
this.$refs.card.style.zIndex = 50
this.isAttached = false
} else if (bookshelf) {
console.log(this.pageX, this.pageY)
this.isAttached = false
}
}
},
attach() {
if (this.$refs.card && !this.isAttached) {
if (this.$refs.wrapper) {
this.isAttached = true
this.$refs.wrapper.appendChild(this.$refs.card)
this.$refs.card.style.left = '0px'
this.$refs.card.style.top = '0px'
}
} else {
console.log('Is attached already', this.isAttached)
}
},
init() {
var standardWidth = this.standardWidth
document.documentElement.style.setProperty('--book-w', standardWidth + 'px')
document.documentElement.style.setProperty('--book-wx', standardWidth + 1 + 'px')
document.documentElement.style.setProperty('--book-h', standardWidth * 1.6 + 'px')
document.documentElement.style.setProperty('--book-d', 40 + 'px')
},
setElBg(el) {
el.style.backgroundImage = `url("${this.src}")`
el.style.backgroundSize = 'cover'
el.style.backgroundPosition = 'center center'
el.style.backgroundRepeat = 'no-repeat'
},
setCover() {
if (this.$refs.front) {
this.setElBg(this.$refs.front)
}
if (this.$refs.bottom) {
this.setElBg(this.$refs.bottom)
this.$refs.bottom.style.backgroundSize = '2000%'
this.$refs.bottom.style.filter = 'blur(1px)'
}
if (this.$refs.left) {
this.setElBg(this.$refs.left)
this.$refs.left.style.backgroundSize = '2000%'
this.$refs.left.style.filter = 'blur(1px)'
}
}
},
mounted() {
this.setCover()
this.init()
}
}
</script>
<style>
/* :root {
--book-w: 200px;
--book-h: 320px;
--book-d: 30px;
--book-wx: 201px;
} */
/*
.wrap {
width: calc(1.1 * var(--book-w));
height: calc(1.1 * var(--book-h));
margin: 0 auto;
}
.perspective {
position: relative;
width: 100%;
height: 100%;
perspective: 600px;
transform-style: preserve-3d;
overflow: hidden;
}
.book-wrap {
height: 100%;
width: 100%;
transform-style: preserve-3d;
transition: 'all ease-out 0.6s';
}
.book {
width: var(--book-w);
height: var(--book-h);
background: url(https://covers.openlibrary.org/b/id/8303020-L.jpg) no-repeat center center;
background-size: cover;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
cursor: pointer;
}
.title {
content: '';
height: var(--book-h);
width: var(--book-d);
position: absolute;
right: 0;
left: calc(var(--book-wx) * -1);
top: 0;
bottom: 0;
margin: auto;
background: #444;
transform: rotateY(-80deg) translateX(-14px);
background: url(https://covers.openlibrary.org/b/id/8303020-L.jpg) no-repeat center center;
background-size: 5000%;
filter: blur(1px);
}
.bottom {
content: '';
height: var(--book-d);
width: var(--book-w);
position: absolute;
right: 0;
bottom: var(--book-h);
top: 0;
left: 0;
margin: auto;
background: #444;
transform: rotateY(0deg) rotateX(90deg) translateY(-15px) translateX(-2.5px) skewX(10deg);
background: url(https://covers.openlibrary.org/b/id/8303020-L.jpg) no-repeat center center;
background-size: 5000%;
filter: blur(1px);
}
.book-back {
width: var(--book-w);
height: var(--book-h);
background-color: #444;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
cursor: pointer;
transform: rotate(180deg) translateZ(-30px) translateX(5px);
}
.book-back .text {
transform: rotateX(180deg);
position: absolute;
bottom: 0px;
padding: 20px;
text-align: left;
font-size: 12px;
}
.book-back .text h3 {
color: #fff;
}
.book-back .text span {
display: block;
margin-bottom: 20px;
color: #fff;
}
.book-wrap.rotate {
transform: rotateY(30deg) rotateX(0deg);
}
.book-wrap.flip {
transform: rotateY(180deg);
} */
</style>
+1 -9
View File
@@ -79,15 +79,7 @@ export default {
return '/book_placeholder.jpg' return '/book_placeholder.jpg'
}, },
fullCoverUrl() { fullCoverUrl() {
if (!this.cover || this.cover === this.placeholderUrl) return this.placeholderUrl return this.$store.getters['audiobooks/getBookCoverSrc'](this.book, this.placeholderUrl)
if (this.cover.startsWith('http:') || this.cover.startsWith('https:')) return this.cover
try {
var url = new URL(this.cover, document.baseURI)
return url.href + `?token=${this.userToken}&ts=${this.bookLastUpdate}`
} catch (err) {
console.error(err)
return ''
}
}, },
cover() { cover() {
return this.book.cover || this.placeholderUrl return this.book.cover || this.placeholderUrl
+87
View File
@@ -0,0 +1,87 @@
<template>
<div class="relative">
<div class="rounded-sm h-full overflow-hidden relative" :style="{ padding: `16px ${paddingX}px` }" @mouseover="isHovering = true" @mouseleave="isHovering = false" @click="clickCard">
<nuxt-link :to="`/library/series?${groupType}=${groupEncode}`" class="cursor-pointer">
<div class="w-full relative" :class="isHovering ? 'bg-black-400' : 'bg-primary'" :style="{ height: height + 'px', width: height + 'px' }">
<cards-group-cover ref="groupcover" :name="groupName" :book-items="bookItems" :width="height" :height="height" />
<div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'">
<p class="truncate font-book" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ groupName }}</p>
</div>
<div class="absolute top-2 right-2 w-7 h-7 rounded-lg bg-black bg-opacity-90 text-gray-300 box-shadow-book flex items-center justify-center border border-white border-opacity-25 pointer-events-none">
<p class="font-book text-xl">{{ bookItems.length }}</p>
</div>
</div>
</nuxt-link>
</div>
</div>
</template>
<script>
export default {
props: {
group: {
type: Object,
default: () => null
},
width: {
type: Number,
default: 120
}
},
data() {
return {
isHovering: false
}
},
watch: {
width(newVal) {
this.$nextTick(() => {
if (this.$refs.groupcover) {
this.$refs.groupcover.init()
}
})
}
},
computed: {
_group() {
return this.group || {}
},
height() {
return this.width * 1.6
},
sizeMultiplier() {
return this.width / 120
},
paddingX() {
return 16 * this.sizeMultiplier
},
bookItems() {
return this._group.books || []
},
groupName() {
return this._group.name || 'No Name'
},
groupType() {
return this._group.type
},
groupEncode() {
return this.$encode(this.groupName)
},
filter() {
return `${this.groupType}.${this.$encode(this.groupName)}`
},
hasValidCovers() {
var validCovers = this.bookItems.map((bookItem) => bookItem.book.cover)
return !!validCovers.length
}
},
methods: {
clickCard() {
this.$emit('click', this.group)
}
},
mounted() {}
}
</script>
+139
View File
@@ -0,0 +1,139 @@
<template>
<div ref="wrapper" :style="{ height: height + 'px', width: width + 'px' }" class="relative">
<div v-if="noValidCovers" class="absolute top-0 left-0 w-full h-full flex items-center justify-center box-shadow-book">
<p :style="{ fontSize: sizeMultiplier + 'rem' }">{{ name }}</p>
</div>
</div>
</template>
<script>
export default {
props: {
name: String,
bookItems: {
type: Array,
default: () => []
},
width: Number,
height: Number
},
data() {
return {
noValidCovers: false,
coverDiv: null
}
},
watch: {
bookItems: {
immediate: true,
handler(newVal) {
if (newVal) {
// ensure wrapper is initialized
this.$nextTick(this.init)
}
}
}
},
computed: {
sizeMultiplier() {
return this.width / 192
}
},
methods: {
getCoverUrl(book) {
return this.$store.getters['audiobooks/getBookCoverSrc'](book, '')
},
async buildCoverImg(src, bgCoverWidth, offsetLeft, forceCoverBg = false) {
var showCoverBg =
forceCoverBg ||
(await new Promise((resolve) => {
var image = new Image()
image.onload = () => {
var { naturalWidth, naturalHeight } = image
var aspectRatio = naturalHeight / naturalWidth
var arDiff = Math.abs(aspectRatio - 1.6)
// If image aspect ratio is <= 1.45 or >= 1.75 then use cover bg, otherwise stretch to fit
if (arDiff > 0.15) {
resolve(true)
} else {
resolve(false)
}
}
image.onerror = (err) => {
console.error(err)
resolve(false)
}
image.src = src
}))
var imgdiv = document.createElement('div')
imgdiv.style.height = this.height + 'px'
imgdiv.style.width = bgCoverWidth + 'px'
imgdiv.style.left = offsetLeft + 'px'
imgdiv.className = 'absolute top-0 box-shadow-book'
imgdiv.style.boxShadow = '-4px 0px 4px #11111166'
// imgdiv.style.transform = 'skew(0deg, 15deg)'
if (showCoverBg) {
var coverbgwrapper = document.createElement('div')
coverbgwrapper.className = 'absolute top-0 left-0 w-full h-full bg-primary'
var coverbg = document.createElement('div')
coverbg.className = 'w-full h-full'
coverbg.style.backgroundImage = `url("${src}")`
coverbg.style.backgroundSize = 'cover'
coverbg.style.backgroundPosition = 'center'
coverbg.style.opacity = 0.25
coverbg.style.filter = 'blur(1px)'
coverbgwrapper.appendChild(coverbg)
imgdiv.appendChild(coverbgwrapper)
}
var img = document.createElement('img')
img.src = src
img.className = 'absolute top-0 left-0 w-full h-full'
img.style.objectFit = showCoverBg ? 'contain' : 'cover'
imgdiv.appendChild(img)
return imgdiv
},
async init() {
if (this.coverDiv) {
this.coverDiv.remove()
this.coverDiv = null
}
var validCovers = this.bookItems.map((bookItem) => this.getCoverUrl(bookItem.book)).filter((b) => b !== '')
if (!validCovers.length) {
this.noValidCovers = true
return
}
this.noValidCovers = false
var coverWidth = this.width
var widthPer = this.width
if (validCovers.length > 1) {
coverWidth = this.height / 1.6
widthPer = (this.width - coverWidth) / (validCovers.length - 1)
}
var outerdiv = document.createElement('div')
outerdiv.className = 'w-full h-full relative'
for (let i = 0; i < validCovers.length; i++) {
var offsetLeft = widthPer * i
var img = await this.buildCoverImg(validCovers[i], coverWidth, offsetLeft, validCovers.length === 1)
outerdiv.appendChild(img)
}
if (this.$refs.wrapper) {
this.coverDiv = outerdiv
this.$refs.wrapper.appendChild(outerdiv)
}
}
},
mounted() {}
}
</script>
+4 -1
View File
@@ -115,7 +115,10 @@ export default {
clickedOption(e, item) { clickedOption(e, item) {
this.textInput = null this.textInput = null
this.currentSearch = null this.currentSearch = null
this.input = this.textInput ? this.textInput.trim() : null this.input = item
// this.input = this.textInput ? this.textInput.trim() : null
console.log('Clicked option', item)
if (this.$refs.input) this.$refs.input.blur() if (this.$refs.input) this.$refs.input.blur()
} }
}, },
+1 -1
View File
@@ -53,7 +53,7 @@ export default {
var tooltip = document.createElement('div') var tooltip = document.createElement('div')
tooltip.className = 'absolute px-2 py-1 text-white pointer-events-none text-xs rounded shadow-lg max-w-xs' tooltip.className = 'absolute px-2 py-1 text-white pointer-events-none text-xs rounded shadow-lg max-w-xs'
tooltip.style.zIndex = 100 tooltip.style.zIndex = 100
tooltip.style.backgroundColor = 'rgba(0,0,0,0.75)' tooltip.style.backgroundColor = 'rgba(0,0,0,0.85)'
tooltip.innerHTML = this.text tooltip.innerHTML = this.text
this.setTooltipPosition(tooltip) this.setTooltipPosition(tooltip)
+3 -1
View File
@@ -1,7 +1,9 @@
<template> <template>
<div class="text-white max-h-screen h-screen overflow-hidden bg-bg"> <div class="text-white max-h-screen h-screen overflow-hidden bg-bg">
<app-appbar /> <app-appbar />
<Nuxt /> <Nuxt />
<app-stream-container ref="streamContainer" /> <app-stream-container ref="streamContainer" />
<modals-edit-modal /> <modals-edit-modal />
<widgets-scan-alert /> <widgets-scan-alert />
@@ -84,7 +86,7 @@ export default {
audiobookRemoved(audiobook) { audiobookRemoved(audiobook) {
if (this.$route.name.startsWith('audiobook')) { if (this.$route.name.startsWith('audiobook')) {
if (this.$route.params.id === audiobook.id) { if (this.$route.params.id === audiobook.id) {
this.$router.replace('/') this.$router.replace('/library')
} }
} }
this.$store.commit('audiobooks/remove', audiobook) this.$store.commit('audiobooks/remove', audiobook)
+2 -2
View File
@@ -1,7 +1,7 @@
export default function ({ store, redirect, route }) { export default function ({ store, redirect, route, app }) {
// If the user is not authenticated // If the user is not authenticated
if (!store.state.user.user) { if (!store.state.user.user) {
if (route.name === 'batch') return redirect('/login') if (route.name === 'batch') return redirect('/login')
return redirect(`/login?redirect=${route.path}`) return redirect(`/login?redirect=${route.fullPath}`)
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "1.1.15", "version": "1.2.0",
"description": "Audiobook manager and player", "description": "Audiobook manager and player",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
+1 -1
View File
@@ -130,7 +130,7 @@ export default {
this.isProcessing = false this.isProcessing = false
if (data.updates) { if (data.updates) {
this.$toast.success(`Successfully updated ${data.updates} audiobooks`) this.$toast.success(`Successfully updated ${data.updates} audiobooks`)
this.$router.replace('/') this.$router.replace('/library')
} else { } else {
this.$toast.warning('No updates were necessary') this.$toast.warning('No updates were necessary')
} }
+10 -2
View File
@@ -1,12 +1,20 @@
<template> <template>
<div class="page" :class="streamAudiobook ? 'streaming' : ''"> <div class="page" :class="streamAudiobook ? 'streaming' : ''">
<app-book-shelf-toolbar /> <!-- <app-book-shelf-toolbar /> -->
<app-book-shelf /> <!-- <div class="flex h-full">
<app-side-rail />
<div class="flex-grow"> -->
<!-- <app-book-shelf /> -->
<!-- </div> -->
<!-- </div> -->
</div> </div>
</template> </template>
<script> <script>
export default { export default {
asyncData({ redirect }) {
redirect('/library')
},
data() { data() {
return {} return {}
}, },
+35
View File
@@ -0,0 +1,35 @@
<template>
<div class="page" :class="streamAudiobook ? 'streaming' : ''">
<div class="flex h-full">
<app-side-rail />
<div class="flex-grow">
<app-book-shelf-toolbar :page="id || ''" :selected-series.sync="selectedSeries" />
<app-book-shelf :page="id || ''" :selected-series.sync="selectedSeries" />
</div>
</div>
</div>
</template>
<script>
export default {
asyncData({ params, query, store, app }) {
if (query.filter) {
store.dispatch('user/updateUserSettings', { filterBy: query.filter })
}
return {
id: params.id,
selectedSeries: query.series ? app.$decode(query.series) : null
}
},
data() {
return {}
},
computed: {
streamAudiobook() {
return this.$store.state.streamAudiobook
}
},
methods: {},
mounted() {}
}
</script>
+1 -1
View File
@@ -37,7 +37,7 @@ export default {
if (this.$route.query.redirect) { if (this.$route.query.redirect) {
this.$router.replace(this.$route.query.redirect) this.$router.replace(this.$route.query.redirect)
} else { } else {
this.$router.replace('/') this.$router.replace('/library')
} }
} }
} }
+3
View File
@@ -124,4 +124,7 @@ Vue.prototype.$decode = decode
export { export {
encode, encode,
decode decode
}
export default ({ app }, inject) => {
app.$decode = decode
} }
+54 -2
View File
@@ -5,6 +5,7 @@ const STANDARD_GENRES = ['Adventure', 'Autobiography', 'Biography', 'Childrens',
export const state = () => ({ export const state = () => ({
audiobooks: [], audiobooks: [],
lastLoad: 0,
listeners: [], listeners: [],
genres: [...STANDARD_GENRES], genres: [...STANDARD_GENRES],
tags: [], tags: [],
@@ -63,6 +64,23 @@ export const getters = {
return value return value
}) })
}, },
getSeriesGroups: (state, getters, rootState) => () => {
var series = {}
state.audiobooks.forEach((audiobook) => {
if (audiobook.book && audiobook.book.series) {
if (series[audiobook.book.series]) {
series[audiobook.book.series].books.push(audiobook)
} else {
series[audiobook.book.series] = {
type: 'series',
name: audiobook.book.series,
books: [audiobook]
}
}
}
})
return Object.values(series)
},
getUniqueAuthors: (state) => { getUniqueAuthors: (state) => {
var _authors = state.audiobooks.filter(ab => !!(ab.book && ab.book.author)).map(ab => ab.book.author) var _authors = state.audiobooks.filter(ab => !!(ab.book && ab.book.author)).map(ab => ab.book.author)
return [...new Set(_authors)].sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1) return [...new Set(_authors)].sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1)
@@ -71,29 +89,63 @@ export const getters = {
var _genres = [] var _genres = []
state.audiobooks.filter(ab => !!(ab.book && ab.book.genres)).forEach(ab => _genres = _genres.concat(ab.book.genres)) state.audiobooks.filter(ab => !!(ab.book && ab.book.genres)).forEach(ab => _genres = _genres.concat(ab.book.genres))
return [...new Set(_genres)].sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1) return [...new Set(_genres)].sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1)
},
getBookCoverSrc: (state, getters, rootState, rootGetters) => (book, placeholder = '/book_placeholder.jpg') => {
if (!book || !book.cover || book.cover === placeholder) return placeholder
var cover = book.cover
// Absolute URL covers
if (cover.startsWith('http:') || cover.startsWith('https:')) return cover
// Server hosted covers
try {
// Ensure cover is refreshed if cached
var bookLastUpdate = book.lastUpdate || Date.now()
var userToken = rootGetters['user/getToken']
var url = new URL(cover, document.baseURI)
return url.href + `?token=${userToken}&ts=${bookLastUpdate}`
} catch (err) {
console.error(err)
return placeholder
}
} }
} }
export const actions = { export const actions = {
load({ commit, rootState }) { // Return true if calling load
load({ state, commit, rootState }) {
if (!rootState.user || !rootState.user.user) { if (!rootState.user || !rootState.user.user) {
console.error('audiobooks/load - User not set') console.error('audiobooks/load - User not set')
return return false
} }
// Don't load again if already loaded in the last 5 minutes
var lastLoadDiff = Date.now() - state.lastLoad
if (lastLoadDiff < 5 * 60 * 1000) {
// Already up to date
return false
}
this.$axios this.$axios
.$get(`/api/audiobooks`) .$get(`/api/audiobooks`)
.then((data) => { .then((data) => {
commit('set', data) commit('set', data)
commit('setLastLoad')
}) })
.catch((error) => { .catch((error) => {
console.error('Failed', error) console.error('Failed', error)
commit('set', []) commit('set', [])
}) })
return true
}, },
} }
export const mutations = { export const mutations = {
setLastLoad(state) {
state.lastLoad = Date.now()
},
setKeywordFilter(state, val) { setKeywordFilter(state, val) {
state.keywordFilter = val state.keywordFilter = val
}, },
+14 -2
View File
@@ -1,13 +1,25 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "1.1.15", "version": "1.2.0",
"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": {
"dev": "node index.js", "dev": "node index.js",
"start": "node index.js", "start": "node index.js",
"client": "cd client && npm install && npm run generate", "client": "cd client && npm install && npm run generate",
"prod": "npm run client && npm install && node prod.js" "prod": "npm run client && npm install && node prod.js",
"generate": "cd client && npm run generate",
"build-win": "pkg -t node12-win-x64 -o ./dist/win/audiobookshelf .",
"build-linuxarm": "pkg -t node12-linux-arm64 -o ./dist/linuxarm/audiobookshelf .",
"build-linuxamd": "pkg -t node12-linux-amd64 -o ./dist/linuxamd/audiobookshelf ."
},
"bin": "prod.js",
"pkg": {
"assets": "client/dist/**/*",
"scripts": [
"prod.js",
"server/**/*.js"
]
}, },
"author": "advplyr", "author": "advplyr",
"license": "ISC", "license": "ISC",
+2 -1
View File
@@ -13,6 +13,7 @@ process.env.TOKEN_SECRET = '09f26e402586e2faa8da4c98a35f1b20d6b033c6097befa8be34
process.env.NODE_ENV = 'production' process.env.NODE_ENV = 'production'
const server = require('./server/Server') 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
@@ -24,7 +25,7 @@ const CONFIG_PATH = inputConfig || process.env.CONFIG_PATH || Path.resolve('conf
const AUDIOBOOK_PATH = inputAudiobook || process.env.AUDIOBOOK_PATH || Path.resolve('audiobooks') 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')
console.log('Config', CONFIG_PATH, METADATA_PATH, AUDIOBOOK_PATH) console.log(process.env.NODE_ENV, 'Config', CONFIG_PATH, METADATA_PATH, AUDIOBOOK_PATH)
const Server = new server(PORT, CONFIG_PATH, METADATA_PATH, AUDIOBOOK_PATH) const Server = new server(PORT, CONFIG_PATH, METADATA_PATH, AUDIOBOOK_PATH)
Server.start() Server.start()
-3
View File
@@ -82,9 +82,6 @@ cd audiobookshelf
# Directories will be created if they don't exist # Directories will be created if they don't exist
# Paths are relative to the root directory, so "../Audiobooks" would be a valid path # Paths are relative to the root directory, so "../Audiobooks" would be a valid path
npm run prod -- -p [PORT] --audiobooks [AUDIOBOOKS_PATH] --config [CONFIG_PATH] --metadata [METADATA_PATH] npm run prod -- -p [PORT] --audiobooks [AUDIOBOOKS_PATH] --config [CONFIG_PATH] --metadata [METADATA_PATH]
# You only need to use `npm run prod` the first time, after that use `npm run start`
npm run start -- -p [PORT] --audiobooks [AUDIOBOOKS_PATH] --config [CONFIG_PATH] --metadata [METADATA_PATH]
``` ```
## Contributing ## Contributing
+3 -1
View File
@@ -113,6 +113,7 @@ class Server {
await this.streamManager.ensureStreamsDir() await this.streamManager.ensureStreamsDir()
await this.streamManager.removeOrphanStreams() await this.streamManager.removeOrphanStreams()
await this.downloadManager.removeOrphanDownloads() await this.downloadManager.removeOrphanDownloads()
await this.db.init() await this.db.init()
this.auth.init() this.auth.init()
@@ -171,7 +172,6 @@ class Server {
async start() { async start() {
Logger.info('=== Starting Server ===') Logger.info('=== Starting Server ===')
await this.init() await this.init()
const app = express() const app = express()
@@ -199,6 +199,8 @@ class Server {
// Dynamic routes are not generated on client // Dynamic routes are not generated on client
app.get('/audiobook/:id', (req, res) => res.sendFile(Path.join(distPath, 'index.html'))) app.get('/audiobook/:id', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
app.get('/library/:id', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
app.get('/library', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
app.use('/api', this.authMiddleware.bind(this), this.apiController.router) app.use('/api', this.authMiddleware.bind(this), this.apiController.router)
app.use('/hls', this.authMiddleware.bind(this), this.hlsController.router) app.use('/hls', this.authMiddleware.bind(this), this.hlsController.router)