Compare commits

...

7 Commits

51 changed files with 1228 additions and 164 deletions
+3 -2
View File
@@ -1,5 +1,6 @@
@import url('./transitions.css'); @import './fonts.css';
@import url('./draggable.css'); @import './transitions.css';
@import './draggable.css';
.page { .page {
width: 100%; width: 100%;
+47
View File
@@ -0,0 +1,47 @@
/* fallback */
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url(/fonts/material-icons.woff2) format('woff2');
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
line-height: 1;
font-size: 1.5rem;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased;
}
.material-icons.text-icon {
font-size: 1.15rem;
}
.material-icons.text-base {
font-size: 1rem;
}
@font-face {
font-family: 'Gentium Book Basic';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/fonts/GentiumBookBasic.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Gentium Book Basic';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/fonts/GentiumBookBasic.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
+65 -6
View File
@@ -1,7 +1,7 @@
<template> <template>
<div id="bookshelf" ref="wrapper" class="w-full h-full overflow-y-scroll relative"> <div id="bookshelf" ref="wrapper" class="w-full h-full overflow-y-scroll relative">
<!-- Cover size widget --> <!-- Cover size widget -->
<div v-show="!isSelectionMode" class="fixed bottom-2 right-4 z-20"> <div v-show="!isSelectionMode" class="fixed bottom-2 right-4 z-30">
<div class="rounded-full py-1 bg-primary px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent> <div class="rounded-full py-1 bg-primary px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent>
<span class="material-icons" :class="selectedSizeIndex === 0 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="decreaseSize">remove</span> <span class="material-icons" :class="selectedSizeIndex === 0 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="decreaseSize">remove</span>
<p class="px-2 font-mono">{{ bookCoverWidth }}</p> <p class="px-2 font-mono">{{ bookCoverWidth }}</p>
@@ -16,6 +16,14 @@
<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-if="page === 'search'" id="bookshelf-categorized" class="w-full flex flex-col items-center">
<template v-for="(shelf, index) in categorizedShelves">
<app-book-shelf-row :key="index" :index="index" :shelf="shelf" :size-multiplier="sizeMultiplier" :book-cover-width="bookCoverWidth" />
</template>
<div v-show="!categorizedShelves.length" class="w-full py-16 text-center text-xl">
<div class="py-4 mb-6"><p class="text-2xl">No Results</p></div>
</div>
</div>
<div v-else id="bookshelf" 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 shelves"> <template v-for="(shelf, index) in shelves">
<div :key="index" class="w-full bookshelfRow relative"> <div :key="index" class="w-full bookshelfRow relative">
@@ -23,7 +31,7 @@
<template v-for="entity in shelf"> <template v-for="entity in shelf">
<cards-group-card v-if="showGroups" :key="entity.id" :width="bookCoverWidth" :group="entity" @click="clickGroup" /> <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-3d :key="entity.id" v-else :width="100" :src="$store.getters['audiobooks/getBookCoverSrc'](entity.book)" /> -->
<cards-book-card v-else :key="entity.id" :show-volume-number="!!selectedSeries" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" /> <cards-book-card v-else :key="entity.id" :show-volume-number="!!selectedSeries" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" @edit="editBook" />
</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" />
@@ -45,8 +53,8 @@ export default {
page: String, page: String,
selectedSeries: String, selectedSeries: String,
searchResults: { searchResults: {
type: Array, type: Object,
default: () => [] default: () => {}
}, },
searchQuery: String searchQuery: String
}, },
@@ -74,7 +82,7 @@ export default {
}, },
searchResults() { searchResults() {
this.$nextTick(() => { this.$nextTick(() => {
this.$store.commit('audiobooks/setSearchResults', this.searchResults) // this.$store.commit('audiobooks/setSearchResults', this.searchResults)
this.setBookshelfEntities() this.setBookshelfEntities()
}) })
}, },
@@ -94,6 +102,9 @@ export default {
audiobooks() { audiobooks() {
return this.$store.state.audiobooks.audiobooks return this.$store.state.audiobooks.audiobooks
}, },
sizeMultiplier() {
return this.bookCoverWidth / 120
},
bookCoverWidth() { bookCoverWidth() {
return this.availableSizes[this.selectedSizeIndex] return this.availableSizes[this.selectedSizeIndex]
}, },
@@ -122,11 +133,54 @@ export default {
showGroups() { showGroups() {
return this.page !== '' && this.page !== 'search' && !this.selectedSeries return this.page !== '' && this.page !== 'search' && !this.selectedSeries
}, },
categorizedShelves() {
if (this.page !== 'search') return []
var audiobookSearchResults = this.searchResults ? this.searchResults.audiobooks || [] : []
const shelves = []
if (audiobookSearchResults.length) {
shelves.push({
label: 'Books',
books: audiobookSearchResults.map((absr) => absr.audiobook)
})
}
if (this.searchResults.series && this.searchResults.series.length) {
var seriesGroups = this.searchResults.series.map((seriesResult) => {
return {
type: 'series',
name: seriesResult.series || '',
books: seriesResult.audiobooks || []
}
})
shelves.push({
label: 'Series',
series: seriesGroups
})
}
if (this.searchResults.tags && this.searchResults.tags.length) {
var tagGroups = this.searchResults.tags.map((tagResult) => {
return {
type: 'tags',
name: tagResult.tag || '',
books: tagResult.audiobooks || []
}
})
shelves.push({
label: 'Tags',
series: tagGroups
})
}
return shelves
},
entities() { entities() {
if (this.page === '') { if (this.page === '') {
return this.$store.getters['audiobooks/getFilteredAndSorted']() return this.$store.getters['audiobooks/getFilteredAndSorted']()
} else if (this.page === 'search') { } else if (this.page === 'search') {
return this.searchResults || [] var audiobookSearchResults = this.searchResults ? this.searchResults.audiobooks || [] : []
return audiobookSearchResults.map((absr) => absr.audiobook)
} else { } else {
var seriesGroups = this.$store.getters['audiobooks/getSeriesGroups']() var seriesGroups = this.$store.getters['audiobooks/getSeriesGroups']()
if (this.selectedSeries) { if (this.selectedSeries) {
@@ -138,6 +192,11 @@ export default {
} }
}, },
methods: { methods: {
editBook(audiobook) {
var bookIds = this.entities.map((e) => e.id)
this.$store.commit('setBookshelfBookIds', bookIds)
this.$store.commit('showEditModal', audiobook)
},
clickGroup(group) { clickGroup(group) {
this.$emit('update:selectedSeries', group.name) this.$emit('update:selectedSeries', group.name)
}, },
@@ -51,9 +51,6 @@ export default {
sizeMultiplier() { sizeMultiplier() {
return this.bookCoverWidth / 120 return this.bookCoverWidth / 120
}, },
signSizeMultiplier() {
return (1 - this.sizeMultiplier) / 2 + this.sizeMultiplier
},
paddingX() { paddingX() {
return 16 * this.sizeMultiplier return 16 * this.sizeMultiplier
}, },
+19 -5
View File
@@ -2,9 +2,21 @@
<div class="relative"> <div class="relative">
<div ref="shelf" class="w-full max-w-full bookshelfRowCategorized relative overflow-x-scroll overflow-y-hidden z-10" :style="{ paddingLeft: 2.5 * sizeMultiplier + 'rem' }" @scroll="scrolled"> <div ref="shelf" class="w-full max-w-full bookshelfRowCategorized relative overflow-x-scroll overflow-y-hidden z-10" :style="{ paddingLeft: 2.5 * sizeMultiplier + 'rem' }" @scroll="scrolled">
<div class="w-full h-full" :style="{ marginTop: sizeMultiplier + 'rem' }"> <div class="w-full h-full" :style="{ marginTop: sizeMultiplier + 'rem' }">
<div class="flex items-center -mb-2"> <div v-if="shelf.books" class="flex items-center -mb-2">
<template v-for="entity in shelf.books"> <template v-for="entity in shelf.books">
<cards-book-card :key="entity.id" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" @hook:updated="updatedBookCard" /> <cards-book-card :key="entity.id" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" @hook:updated="updatedBookCard" @edit="editBook" />
</template>
</div>
<div v-else-if="shelf.series" class="flex items-center -mb-2">
<template v-for="entity in shelf.series">
<cards-group-card :key="entity.name" :width="bookCoverWidth" :group="entity" @click="$emit('clickSeries', entity)" />
</template>
</div>
<div v-else-if="shelf.tags" class="flex items-center -mb-2">
<template v-for="entity in shelf.tags">
<nuxt-link :key="entity.name" :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(entity.name)}`">
<cards-group-card :width="bookCoverWidth" :group="entity" />
</nuxt-link>
</template> </template>
</div> </div>
</div> </div>
@@ -53,6 +65,11 @@ export default {
} }
}, },
methods: { methods: {
editBook(audiobook) {
var bookIds = this.shelf.books.map((e) => e.id)
this.$store.commit('setBookshelfBookIds', bookIds)
this.$store.commit('showEditModal', audiobook)
},
scrolled() { scrolled() {
clearTimeout(this.scrollTimer) clearTimeout(this.scrollTimer)
this.scrollTimer = setTimeout(() => { this.scrollTimer = setTimeout(() => {
@@ -62,7 +79,6 @@ export default {
}, },
scrollLeft() { scrollLeft() {
if (!this.$refs.shelf) { if (!this.$refs.shelf) {
console.error('No Shelf', this.index)
return return
} }
this.isScrolling = true this.isScrolling = true
@@ -70,7 +86,6 @@ export default {
}, },
scrollRight() { scrollRight() {
if (!this.$refs.shelf) { if (!this.$refs.shelf) {
console.error('No Shelf', this.index)
return return
} }
this.isScrolling = true this.isScrolling = true
@@ -84,7 +99,6 @@ export default {
}, },
checkCanScroll() { checkCanScroll() {
if (!this.$refs.shelf) { if (!this.$refs.shelf) {
console.error('No Shelf', this.index)
return return
} }
var clientWidth = this.$refs.shelf.clientWidth var clientWidth = this.$refs.shelf.clientWidth
+4 -3
View File
@@ -41,8 +41,8 @@ export default {
isHome: Boolean, isHome: Boolean,
selectedSeries: String, selectedSeries: String,
searchResults: { searchResults: {
type: Array, type: Object,
default: () => [] default: () => {}
}, },
searchQuery: String searchQuery: String
}, },
@@ -60,7 +60,8 @@ export default {
if (this.page === '') { if (this.page === '') {
return this.$store.getters['audiobooks/getFiltered']().length return this.$store.getters['audiobooks/getFiltered']().length
} else if (this.page === 'search') { } else if (this.page === 'search') {
return (this.searchResults || []).length var audiobookSearchResults = this.searchResults ? this.searchResults.audiobooks || [] : []
return audiobookSearchResults.length
} else { } else {
var groups = this.$store.getters['audiobooks/getSeriesGroups']() var groups = this.$store.getters['audiobooks/getSeriesGroups']()
if (this.selectedSeries) { if (this.selectedSeries) {
+219
View File
@@ -0,0 +1,219 @@
<template>
<div class="w-full">
<div v-show="showPageMenu" v-click-outside="clickOutside" class="pagemenu absolute top-9 right-20 rounded-md overflow-y-auto bg-white shadow-lg z-20 border border-gray-400">
<div v-for="(file, index) in pages" :key="file" class="w-full cursor-pointer hover:bg-gray-200 px-2 py-1" @click="setPage(index)">
<p class="text-sm">{{ file }}</p>
</div>
</div>
<div class="absolute top-0 right-40 border-b border-l border-r border-gray-400 hover:bg-gray-200 cursor-pointer rounded-b-md bg-gray-50 w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="showPageMenu = !showPageMenu">
<span class="material-icons">menu</span>
</div>
<div class="absolute top-0 right-20 border-b border-l border-r border-gray-400 rounded-b-md bg-gray-50 px-2 h-9 flex items-center text-center">
<p class="font-mono">{{ page + 1 }} / {{ numPages }}</p>
</div>
<div class="overflow-hidden m-auto comicwrapper relative">
<div class="flex items-center justify-center">
<div class="px-12">
<span v-show="loadedFirstPage" class="material-icons text-5xl text-black" :class="!canGoPrev ? 'text-opacity-10' : 'cursor-pointer text-opacity-30 hover:text-opacity-90'" @click.stop.prevent="goPrevPage" @mousedown.prevent>arrow_back_ios</span>
</div>
<img v-if="mainImg" :src="mainImg" class="object-contain comicimg" />
<div class="px-12">
<span v-show="loadedFirstPage" class="material-icons text-5xl text-black" :class="!canGoNext ? 'text-opacity-10' : 'cursor-pointer text-opacity-30 hover:text-opacity-90'" @click.stop.prevent="goNextPage" @mousedown.prevent>arrow_forward_ios</span>
</div>
</div>
<div v-show="loading" class="w-full h-full absolute top-0 left-0 flex items-center justify-center z-10">
<ui-loading-indicator />
</div>
</div>
<!-- <div v-show="loading" class="w-screen h-screen absolute top-0 left-0 bg-black bg-opacity-20 flex items-center justify-center">
<ui-loading-indicator />
</div> -->
</div>
</template>
<script>
import Path from 'path'
import { Archive } from 'libarchive.js/main.js'
Archive.init({
workerUrl: '/libarchive/worker-bundle.js'
})
// Archive.init()
export default {
props: {
src: String
},
data() {
return {
loading: false,
pages: null,
filesObject: null,
mainImg: null,
page: 0,
numPages: 0,
showPageMenu: false,
loadTimeout: null,
loadedFirstPage: false
}
},
watch: {
src: {
immediate: true,
handler(newVal) {
this.extract()
}
}
},
computed: {
canGoNext() {
return this.page < this.numPages - 1
},
canGoPrev() {
return this.page > 0
}
},
methods: {
clickOutside() {
if (this.showPageMenu) this.showPageMenu = false
},
goNextPage() {
if (!this.canGoNext) return
this.setPage(this.page + 1)
},
goPrevPage() {
if (!this.canGoPrev) return
this.setPage(this.page - 1)
},
setPage(index) {
if (index < 0 || index > this.numPages - 1) {
return
}
var filename = this.pages[index]
this.page = index
return this.extractFile(filename)
},
setLoadTimeout() {
this.loadTimeout = setTimeout(() => {
this.loading = true
}, 150)
},
extractFile(filename) {
return new Promise(async (resolve) => {
this.setLoadTimeout()
var file = await this.filesObject[filename].extract()
var reader = new FileReader()
reader.onload = (e) => {
this.mainImg = e.target.result
this.loading = false
resolve()
}
reader.onerror = (e) => {
console.error(e)
this.$toast.error('Read page file failed')
this.loading = false
resolve()
}
reader.readAsDataURL(file)
clearTimeout(this.loadTimeout)
})
},
async extract() {
this.loading = true
console.log('Extracting', this.src)
var buff = await this.$axios.$get(this.src, {
responseType: 'blob'
})
const archive = await Archive.open(buff)
this.filesObject = await archive.getFilesObject()
var filenames = Object.keys(this.filesObject)
this.parseFilenames(filenames)
this.numPages = this.pages.length
if (this.pages.length) {
this.loading = false
await this.setPage(0)
this.loadedFirstPage = true
} else {
this.$toast.error('Unable to extract pages')
this.loading = false
}
},
parseImageFilename(filename) {
var basename = Path.basename(filename, Path.extname(filename))
var numbersinpath = basename.match(/\d{1,4}/g)
if (!numbersinpath || !numbersinpath.length) {
return {
index: -1,
filename
}
} else {
return {
index: Number(numbersinpath[numbersinpath.length - 1]),
filename
}
}
},
parseFilenames(filenames) {
const acceptableImages = ['.jpeg', '.jpg', '.png']
var imageFiles = filenames.filter((f) => {
return acceptableImages.includes((Path.extname(f) || '').toLowerCase())
})
var imageFileObjs = imageFiles.map((img) => {
return this.parseImageFilename(img)
})
var imagesWithNum = imageFileObjs.filter((i) => i.index >= 0)
var orderedImages = imagesWithNum.sort((a, b) => a.index - b.index).map((i) => i.filename)
var noNumImages = imageFileObjs.filter((i) => i.index < 0)
orderedImages = orderedImages.concat(noNumImages.map((i) => i.filename))
this.pages = orderedImages
},
keyUp(e) {
if ((e.keyCode || e.which) == 37) {
this.goPrevPage()
} else if ((e.keyCode || e.which) == 39) {
this.goNextPage()
} else if ((e.keyCode || e.which) == 27) {
this.unregisterListeners()
this.$emit('close')
}
},
registerListeners() {
document.addEventListener('keyup', this.keyUp)
},
unregisterListeners() {
document.removeEventListener('keyup', this.keyUp)
}
},
mounted() {
this.registerListeners()
},
beforeDestroy() {
this.unregisterListeners()
}
}
</script>
<style scoped>
.pagemenu {
max-height: calc(100vh - 60px);
}
.comicimg {
height: calc(100vh - 40px);
margin: auto;
}
.comicwrapper {
width: calc(100vw - 300px);
height: calc(100vh - 40px);
}
</style>
+74
View File
@@ -0,0 +1,74 @@
<template>
<div class="w-full pt-20">
<div :style="{ height: pdfHeight + 'px' }" class="overflow-hidden m-auto">
<div class="flex items-center justify-center">
<div class="px-12">
<span class="material-icons text-5xl text-black" :class="!canGoPrev ? 'text-opacity-10' : 'cursor-pointer text-opacity-30 hover:text-opacity-90'" @click.stop.prevent="goPrevPage" @mousedown.prevent>arrow_back_ios</span>
</div>
<div :style="{ width: pdfWidth + 'px', height: pdfHeight + 'px' }" class="w-full h-full overflow-auto">
<div v-if="loadedRatio > 0 && loadedRatio < 1" style="background-color: green; color: white; text-align: center" :style="{ width: loadedRatio * 100 + '%' }">{{ Math.floor(loadedRatio * 100) }}%</div>
<pdf ref="pdf" class="m-auto z-10 border border-black border-opacity-20 shadow-md" :src="src" :page="page" :rotate="rotate" @progress="loadedRatio = $event" @error="error" @num-pages="numPagesLoaded" @link-clicked="page = $event"></pdf>
</div>
<div class="px-12">
<span class="material-icons text-5xl text-black" :class="!canGoNext ? 'text-opacity-10' : 'cursor-pointer text-opacity-30 hover:text-opacity-90'" @click.stop.prevent="goNextPage" @mousedown.prevent>arrow_forward_ios</span>
</div>
</div>
</div>
<div class="text-center py-2 text-lg">
<p>{{ page }} / {{ numPages }}</p>
</div>
</div>
</template>
<script>
import pdf from 'vue-pdf'
export default {
components: {
pdf
},
props: {
src: String
},
data() {
return {
rotate: 0,
loadedRatio: 0,
page: 1,
numPages: 0
}
},
computed: {
pdfWidth() {
return this.pdfHeight * 0.6667
},
pdfHeight() {
return window.innerHeight - 120
},
canGoNext() {
return this.page < this.numPages
},
canGoPrev() {
return this.page > 1
}
},
methods: {
numPagesLoaded(e) {
this.numPages = e
},
goPrevPage() {
if (this.page <= 1) return
this.page--
},
goNextPage() {
if (this.page >= this.numPages) return
this.page++
},
error(err) {
console.error(err)
}
},
mounted() {}
}
</script>
+56 -22
View File
@@ -14,7 +14,7 @@
</div> </div>
<!-- EPUB --> <!-- EPUB -->
<div v-if="epubEbook" class="h-full flex items-center"> <div v-if="ebookType === 'epub'" class="h-full flex items-center">
<div style="width: 100px; max-width: 100px" class="h-full flex items-center overflow-x-hidden"> <div style="width: 100px; max-width: 100px" class="h-full flex items-center overflow-x-hidden">
<span v-show="hasPrev" class="material-icons text-black text-opacity-30 hover:text-opacity-80 cursor-pointer text-8xl" @mousedown.prevent @click="pageLeft">chevron_left</span> <span v-show="hasPrev" class="material-icons text-black text-opacity-30 hover:text-opacity-80 cursor-pointer text-8xl" @mousedown.prevent @click="pageLeft">chevron_left</span>
</div> </div>
@@ -30,11 +30,21 @@
</div> </div>
</div> </div>
<!-- MOBI/AZW3 --> <!-- MOBI/AZW3 -->
<div v-else class="h-full max-h-full w-full"> <div v-else-if="ebookType === 'mobi'" class="h-full max-h-full w-full">
<div class="ebook-viewer absolute overflow-y-scroll left-0 right-0 top-12 w-full max-w-4xl m-auto z-10 border border-black border-opacity-20"> <div class="ebook-viewer absolute overflow-y-scroll left-0 right-0 top-12 w-full max-w-4xl m-auto z-10 border border-black border-opacity-20">
<iframe title="html-viewer" width="100%"> Loading </iframe> <iframe title="html-viewer" width="100%"> Loading </iframe>
</div> </div>
</div> </div>
<!-- PDF -->
<div v-else-if="ebookType === 'pdf'" class="h-full flex items-center">
<app-pdf-reader :src="ebookUrl" />
</div>
<!-- COMIC -->
<div v-else-if="ebookType === 'comic'" class="h-full flex items-center">
<app-comic-reader :src="ebookUrl" @close="show = false" />
</div>
<div class="absolute bottom-2 left-2">{{ ebookType }}</div>
</div> </div>
</template> </template>
@@ -55,7 +65,9 @@ export default {
author: '', author: '',
progress: 0, progress: 0,
hasNext: true, hasNext: true,
hasPrev: false hasPrev: false,
ebookType: '',
ebookUrl: ''
} }
}, },
watch: { watch: {
@@ -97,28 +109,26 @@ export default {
epubEbook() { epubEbook() {
return this.ebooks.find((eb) => eb.ext === '.epub') return this.ebooks.find((eb) => eb.ext === '.epub')
}, },
epubPath() {
return this.epubEbook ? this.epubEbook.path : null
},
mobiEbook() { mobiEbook() {
return this.ebooks.find((eb) => eb.ext === '.mobi' || eb.ext === '.azw3') return this.ebooks.find((eb) => eb.ext === '.mobi' || eb.ext === '.azw3')
}, },
mobiPath() { pdfEbook() {
return this.mobiEbook ? this.mobiEbook.path : null return this.ebooks.find((eb) => eb.ext === '.pdf')
}, },
mobiUrl() { comicEbook() {
if (!this.mobiPath) return null return this.ebooks.find((eb) => eb.ext === '.cbz' || eb.ext === '.cbr')
return `/ebook/${this.libraryId}/${this.folderId}/${this.mobiPath}`
},
url() {
if (!this.epubPath) return null
return `/ebook/${this.libraryId}/${this.folderId}/${this.epubPath}`
}, },
userToken() { userToken() {
return this.$store.getters['user/getToken'] return this.$store.getters['user/getToken']
},
selectedAudiobookFile() {
return this.$store.state.selectedAudiobookFile
} }
}, },
methods: { methods: {
getEbookUrl(path) {
return `/ebook/${this.libraryId}/${this.folderId}/${path}`
},
changedChapter() { changedChapter() {
if (this.rendition) { if (this.rendition) {
this.rendition.display(this.selectedChapter) this.rendition.display(this.selectedChapter)
@@ -155,12 +165,33 @@ export default {
document.removeEventListener('keyup', this.keyUp) document.removeEventListener('keyup', this.keyUp)
}, },
init() { init() {
this.registerListeners() if (this.selectedAudiobookFile) {
this.ebookUrl = this.getEbookUrl(this.selectedAudiobookFile.path)
if (this.epubEbook) { if (this.selectedAudiobookFile.ext === '.pdf') {
this.ebookType = 'pdf'
} else if (this.selectedAudiobookFile.ext === '.mobi' || this.selectedAudiobookFile.ext === '.azw3') {
this.ebookType = 'mobi'
this.initMobi()
} else if (this.selectedAudiobookFile.ext === '.epub') {
this.ebookType = 'epub'
this.initEpub()
} else if (this.selectedAudiobookFile.ext === '.cbr' || this.selectedAudiobookFile.ext === '.cbz') {
this.ebookType = 'comic'
}
} else if (this.epubEbook) {
this.ebookType = 'epub'
this.ebookUrl = this.getEbookUrl(this.epubEbook.path)
this.initEpub() this.initEpub()
} else if (this.mobiEbook) { } else if (this.mobiEbook) {
this.ebookType = 'mobi'
this.ebookUrl = this.getEbookUrl(this.mobiEbook.path)
this.initMobi() this.initMobi()
} else if (this.pdfEbook) {
this.ebookType = 'pdf'
this.ebookUrl = this.getEbookUrl(this.pdfEbook.path)
} else if (this.comicEbook) {
this.ebookType = 'comic'
this.ebookUrl = this.getEbookUrl(this.comicEbook.path)
} }
}, },
addHtmlCss() { addHtmlCss() {
@@ -219,7 +250,7 @@ export default {
}, },
async initMobi() { async initMobi() {
// Fetch mobi file as blob // Fetch mobi file as blob
var buff = await this.$axios.$get(this.mobiUrl, { var buff = await this.$axios.$get(this.ebookUrl, {
responseType: 'blob' responseType: 'blob'
}) })
var reader = new FileReader() var reader = new FileReader()
@@ -246,12 +277,13 @@ export default {
reader.readAsArrayBuffer(buff) reader.readAsArrayBuffer(buff)
}, },
initEpub() { initEpub() {
this.registerListeners()
// var book = ePub(this.url, { // var book = ePub(this.url, {
// requestHeaders: { // requestHeaders: {
// Authorization: `Bearer ${this.userToken}` // Authorization: `Bearer ${this.userToken}`
// } // }
// }) // })
var book = ePub(this.url) var book = ePub(this.ebookUrl)
this.book = book this.book = book
this.rendition = book.renderTo('viewer', { this.rendition = book.renderTo('viewer', {
@@ -307,14 +339,16 @@ export default {
}) })
}, },
close() { close() {
this.unregisterListeners() if (this.ebookType === 'epub') {
this.unregisterListeners()
}
} }
}, },
mounted() { mounted() {
if (this.show) this.init() if (this.show) this.init()
}, },
beforeDestroy() { beforeDestroy() {
this.unregisterListeners() this.close()
} }
} }
</script> </script>
@@ -1,9 +1,16 @@
<template> <template>
<div class="flex h-full px-1 overflow-hidden"> <div class="flex h-full px-1 overflow-hidden">
<cards-book-cover :audiobook="audiobook" :width="50" /> <cards-book-cover :audiobook="audiobook" :width="50" />
<div class="flex-grow px-2 searchCardContent h-full"> <div class="flex-grow px-2 audiobookSearchCardContent">
<p class="truncate text-sm">{{ title }}</p> <p v-if="matchKey !== 'title'" class="truncate text-sm">{{ title }}</p>
<p class="text-xs text-gray-200 truncate">by {{ author }}</p> <p v-else class="truncate text-sm" v-html="matchHtml" />
<p v-if="matchKey === 'subtitle'" class="truncate text-xs text-gray-300">{{ matchHtml }}</p>
<p v-if="matchKey !== 'author'" class="text-xs text-gray-200 truncate">by {{ author }}</p>
<p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" />
<div v-if="matchKey === 'series' || matchKey === 'tags'" class="m-0 p-0 truncate" v-html="matchHtml" />
</div> </div>
</div> </div>
</template> </template>
@@ -14,7 +21,10 @@ export default {
audiobook: { audiobook: {
type: Object, type: Object,
default: () => {} default: () => {}
} },
search: String,
matchKey: String,
matchText: String
}, },
data() { data() {
return {} return {}
@@ -26,8 +36,34 @@ export default {
title() { title() {
return this.book ? this.book.title : 'No Title' return this.book ? this.book.title : 'No Title'
}, },
subtitle() {
return this.book ? this.book.subtitle : ''
},
author() { author() {
return this.book ? this.book.author : 'Unknown' return this.book ? this.book.author : 'Unknown'
},
matchHtml() {
if (!this.matchText || !this.search) return ''
if (this.matchKey === 'subtitle') return ''
var matchSplit = this.matchText.toLowerCase().split(this.search.toLowerCase().trim())
if (matchSplit.length < 2) return ''
var html = ''
var totalLenSoFar = 0
for (let i = 0; i < matchSplit.length - 1; i++) {
var indexOf = matchSplit[i].length
var firstPart = this.matchText.substr(totalLenSoFar, indexOf)
var actualWasThere = this.matchText.substr(totalLenSoFar + indexOf, this.search.length)
totalLenSoFar += indexOf + this.search.length
html += `${firstPart}<strong class="text-warning">${actualWasThere}</strong>`
}
var lastPart = this.matchText.substr(totalLenSoFar)
html += lastPart
if (this.matchKey === 'tags') return `<p class="truncate">Tags: ${html}</p>`
if (this.matchKey === 'author') return `by ${html}`
return `${html}`
} }
}, },
methods: {}, methods: {},
@@ -36,9 +72,9 @@ export default {
</script> </script>
<style> <style>
.searchCardContent { .audiobookSearchCardContent {
width: calc(100% - 80px); width: calc(100% - 80px);
height: calc(50px * 1.5); height: 75px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
+2 -2
View File
@@ -1,6 +1,6 @@
<template> <template>
<div class="flex h-full px-1 overflow-hidden"> <div class="flex h-full px-1 overflow-hidden">
<img src="https://rpgplanner.com/wp-content/uploads/2020/06/no-photo-available.png" class="w-40 h-40 max-h-40 object-contain" style="max-height: 40px; max-width: 40px" /> <img src="/icons/NoUserPhoto.png" class="w-40 h-40 max-h-40 object-contain" style="max-height: 40px; max-width: 40px" />
<div class="flex-grow px-2 authorSearchCardContent h-full"> <div class="flex-grow px-2 authorSearchCardContent h-full">
<p class="truncate text-sm">{{ author }}</p> <p class="truncate text-sm">{{ author }}</p>
</div> </div>
@@ -22,7 +22,7 @@ export default {
</script> </script>
<style> <style>
.searchCardContent { .authorSearchCardContent {
width: calc(100% - 80px); width: calc(100% - 80px);
height: 40px; height: 40px;
display: flex; display: flex;
+2 -1
View File
@@ -228,7 +228,8 @@ export default {
this.$root.socket.emit('open_stream', this.audiobookId) this.$root.socket.emit('open_stream', this.audiobookId)
}, },
editClick() { editClick() {
this.$store.commit('showEditModal', this.audiobook) // this.$store.commit('showEditModal', this.audiobook)
this.$emit('edit', this.audiobook)
}, },
clickCard(e) { clickCard(e) {
if (this.isSelectionMode) { if (this.isSelectionMode) {
+11 -1
View File
@@ -1,7 +1,7 @@
<template> <template>
<div class="relative"> <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"> <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/${currentLibraryId}/bookshelf/series?${groupType}=${groupEncode}`" class="cursor-pointer"> <nuxt-link :to="groupTo" class="cursor-pointer">
<div class="w-full relative" :class="isHovering ? 'bg-black-400' : 'bg-primary'" :style="{ height: height + 'px', width: height + 'px' }"> <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" /> <cards-group-cover ref="groupcover" :name="groupName" :book-items="bookItems" :width="height" :height="height" />
@@ -54,6 +54,16 @@ export default {
_group() { _group() {
return this.group || {} return this.group || {}
}, },
groupType() {
return this._group.type
},
groupTo() {
if (this.groupType === 'series') {
return `/library/${this.currentLibraryId}/bookshelf/series?series=${this.groupEncode}`
} else {
return `/library/${this.currentLibraryId}/bookshelf?filter=tags.${this.groupEncode}`
}
},
height() { height() {
return this.width * 1.6 return this.width * 1.6
}, },
+34
View File
@@ -0,0 +1,34 @@
<template>
<div class="flex h-full px-1 overflow-hidden">
<div class="w-10 h-10 flex items-center justify-center">
<span class="material-icons text-2xl text-gray-200">local_offer</span>
</div>
<div class="flex-grow px-2 tagSearchCardContent h-full">
<p class="truncate text-sm">{{ tag }}</p>
</div>
</div>
</template>
<script>
export default {
props: {
tag: String
},
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>
<style>
.tagSearchCardContent {
width: calc(100% - 40px);
height: 40px;
display: flex;
flex-direction: column;
justify-content: center;
}
</style>
+32 -13
View File
@@ -23,7 +23,7 @@
<template v-for="item in audiobookResults"> <template v-for="item in audiobookResults">
<li :key="item.audiobook.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option"> <li :key="item.audiobook.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option">
<nuxt-link :to="`/audiobook/${item.audiobook.id}`"> <nuxt-link :to="`/audiobook/${item.audiobook.id}`">
<cards-audiobook-search-card :audiobook="item.audiobook" /> <cards-audiobook-search-card :audiobook="item.audiobook" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" />
</nuxt-link> </nuxt-link>
</li> </li>
</template> </template>
@@ -39,12 +39,21 @@
<p v-if="seriesResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Series</p> <p v-if="seriesResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Series</p>
<template v-for="item in seriesResults"> <template v-for="item in seriesResults">
<li :key="item.series" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickedOption(item.series)"> <li :key="item.series" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option">
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf/series?series=${$encode(item.series)}`"> <nuxt-link :to="`/library/${currentLibraryId}/bookshelf/series?series=${$encode(item.series)}`">
<cards-series-search-card :series="item.series" :book-items="item.audiobooks" /> <cards-series-search-card :series="item.series" :book-items="item.audiobooks" />
</nuxt-link> </nuxt-link>
</li> </li>
</template> </template>
<p v-if="tagResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Tags</p>
<template v-for="item in tagResults">
<li :key="item.tag" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option">
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(item.tag)}`">
<cards-tag-search-card :tag="item.tag" />
</nuxt-link>
</li>
</template>
</template> </template>
</ul> </ul>
</div> </div>
@@ -64,6 +73,7 @@ export default {
audiobookResults: [], audiobookResults: [],
authorResults: [], authorResults: [],
seriesResults: [], seriesResults: [],
tagResults: [],
searchTimeout: null, searchTimeout: null,
lastSearch: null lastSearch: null
} }
@@ -76,19 +86,27 @@ export default {
return this.$store.state.libraries.currentLibraryId return this.$store.state.libraries.currentLibraryId
}, },
totalResults() { totalResults() {
return this.audiobookResults.length + this.seriesResults.length + this.authorResults.length return this.audiobookResults.length + this.seriesResults.length + this.authorResults.length + this.tagResults.length
} }
}, },
methods: { methods: {
submitSearch() { submitSearch() {
if (!this.search) return if (!this.search) return
this.$router.push(`/library/${this.currentLibraryId}/bookshelf/search?query=${this.search}`) var search = this.search
this.clearResults()
this.$router.push(`/library/${this.currentLibraryId}/bookshelf/search?query=${search}`)
},
clearResults() {
this.search = null this.search = null
this.lastSearch = null
this.audiobookResults = [] this.audiobookResults = []
this.authorResults = [] this.authorResults = []
this.seriesResults = [] this.seriesResults = []
this.tagResults = []
this.showMenu = false this.showMenu = false
this.isFetching = false
this.isTyping = false
clearTimeout(this.searchTimeout)
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.input) { if (this.$refs.input) {
this.$refs.input.blur() this.$refs.input.blur()
@@ -117,9 +135,14 @@ export default {
console.error('Search error', error) console.error('Search error', error)
return [] return []
}) })
// Search was canceled
if (!this.isFetching) return
this.audiobookResults = searchResults.audiobooks || [] this.audiobookResults = searchResults.audiobooks || []
this.authorResults = searchResults.authors || [] this.authorResults = searchResults.authors || []
this.seriesResults = searchResults.series || [] this.seriesResults = searchResults.series || []
this.tagResults = searchResults.tags || []
this.isFetching = false this.isFetching = false
if (!this.showMenu) { if (!this.showMenu) {
@@ -135,19 +158,15 @@ export default {
} }
this.isTyping = true this.isTyping = true
this.searchTimeout = setTimeout(() => { this.searchTimeout = setTimeout(() => {
// Canceled search
if (!this.isTyping) return
this.isTyping = false this.isTyping = false
this.runSearch(val) this.runSearch(val)
}, 750) }, 750)
}, },
clickClear() { clickClear() {
if (this.search) { this.clearResults()
this.search = null
this.lastSearch = null
this.audiobookResults = []
this.authorResults = []
this.seriesResults = []
this.showMenu = false
}
} }
}, },
mounted() {} mounted() {}
+52
View File
@@ -10,6 +10,14 @@
<div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-1 cursor-pointer hover:bg-bg font-book border-t border-l border-r border-black-300 tab" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div> <div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-1 cursor-pointer hover:bg-bg font-book border-t border-l border-r border-black-300 tab" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div>
</template> </template>
</div> </div>
<div v-show="canGoPrev" class="absolute -left-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
<div class="material-icons text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goPrevBook" @mousedown.prevent>arrow_back_ios</div>
</div>
<div v-show="canGoNext" class="absolute -right-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
<div class="material-icons text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goNextBook" @mousedown.prevent>arrow_forward_ios</div>
</div>
<div class="w-full h-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300"> <div class="w-full h-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300">
<keep-alive> <keep-alive>
<component v-if="audiobook" :is="tabName" :audiobook="audiobook" :processing.sync="processing" @close="show = false" /> <component v-if="audiobook" :is="tabName" :audiobook="audiobook" :processing.sync="processing" @close="show = false" />
@@ -51,6 +59,11 @@ export default {
title: 'Chapters', title: 'Chapters',
component: 'modals-edit-tabs-chapters' component: 'modals-edit-tabs-chapters'
}, },
{
id: 'files',
title: 'Files',
component: 'modals-edit-tabs-files'
},
{ {
id: 'download', id: 'download',
title: 'Download', title: 'Download',
@@ -68,6 +81,7 @@ export default {
this.show = false this.show = false
return return
} }
if (!availableTabIds.includes(this.selectedTab)) { if (!availableTabIds.includes(this.selectedTab)) {
this.selectedTab = availableTabIds[0] this.selectedTab = availableTabIds[0]
} }
@@ -137,9 +151,44 @@ export default {
}, },
title() { title() {
return this.book.title || 'No Title' return this.book.title || 'No Title'
},
bookshelfBookIds() {
return this.$store.state.bookshelfBookIds || []
},
currentBookshelfIndex() {
if (!this.bookshelfBookIds.length) return 0
return this.bookshelfBookIds.findIndex((bid) => bid === this.selectedAudiobookId)
},
canGoPrev() {
return this.bookshelfBookIds.length && this.currentBookshelfIndex > 0
},
canGoNext() {
return this.bookshelfBookIds.length && this.currentBookshelfIndex < this.bookshelfBookIds.length - 1
} }
}, },
methods: { methods: {
goPrevBook() {
if (this.currentBookshelfIndex - 1 < 0) return
var prevBookId = this.bookshelfBookIds[this.currentBookshelfIndex - 1]
var prevBook = this.$store.getters['audiobooks/getAudiobook'](prevBookId)
if (prevBook) {
this.$store.commit('showEditModalOnTab', { audiobook: prevBook, tab: this.selectedTab })
this.$nextTick(this.init)
} else {
console.error('Book not found', prevBookId)
}
},
goNextBook() {
if (this.currentBookshelfIndex >= this.bookshelfBookIds.length) return
var nextBookId = this.bookshelfBookIds[this.currentBookshelfIndex + 1]
var nextBook = this.$store.getters['audiobooks/getAudiobook'](nextBookId)
if (nextBook) {
this.$store.commit('showEditModalOnTab', { audiobook: nextBook, tab: this.selectedTab })
this.$nextTick(this.init)
} else {
console.error('Book not found', nextBookId)
}
},
selectTab(tab) { selectTab(tab) {
this.selectedTab = tab this.selectedTab = tab
}, },
@@ -155,9 +204,12 @@ export default {
}, },
async fetchFull() { async fetchFull() {
try { try {
this.processing = true
this.audiobook = await this.$axios.$get(`/api/audiobook/${this.selectedAudiobookId}`) this.audiobook = await this.$axios.$get(`/api/audiobook/${this.selectedAudiobookId}`)
this.processing = false
} catch (error) { } catch (error) {
console.error('Failed to fetch audiobook', this.selectedAudiobookId, error) console.error('Failed to fetch audiobook', this.selectedAudiobookId, error)
this.processing = false
this.show = false this.show = false
} }
} }
+20 -6
View File
@@ -31,7 +31,7 @@
<div v-if="showLocalCovers" class="flex items-center justify-center"> <div v-if="showLocalCovers" class="flex items-center justify-center">
<template v-for="cover in localCovers"> <template v-for="cover in localCovers">
<div :key="cover.path" class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.localPath === imageUrl ? 'border-yellow-300' : ''" @click="setCover(cover.localPath)"> <div :key="cover.path" class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.localPath === imageUrl ? 'border-yellow-300' : ''" @click="setCover(cover)">
<div class="h-24 bg-primary" style="width: 60px"> <div class="h-24 bg-primary" style="width: 60px">
<img :src="`${cover.localPath}?token=${userToken}`" class="h-full w-full object-contain" /> <img :src="`${cover.localPath}?token=${userToken}`" class="h-full w-full object-contain" />
</div> </div>
@@ -56,7 +56,7 @@
<div v-if="hasSearched" class="flex items-center flex-wrap justify-center max-h-80 overflow-y-scroll mt-2 max-w-full"> <div v-if="hasSearched" class="flex items-center flex-wrap justify-center max-h-80 overflow-y-scroll mt-2 max-w-full">
<p v-if="!coversFound.length">No Covers Found</p> <p v-if="!coversFound.length">No Covers Found</p>
<template v-for="cover in coversFound"> <template v-for="cover in coversFound">
<div :key="cover" class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === imageUrl ? 'border-yellow-300' : ''" @click="setCover(cover)"> <div :key="cover" class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === imageUrl ? 'border-yellow-300' : ''" @click="updateCover(cover)">
<cards-preview-cover :src="cover" :width="80" show-open-new-tab /> <cards-preview-cover :src="cover" :width="80" show-open-new-tab />
</div> </div>
</template> </template>
@@ -79,8 +79,6 @@
</template> </template>
<script> <script>
import Path from 'path'
export default { export default {
props: { props: {
processing: Boolean, processing: Boolean,
@@ -265,8 +263,24 @@ export default {
this.isProcessing = false this.isProcessing = false
this.hasSearched = true this.hasSearched = true
}, },
setCover(cover) { setCover(coverFile) {
this.updateCover(cover) this.isProcessing = true
this.$axios
.$patch(`/api/audiobook/${this.audiobook.id}/coverfile`, coverFile)
.then((data) => {
console.log('response data', data)
if (data && typeof data === 'string') {
this.$toast.success(data)
}
this.isProcessing = false
})
.catch((error) => {
console.error('Failed to update', error)
if (error.response && error.response.data) {
this.$toast.error(error.response.data)
}
this.isProcessing = false
})
} }
} }
} }
@@ -214,8 +214,6 @@ export default {
this.details.volumeNumber = this.book.volumeNumber this.details.volumeNumber = this.book.volumeNumber
this.details.publishYear = this.book.publishYear this.details.publishYear = this.book.publishYear
console.log('INIT', this.details)
this.newTags = this.audiobook.tags || [] this.newTags = this.audiobook.tags || []
}, },
resetProgress() { resetProgress() {
@@ -0,0 +1,21 @@
<template>
<div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6">
<tables-all-files-table :audiobook="audiobook" />
</div>
</template>
<script>
export default {
props: {
audiobook: {
type: Object,
default: () => {}
}
},
data() {
return {}
},
computed: {},
methods: {}
}
</script>
@@ -53,7 +53,6 @@ export default {
data() { data() {
return { return {
tracks: null, tracks: null,
audioFiles: null,
showFullPath: false showFullPath: false
} }
}, },
@@ -104,7 +103,6 @@ export default {
}, },
methods: { methods: {
init() { init() {
this.audioFiles = this.audiobook.audioFiles
this.tracks = this.audiobook.tracks this.tracks = this.audiobook.tracks
} }
} }
+109
View File
@@ -0,0 +1,109 @@
<template>
<div class="w-full my-2">
<div class="w-full bg-primary px-6 py-2 flex items-center cursor-pointer">
<p class="pr-4">Files</p>
<span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ allFiles.length }}</span>
<div class="flex-grow" />
<ui-btn small :color="showFullPath ? 'gray-600' : 'primary'" @click.stop="showFullPath = !showFullPath">Full Path</ui-btn>
</div>
<div class="w-full">
<table class="text-sm tracksTable">
<tr class="font-book">
<th class="text-left px-4">Path</th>
<th class="text-left px-4 w-24">Filetype</th>
<th v-if="userCanDownload" class="text-center w-20">Download</th>
</tr>
<template v-for="file in allFiles">
<tr :key="file.path">
<td class="font-book pl-2">
{{ showFullPath ? file.fullPath : file.path }}
</td>
<td class="text-xs">
<p>{{ file.filetype }}</p>
</td>
<td v-if="userCanDownload" class="text-center">
<a :href="`/s/book/${audiobookId}/${file.relativePath}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a>
</td>
</tr>
</template>
</table>
</div>
</div>
</template>
<script>
export default {
props: {
audiobook: {
type: Object,
default: () => {}
}
},
data() {
return {
showFullPath: false
}
},
computed: {
audiobookId() {
return this.audiobook.id
},
audiobookPath() {
return this.audiobook.path
},
userCanDownload() {
return this.$store.getters['user/getUserCanDownload']
},
userToken() {
return this.$store.getters['user/getToken']
},
isMissing() {
return this.audiobook.isMissing
},
showDownload() {
return this.userCanDownload && !this.isMissing
},
otherFiles() {
return this.audiobook.otherFiles || []
},
audioFiles() {
return this.audiobook.audioFiles || []
},
audioFilesCleaned() {
return this.audioFiles.map((af) => {
return {
path: af.path,
fullPath: af.fullPath,
relativePath: this.getRelativePath(af.path),
filetype: 'audio'
}
})
},
otherFilesCleaned() {
return this.otherFiles.map((af) => {
return {
path: af.path,
fullPath: af.fullPath,
relativePath: this.getRelativePath(af.path),
filetype: af.filetype
}
})
},
allFiles() {
return this.audioFilesCleaned.concat(this.otherFilesCleaned)
}
},
methods: {
getRelativePath(path) {
var filePath = path.replace(/\\/g, '/')
var audiobookPath = this.audiobookPath.replace(/\\/g, '/')
return filePath
.replace(audiobookPath + '/', '')
.replace(/%/g, '%25')
.replace(/#/g, '%23')
}
},
mounted() {}
}
</script>
+12 -3
View File
@@ -20,7 +20,7 @@
<tr class="font-book"> <tr class="font-book">
<th class="text-left px-4">Path</th> <th class="text-left px-4">Path</th>
<th class="text-left px-4 w-24">Filetype</th> <th class="text-left px-4 w-24">Filetype</th>
<th v-if="userCanDownload" class="text-center w-20">Download</th> <th v-if="userCanDownload && !isMissing" class="text-center w-20">Download</th>
</tr> </tr>
<template v-for="file in otherFilesCleaned"> <template v-for="file in otherFilesCleaned">
<tr :key="file.path"> <tr :key="file.path">
@@ -28,9 +28,12 @@
{{ showFullPath ? file.fullPath : file.path }} {{ showFullPath ? file.fullPath : file.path }}
</td> </td>
<td class="text-xs"> <td class="text-xs">
<p>{{ file.filetype }}</p> <div class="flex items-center">
<span v-if="file.filetype === 'ebook'" class="material-icons text-base mr-1 cursor-pointer text-white text-opacity-60 hover:text-opacity-100" @click="readEbookClick(file)">auto_stories </span>
<p>{{ file.filetype }}</p>
</div>
</td> </td>
<td v-if="userCanDownload" class="text-center"> <td v-if="userCanDownload && !isMissing" class="text-center">
<a :href="`/s/book/${audiobookId}/${file.relativePath}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a> <a :href="`/s/book/${audiobookId}/${file.relativePath}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a>
</td> </td>
</tr> </tr>
@@ -83,11 +86,17 @@ export default {
userToken() { userToken() {
return this.$store.getters['user/getToken'] return this.$store.getters['user/getToken']
}, },
isMissing() {
return this.audiobook.isMissing
},
userCanDownload() { userCanDownload() {
return this.$store.getters['user/getUserCanDownload'] return this.$store.getters['user/getUserCanDownload']
} }
}, },
methods: { methods: {
readEbookClick(file) {
this.$store.commit('showEReaderForFile', { audiobook: this.audiobook, file })
},
clickBar() { clickBar() {
this.showFiles = !this.showFiles this.showFiles = !this.showFiles
} }
-3
View File
@@ -116,9 +116,6 @@ export default {
this.textInput = null this.textInput = null
this.currentSearch = null this.currentSearch = null
this.input = item 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()
} }
}, },
+4
View File
@@ -127,6 +127,7 @@ export default {
return return
} }
this.isFocused = false this.isFocused = false
if (this.textInput) this.submitForm()
}, 50) }, 50)
}, },
focus() { focus() {
@@ -145,6 +146,7 @@ export default {
var newSelected = null var newSelected = null
if (this.selected.includes(itemValue)) { if (this.selected.includes(itemValue)) {
newSelected = this.selected.filter((s) => s !== itemValue) newSelected = this.selected.filter((s) => s !== itemValue)
this.$emit('removedItem', itemValue)
} else { } else {
newSelected = this.selected.concat([itemValue]) newSelected = this.selected.concat([itemValue])
} }
@@ -164,6 +166,7 @@ export default {
removeItem(item) { removeItem(item) {
var remaining = this.selected.filter((i) => i !== item) var remaining = this.selected.filter((i) => i !== item)
this.$emit('input', remaining) this.$emit('input', remaining)
this.$emit('removedItem', item)
this.$nextTick(() => { this.$nextTick(() => {
this.recalcMenuPos() this.recalcMenuPos()
}) })
@@ -171,6 +174,7 @@ export default {
insertNewItem(item) { insertNewItem(item) {
this.selected.push(item) this.selected.push(item)
this.$emit('input', this.selected) this.$emit('input', this.selected)
this.$emit('newItem', item)
this.textInput = null this.textInput = null
this.currentSearch = null this.currentSearch = null
this.$nextTick(() => { this.$nextTick(() => {
+1 -1
View File
@@ -1,7 +1,7 @@
<template> <template>
<button class="icon-btn rounded-md bg-primary border border-gray-600 flex items-center justify-center h-9 w-9 relative" @click="clickBtn"> <button class="icon-btn rounded-md bg-primary border border-gray-600 flex items-center justify-center h-9 w-9 relative" @click="clickBtn">
<div class="w-5 h-5 text-white relative"> <div class="w-5 h-5 text-white relative">
<svg v-if="isRead" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> <svg v-if="isRead" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)">
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" /> <path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" />
</svg> </svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> <svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
+4 -5
View File
@@ -19,7 +19,7 @@ module.exports = {
// Global page headers: https://go.nuxtjs.dev/config-head // Global page headers: https://go.nuxtjs.dev/config-head
head: { head: {
title: 'AudioBookshelf', title: 'Audiobookshelf',
htmlAttrs: { htmlAttrs: {
lang: 'en' lang: 'en'
}, },
@@ -35,8 +35,8 @@ module.exports = {
], ],
link: [ link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }, { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Fira+Mono&family=Ubuntu+Mono&family=Gentium+Book+Basic&&family=Source+Sans+Pro:wght@300;400;600' }, { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Ubuntu+Mono&family=Source+Sans+Pro:wght@300;400;600' },
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/icon?family=Material+Icons' } // { rel: 'stylesheet', href: 'https://fonts.googleapis.com/icon?family=Material+Icons' }
] ]
}, },
@@ -98,8 +98,7 @@ module.exports = {
}, },
// Build Configuration: https://go.nuxtjs.dev/config-build // Build Configuration: https://go.nuxtjs.dev/config-build
build: { build: {},
},
watchers: { watchers: {
webpack: { webpack: {
aggregateTimeout: 300, aggregateTimeout: 300,
+85 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "1.4.6", "version": "1.4.10",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -3415,6 +3415,11 @@
"@babel/helper-define-polyfill-provider": "^0.2.2" "@babel/helper-define-polyfill-provider": "^0.2.2"
} }
}, },
"babel-plugin-syntax-dynamic-import": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz",
"integrity": "sha1-jWomIpyDdFqZgqRBBRVyyqF5sdo="
},
"backo2": { "backo2": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
@@ -7380,6 +7385,11 @@
"launch-editor": "^2.2.1" "launch-editor": "^2.2.1"
} }
}, },
"libarchive.js": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/libarchive.js/-/libarchive.js-1.3.0.tgz",
"integrity": "sha512-EkQfRXt9DhWwj6BnEA2TNpOf4jTnzSTUPGgE+iFxcdNqjktY8GitbDeHnx8qZA0/IukNyyBUR3oQKRdYkO+HFg=="
},
"lie": { "lie": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
@@ -8494,6 +8504,11 @@
"sha.js": "^2.4.8" "sha.js": "^2.4.8"
} }
}, },
"pdfjs-dist": {
"version": "2.6.347",
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.6.347.tgz",
"integrity": "sha512-QC+h7hG2su9v/nU1wEI3SnpPIrqJODL7GTDFvR74ANKGq1AFJW16PH8VWnhpiTi9YcLSFV9xLeWSgq+ckHLdVQ=="
},
"picomatch": { "picomatch": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
@@ -11239,6 +11254,37 @@
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
}, },
"raw-loader": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz",
"integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==",
"requires": {
"loader-utils": "^2.0.0",
"schema-utils": "^3.0.0"
},
"dependencies": {
"loader-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"schema-utils": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
"integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==",
"requires": {
"@types/json-schema": "^7.0.8",
"ajv": "^6.12.5",
"ajv-keywords": "^3.5.2"
}
}
}
},
"rc9": { "rc9": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/rc9/-/rc9-1.2.0.tgz", "resolved": "https://registry.npmjs.org/rc9/-/rc9-1.2.0.tgz",
@@ -13314,6 +13360,24 @@
"resolved": "https://registry.npmjs.org/vue-no-ssr/-/vue-no-ssr-1.1.1.tgz", "resolved": "https://registry.npmjs.org/vue-no-ssr/-/vue-no-ssr-1.1.1.tgz",
"integrity": "sha512-ZMjqRpWabMPqPc7gIrG0Nw6vRf1+itwf0Itft7LbMXs2g3Zs/NFmevjZGN1x7K3Q95GmIjWbQZTVerxiBxI+0g==" "integrity": "sha512-ZMjqRpWabMPqPc7gIrG0Nw6vRf1+itwf0Itft7LbMXs2g3Zs/NFmevjZGN1x7K3Q95GmIjWbQZTVerxiBxI+0g=="
}, },
"vue-pdf": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/vue-pdf/-/vue-pdf-4.3.0.tgz",
"integrity": "sha512-zd3lJj6CbtrawgaaDDciTDjkJMUKiLWtbEmBg5CvFn9Noe9oAO/GNy/fc5c59qGuFCJ14ibIV1baw4S07e5bSQ==",
"requires": {
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"loader-utils": "^1.4.0",
"pdfjs-dist": "2.6.347",
"raw-loader": "^4.0.2",
"vue-resize-sensor": "^2.0.0",
"worker-loader": "^2.0.0"
}
},
"vue-resize-sensor": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/vue-resize-sensor/-/vue-resize-sensor-2.0.0.tgz",
"integrity": "sha512-W+y2EAI/BxS4Vlcca9scQv8ifeBFck56DRtSwWJ2H4Cw1GLNUYxiZxUHHkuzuI5JPW/cYtL1bPO5xPyEXx4LmQ=="
},
"vue-router": { "vue-router": {
"version": "3.5.2", "version": "3.5.2",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.5.2.tgz", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.5.2.tgz",
@@ -14181,6 +14245,26 @@
"errno": "~0.1.7" "errno": "~0.1.7"
} }
}, },
"worker-loader": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-2.0.0.tgz",
"integrity": "sha512-tnvNp4K3KQOpfRnD20m8xltE3eWh89Ye+5oj7wXEEHKac1P4oZ6p9oTj8/8ExqoSBnk9nu5Pr4nKfQ1hn2APJw==",
"requires": {
"loader-utils": "^1.0.0",
"schema-utils": "^0.4.0"
},
"dependencies": {
"schema-utils": {
"version": "0.4.7",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz",
"integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==",
"requires": {
"ajv": "^6.1.0",
"ajv-keywords": "^3.1.0"
}
}
}
},
"wrap-ansi": { "wrap-ansi": {
"version": "6.2.0", "version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+3 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "1.4.7", "version": "1.4.11",
"description": "Audiobook manager and player", "description": "Audiobook manager and player",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@@ -19,8 +19,10 @@
"date-fns": "^2.25.0", "date-fns": "^2.25.0",
"epubjs": "^0.3.88", "epubjs": "^0.3.88",
"hls.js": "^1.0.7", "hls.js": "^1.0.7",
"libarchive.js": "^1.3.0",
"nuxt": "^2.15.7", "nuxt": "^2.15.7",
"nuxt-socket-io": "^1.1.18", "nuxt-socket-io": "^1.1.18",
"vue-pdf": "^4.3.0",
"vue-toastification": "^1.7.11", "vue-toastification": "^1.7.11",
"vuedraggable": "^2.24.3" "vuedraggable": "^2.24.3"
}, },
+42 -1
View File
@@ -56,6 +56,28 @@
</li> </li>
</transition-group> </transition-group>
</draggable> </draggable>
<div v-if="showExperimentalFeatures" class="p-4">
<ui-btn :loading="checkingTrackNumbers" small @click="checkTrackNumbers">Check Track Numbers</ui-btn>
<div v-if="trackNumData && trackNumData.length" class="w-full max-w-4xl py-2">
<table class="tracksTable">
<tr>
<th class="text-left">Filename</th>
<th class="w-32">Index</th>
<th class="w-32"># From Metadata</th>
<th class="w-32"># From Filename</th>
<th class="w-32"># From Probe</th>
</tr>
<tr v-for="trackData in trackNumData" :key="trackData.filename">
<td class="text-xs">{{ trackData.filename }}</td>
<td class="text-center">{{ trackData.currentTrackNum }}</td>
<td class="text-center">{{ trackData.trackNumFromMeta }}</td>
<td class="text-center">{{ trackData.trackNumFromFilename }}</td>
<td class="text-center">{{ trackData.scanDataTrackNum }}</td>
</tr>
</table>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -95,7 +117,9 @@ export default {
group: 'description', group: 'description',
ghostClass: 'ghost' ghostClass: 'ghost'
}, },
saving: false saving: false,
checkingTrackNumbers: false,
trackNumData: []
} }
}, },
computed: { computed: {
@@ -172,9 +196,26 @@ export default {
}, },
streamAudiobook() { streamAudiobook() {
return this.$store.state.streamAudiobook return this.$store.state.streamAudiobook
},
showExperimentalFeatures() {
return this.$store.state.showExperimentalFeatures
} }
}, },
methods: { methods: {
checkTrackNumbers() {
this.checkingTrackNumbers = true
this.$axios
.$get(`/api/scantracks/${this.audiobookId}`)
.then((res) => {
console.log('RES', res)
this.trackNumData = res
this.checkingTrackNumbers = false
})
.catch((error) => {
console.error('Failed', error)
this.checkingTrackNumbers = false
})
},
includeToggled(audio) { includeToggled(audio) {
var new_index = 0 var new_index = 0
if (audio.include) { if (audio.include) {
+6 -7
View File
@@ -104,7 +104,7 @@
{{ isMissing ? 'Missing' : 'Incomplete' }} {{ isMissing ? 'Missing' : 'Incomplete' }}
</ui-btn> </ui-btn>
<ui-btn v-if="showExperimentalFeatures && (epubEbook || mobiEbook)" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook"> <ui-btn v-if="showExperimentalFeatures && numEbooks" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook">
<span class="material-icons -ml-2 pr-2 text-white">auto_stories</span> <span class="material-icons -ml-2 pr-2 text-white">auto_stories</span>
Read Read
</ui-btn> </ui-btn>
@@ -322,16 +322,14 @@ export default {
return this.audiobook.ebooks return this.audiobook.ebooks
}, },
showEpubAlert() { showEpubAlert() {
return this.ebooks.length && !this.epubEbook && !this.tracks.length return this.ebooks.length && !this.numEbooks && !this.tracks.length
}, },
showExperimentalReadAlert() { showExperimentalReadAlert() {
return !this.tracks.length && this.ebooks.length && !this.showExperimentalFeatures return !this.tracks.length && this.ebooks.length && !this.showExperimentalFeatures
}, },
epubEbook() { numEbooks() {
return this.ebooks.find((eb) => eb.ext === '.epub') // Number of currently supported for reading ebooks, not all ebooks
}, return this.audiobook.numEbooks
mobiEbook() {
return this.ebooks.find((eb) => eb.ext === '.mobi' || eb.ext === '.azw3')
}, },
userToken() { userToken() {
return this.$store.getters['user/getToken'] return this.$store.getters['user/getToken']
@@ -411,6 +409,7 @@ export default {
this.$root.socket.emit('open_stream', this.audiobook.id) this.$root.socket.emit('open_stream', this.audiobook.id)
}, },
editClick() { editClick() {
this.$store.commit('setBookshelfBookIds', [])
this.$store.commit('showEditModal', this.audiobook) this.$store.commit('showEditModal', this.audiobook)
}, },
lookupMetadata(index) { lookupMetadata(index) {
+46 -5
View File
@@ -33,10 +33,10 @@
<div class="flex mt-2 -mx-1"> <div class="flex mt-2 -mx-1">
<div class="w-1/2 px-1"> <div class="w-1/2 px-1">
<ui-multi-select v-model="audiobook.book.genres" label="Genres" :items="genres" /> <ui-multi-select v-model="audiobook.book.genres" label="Genres" :items="genreItems" @newItem="newGenreItem" @removedItem="removedGenreItem" />
</div> </div>
<div class="flex-grow px-1"> <div class="flex-grow px-1">
<ui-multi-select v-model="audiobook.tags" label="Tags" :items="tags" /> <ui-multi-select v-model="audiobook.tags" label="Tags" :items="tagItems" @newItem="newTagItem" @removedItem="removedTagItem" />
</div> </div>
</div> </div>
@@ -76,7 +76,9 @@ export default {
isProcessing: false, isProcessing: false,
audiobookCopies: [], audiobookCopies: [],
isScrollable: false, isScrollable: false,
newSeriesItems: [] newSeriesItems: [],
newTagItems: [],
newGenreItems: []
} }
}, },
computed: { computed: {
@@ -86,9 +88,15 @@ export default {
genres() { genres() {
return this.$store.state.audiobooks.genres return this.$store.state.audiobooks.genres
}, },
genreItems() {
return this.genres.concat(this.newGenreItems)
},
tags() { tags() {
return this.$store.state.audiobooks.tags return this.$store.state.audiobooks.tags
}, },
tagItems() {
return this.tags.concat(this.newTagItems)
},
series() { series() {
return this.$store.state.audiobooks.series return this.$store.state.audiobooks.series
}, },
@@ -100,9 +108,42 @@ export default {
} }
}, },
methods: { methods: {
newTagItem(item) {
if (item && !this.newTagItems.includes(item)) {
this.newTagItems.push(item)
}
},
removedTagItem(item) {
// If newly added, remove if not used on any other audiobooks
if (item && this.newTagItems.includes(item)) {
var usedByOtherAb = this.audiobookCopies.find((ab) => {
return ab.tags && ab.tags.includes(item)
})
if (!usedByOtherAb) {
this.newTagItems = this.newTagItems.filter((t) => t !== item)
}
}
},
newGenreItem(item) {
if (item && !this.newGenreItems.includes(item)) {
this.newGenreItems.push(item)
}
},
removedGenreItem(item) {
// If newly added, remove if not used on any other audiobooks
if (item && this.newGenreItems.includes(item)) {
var usedByOtherAb = this.audiobookCopies.find((ab) => {
return ab.book.genres && ab.book.genres.includes(item)
})
if (!usedByOtherAb) {
this.newGenreItems = this.newGenreItems.filter((t) => t !== item)
}
}
},
newSeriesItem(item) { newSeriesItem(item) {
if (!item) return if (item && !this.newSeriesItems.includes(item)) {
this.newSeriesItems.push(item) this.newSeriesItems.push(item)
}
}, },
seriesChanged() { seriesChanged() {
this.newSeriesItems = this.newSeriesItems.filter((item) => { this.newSeriesItems = this.newSeriesItems.filter((item) => {
+1 -3
View File
@@ -21,9 +21,7 @@
<script> <script>
export default { export default {
asyncData({ redirect, store }) { asyncData({ redirect, store }) {
var currentLibraryId = store.state.libraries.currentLibraryId redirect(`/library/${store.state.libraries.currentLibraryId}`)
console.log('Redir', currentLibraryId)
redirect(`/library/${currentLibraryId}`)
}, },
data() { data() {
return {} return {}
@@ -19,26 +19,37 @@ export default {
return redirect('/oops?message=Library not found') return redirect('/oops?message=Library not found')
} }
// Set filter by
if (query.filter) { if (query.filter) {
store.dispatch('user/updateUserSettings', { filterBy: query.filter }) store.dispatch('user/updateUserSettings', { filterBy: query.filter })
} }
var searchResults = []
// Search page
var searchResults = {}
var audiobookSearchResults = []
var searchQuery = null var searchQuery = null
if (params.id === 'search' && query.query) { if (params.id === 'search' && query.query) {
searchQuery = query.query searchQuery = query.query
searchResults = await app.$axios.$get(`/api/library/${libraryId}/audiobooks?q=${query.query}`).catch((error) => {
searchResults = await app.$axios.$get(`/api/library/${libraryId}/search?q=${searchQuery}`).catch((error) => {
console.error('Search error', error) console.error('Search error', error)
return [] return {}
}) })
audiobookSearchResults = searchResults.audiobooks || []
store.commit('audiobooks/setSearchResults', searchResults) store.commit('audiobooks/setSearchResults', searchResults)
if (audiobookSearchResults.length) audiobookSearchResults.forEach((ab) => store.commit('audiobooks/addUpdate', ab.audiobook))
} }
// Series page
var selectedSeries = query.series ? app.$decode(query.series) : null var selectedSeries = query.series ? app.$decode(query.series) : null
store.commit('audiobooks/setSelectedSeries', selectedSeries) store.commit('audiobooks/setSelectedSeries', selectedSeries)
var libraryPage = params.id || '' var libraryPage = params.id || ''
store.commit('audiobooks/setLibraryPage', libraryPage) store.commit('audiobooks/setLibraryPage', libraryPage)
return { return {
id: libraryPage, id: libraryPage,
libraryId,
searchQuery, searchQuery,
searchResults, searchResults,
selectedSeries selectedSeries
@@ -64,9 +75,9 @@ export default {
methods: { methods: {
async newQuery() { async newQuery() {
var query = this.$route.query.query var query = this.$route.query.query
this.searchResults = await this.$axios.$get(`/api/audiobooks?q=${query}`).catch((error) => { this.searchResults = await this.$axios.$get(`/api/library/${this.libraryId}/search?q=${query}`).catch((error) => {
console.error('Search error', error) console.error('Search error', error)
return [] return {}
}) })
this.searchQuery = query this.searchQuery = query
} }
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
+4 -2
View File
@@ -14,7 +14,8 @@ export const state = () => ({
keywordFilter: null, keywordFilter: null,
selectedSeries: null, selectedSeries: null,
libraryPage: null, libraryPage: null,
searchResults: [] searchResults: {},
searchResultAudiobooks: []
}) })
export const getters = { export const getters = {
@@ -25,7 +26,7 @@ export const getters = {
if (!state.libraryPage) { if (!state.libraryPage) {
return getters.getFiltered() return getters.getFiltered()
} else if (state.libraryPage === 'search') { } else if (state.libraryPage === 'search') {
return state.searchResults return state.searchResultAudiobooks
} else if (state.libraryPage === 'series') { } else if (state.libraryPage === 'series') {
var series = getters.getSeriesGroups() var series = getters.getSeriesGroups()
if (state.selectedSeries) { if (state.selectedSeries) {
@@ -222,6 +223,7 @@ export const mutations = {
}, },
setSearchResults(state, val) { setSearchResults(state, val) {
state.searchResults = val state.searchResults = val
state.searchResultAudiobooks = val && val.audiobooks ? val.audiobooks.map(ab => ab.audiobook) : []
}, },
set(state, audiobooks) { set(state, audiobooks) {
// GENRES // GENRES
+14 -1
View File
@@ -9,6 +9,7 @@ export const state = () => ({
showEditModal: false, showEditModal: false,
showEReader: false, showEReader: false,
selectedAudiobook: null, selectedAudiobook: null,
selectedAudiobookFile: null,
playOnLoad: false, playOnLoad: false,
developerMode: false, developerMode: false,
selectedAudiobooks: [], selectedAudiobooks: [],
@@ -16,7 +17,8 @@ export const state = () => ({
previousPath: '/', previousPath: '/',
routeHistory: [], routeHistory: [],
showExperimentalFeatures: false, showExperimentalFeatures: false,
backups: [] backups: [],
bookshelfBookIds: []
}) })
export const getters = { export const getters = {
@@ -66,6 +68,9 @@ export const actions = {
} }
export const mutations = { export const mutations = {
setBookshelfBookIds(state, val) {
state.bookshelfBookIds = val || []
},
setRouteHistory(state, val) { setRouteHistory(state, val) {
state.routeHistory = val state.routeHistory = val
}, },
@@ -113,7 +118,15 @@ export const mutations = {
state.showEditModal = val state.showEditModal = val
}, },
showEReader(state, audiobook) { showEReader(state, audiobook) {
state.selectedAudiobookFile = null
state.selectedAudiobook = audiobook state.selectedAudiobook = audiobook
state.showEReader = true
},
showEReaderForFile(state, { audiobook, file }) {
state.selectedAudiobookFile = file
state.selectedAudiobook = audiobook
state.showEReader = true state.showEReader = true
}, },
setShowEReader(state, val) { setShowEReader(state, val) {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "1.4.7", "version": "1.4.11",
"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": {
+59 -4
View File
@@ -1,10 +1,13 @@
const express = require('express') const express = require('express')
const Path = require('path') const Path = require('path')
const fs = require('fs-extra') const fs = require('fs-extra')
const Logger = require('./Logger') const Logger = require('./Logger')
const User = require('./objects/User')
const { isObject } = require('./utils/index') const { isObject } = require('./utils/index')
const audioFileScanner = require('./utils/audioFileScanner')
const Library = require('./objects/Library') const Library = require('./objects/Library')
const User = require('./objects/User')
class ApiController { class ApiController {
constructor(MetadataPath, db, scanner, auth, streamManager, rssFeeds, downloadManager, coverController, backupManager, watcher, emitter, clientEmitter) { constructor(MetadataPath, db, scanner, auth, streamManager, rssFeeds, downloadManager, coverController, backupManager, watcher, emitter, clientEmitter) {
@@ -47,6 +50,7 @@ class ApiController {
this.router.delete('/audiobook/:id', this.deleteAudiobook.bind(this)) this.router.delete('/audiobook/:id', this.deleteAudiobook.bind(this))
this.router.patch('/audiobook/:id/tracks', this.updateAudiobookTracks.bind(this)) this.router.patch('/audiobook/:id/tracks', this.updateAudiobookTracks.bind(this))
this.router.post('/audiobook/:id/cover', this.uploadAudiobookCover.bind(this)) this.router.post('/audiobook/:id/cover', this.uploadAudiobookCover.bind(this))
this.router.patch('/audiobook/:id/coverfile', this.updateAudiobookCoverFromFile.bind(this))
this.router.patch('/audiobook/:id', this.updateAudiobook.bind(this)) this.router.patch('/audiobook/:id', this.updateAudiobook.bind(this))
this.router.patch('/match/:id', this.match.bind(this)) this.router.patch('/match/:id', this.match.bind(this))
@@ -76,6 +80,8 @@ class ApiController {
this.router.get('/download/:id', this.download.bind(this)) this.router.get('/download/:id', this.download.bind(this))
this.router.get('/filesystem', this.getFileSystemPaths.bind(this)) this.router.get('/filesystem', this.getFileSystemPaths.bind(this))
this.router.get('/scantracks/:id', this.scanAudioTrackNums.bind(this))
} }
find(req, res) { find(req, res) {
@@ -142,15 +148,18 @@ class ApiController {
var bookMatches = [] var bookMatches = []
var authorMatches = {} var authorMatches = {}
var seriesMatches = {} var seriesMatches = {}
var tagMatches = {}
var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === library.id) var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === library.id)
audiobooksInLibrary.forEach((ab) => { audiobooksInLibrary.forEach((ab) => {
var queryResult = ab.searchQuery(req.query.q) var queryResult = ab.searchQuery(req.query.q)
if (queryResult.book) { if (queryResult.book) {
bookMatches.push({ var bookMatchObj = {
audiobook: ab, audiobook: ab,
matchKey: queryResult.book matchKey: queryResult.book,
}) matchText: queryResult.bookMatchText
}
bookMatches.push(bookMatchObj)
} }
if (queryResult.author && !authorMatches[queryResult.author]) { if (queryResult.author && !authorMatches[queryResult.author]) {
authorMatches[queryResult.author] = { authorMatches[queryResult.author] = {
@@ -167,10 +176,23 @@ class ApiController {
seriesMatches[queryResult.series].audiobooks.push(ab) seriesMatches[queryResult.series].audiobooks.push(ab)
} }
} }
if (queryResult.tags && queryResult.tags.length) {
queryResult.tags.forEach((tag) => {
if (!tagMatches[tag]) {
tagMatches[tag] = {
tag,
audiobooks: [ab]
}
} else {
tagMatches[tag].audiobooks.push(ab)
}
})
}
}) })
res.json({ res.json({
audiobooks: bookMatches.slice(0, maxResults), audiobooks: bookMatches.slice(0, maxResults),
tags: Object.values(tagMatches).slice(0, maxResults),
authors: Object.values(authorMatches).slice(0, maxResults), authors: Object.values(authorMatches).slice(0, maxResults),
series: Object.values(seriesMatches).slice(0, maxResults) series: Object.values(seriesMatches).slice(0, maxResults)
}) })
@@ -445,6 +467,26 @@ class ApiController {
}) })
} }
async updateAudiobookCoverFromFile(req, res) {
if (!req.user.canUpdate) {
Logger.warn('User attempted to update without permission', req.user)
return res.sendStatus(403)
}
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
if (!audiobook) return res.sendStatus(404)
var coverFile = req.body
var updated = await audiobook.setCoverFromFile(coverFile)
if (updated) {
await this.db.updateAudiobook(audiobook)
this.emitter('audiobook_updated', audiobook.toJSONMinified())
}
if (updated) res.status(200).send('Cover updated successfully')
else res.status(200).send('No update was made to cover')
}
async updateAudiobook(req, res) { async updateAudiobook(req, res) {
if (!req.user.canUpdate) { if (!req.user.canUpdate) {
Logger.warn('User attempted to update without permission', req.user) Logger.warn('User attempted to update without permission', req.user)
@@ -762,5 +804,18 @@ class ApiController {
var dirs = await this.getDirectories(global.appRoot, '/', excludedDirs) var dirs = await this.getDirectories(global.appRoot, '/', excludedDirs)
res.json(dirs) res.json(dirs)
} }
async scanAudioTrackNums(req, res) {
if (!req.user || !req.user.isRoot) {
return res.sendStatus(403)
}
var audiobook = this.db.audiobooks.find(ab => ab.id === req.params.id)
if (!audiobook) {
return res.status(404).send('Audiobook not found')
}
var scandata = await audioFileScanner.scanTrackNumbers(audiobook)
res.json(scandata)
}
} }
module.exports = ApiController module.exports = ApiController
+10 -1
View File
@@ -307,7 +307,16 @@ class BackupManager {
// pipe archive data to the file // pipe archive data to the file
archive.pipe(output) archive.pipe(output)
archive.directory(configPath, 'config') var audiobooksDbDir = Path.join(configPath, 'audiobooks')
var librariesDbDir = Path.join(configPath, 'libraries')
var settingsDbDir = Path.join(configPath, 'settings')
var usersDbDir = Path.join(configPath, 'users')
archive.directory(audiobooksDbDir, 'config/audiobooks')
archive.directory(librariesDbDir, 'config/libraries')
archive.directory(settingsDbDir, 'config/settings')
archive.directory(usersDbDir, 'config/users')
if (metadataBooksPath) { if (metadataBooksPath) {
Logger.debug(`[BackupManager] Backing up Metadata Books "${metadataBooksPath}"`) Logger.debug(`[BackupManager] Backing up Metadata Books "${metadataBooksPath}"`)
archive.directory(metadataBooksPath, 'metadata-books') archive.directory(metadataBooksPath, 'metadata-books')
+5 -34
View File
@@ -82,10 +82,14 @@ class Db {
await this.load() await this.load()
// Insert Defaults // Insert Defaults
if (!this.users.find(u => u.type === 'root')) { var rootUser = this.users.find(u => u.type === 'root')
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')
await this.insertEntity('user', this.getDefaultUser(token)) await this.insertEntity('user', this.getDefaultUser(token))
} else {
Logger.info(`[Db] Root user exists, pw: ${rootUser.hasPw}`)
} }
if (!this.libraries.length) { if (!this.libraries.length) {
@@ -123,19 +127,6 @@ class Db {
await Promise.all([p1, p2, p3, p4]) await Promise.all([p1, p2, p3, p4])
} }
// insertAudiobook(audiobook) {
// return this.insertAudiobooks([audiobook])
// }
// insertAudiobooks(audiobooks) {
// return this.audiobooksDb.insert(audiobooks).then((results) => {
// Logger.debug(`[DB] Inserted ${results.inserted} audiobooks`)
// this.audiobooks = this.audiobooks.concat(audiobooks)
// }).catch((error) => {
// Logger.error(`[DB] Insert audiobooks Failed ${error}`)
// })
// }
updateAudiobook(audiobook) { updateAudiobook(audiobook) {
return this.audiobooksDb.update((record) => record.id === audiobook.id, () => audiobook).then((results) => { return this.audiobooksDb.update((record) => record.id === audiobook.id, () => audiobook).then((results) => {
Logger.debug(`[DB] Audiobook updated ${results.updated}`) Logger.debug(`[DB] Audiobook updated ${results.updated}`)
@@ -146,26 +137,6 @@ class Db {
}) })
} }
// insertUser(user) {
// return this.usersDb.insert([user]).then((results) => {
// Logger.debug(`[DB] Inserted user ${results.inserted}`)
// this.users.push(user)
// return true
// }).catch((error) => {
// Logger.error(`[DB] Insert user Failed ${error}`)
// return false
// })
// }
// 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}`)
// })
// }
updateUserStream(userId, streamId) { updateUserStream(userId, streamId) {
return this.usersDb.update((record) => record.id === userId, (user) => { return this.usersDb.update((record) => record.id === userId, (user) => {
user.stream = streamId user.stream = streamId
+3
View File
@@ -241,10 +241,13 @@ class DownloadManager {
if (shouldIncludeCover) { if (shouldIncludeCover) {
var _cover = audiobook.book.coverFullPath var _cover = audiobook.book.coverFullPath
// Supporting old local file prefix
if (!_cover && audiobook.book.cover && audiobook.book.cover.startsWith(Path.sep + 'local')) { if (!_cover && audiobook.book.cover && audiobook.book.cover.startsWith(Path.sep + 'local')) {
_cover = Path.join(this.AudiobookPath, _cover.replace(Path.sep + 'local', '')) _cover = Path.join(this.AudiobookPath, _cover.replace(Path.sep + 'local', ''))
Logger.debug('Local cover url', _cover) Logger.debug('Local cover url', _cover)
} }
ffmpegInputs.push({ ffmpegInputs.push({
input: _cover, input: _cover,
options: ['-f image2pipe'] options: ['-f image2pipe']
+39 -6
View File
@@ -128,11 +128,19 @@ class Audiobook {
} }
get hasEpub() { get hasEpub() {
return this.otherFiles.find(file => file.ext === '.epub') return this.ebooks.find(file => file.ext === '.epub')
} }
get hasMobi() { get hasMobi() {
return this.otherFiles.find(file => file.ext === '.mobi' || file.ext === '.azw3') return this.ebooks.find(file => file.ext === '.mobi' || file.ext === '.azw3')
}
get hasPdf() {
return this.ebooks.find(file => file.ext === '.pdf')
}
get hasComic() {
return this.ebooks.find(file => file.ext === '.cbr' || file.ext === '.cbz')
} }
get hasMissingIno() { get hasMissingIno() {
@@ -206,7 +214,7 @@ class Audiobook {
hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0, hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0,
// numEbooks: this.ebooks.length, // numEbooks: this.ebooks.length,
ebooks: this.ebooks.map(ebook => ebook.toJSON()), ebooks: this.ebooks.map(ebook => ebook.toJSON()),
numEbooks: (this.hasEpub || this.hasMobi) ? 1 : 0, // Only supporting epubs in the reader currently numEbooks: (this.hasEpub || this.hasMobi || this.hasPdf || this.hasComic) ? 1 : 0,
numTracks: this.tracks.length, numTracks: this.tracks.length,
chapters: this.chapters || [], chapters: this.chapters || [],
isMissing: !!this.isMissing, isMissing: !!this.isMissing,
@@ -233,7 +241,8 @@ class Audiobook {
audioFiles: this._audioFiles.map(audioFile => audioFile.toJSON()), audioFiles: this._audioFiles.map(audioFile => audioFile.toJSON()),
otherFiles: this._otherFiles.map(otherFile => otherFile.toJSON()), otherFiles: this._otherFiles.map(otherFile => otherFile.toJSON()),
ebooks: this.ebooks.map(ebook => ebook.toJSON()), ebooks: this.ebooks.map(ebook => ebook.toJSON()),
numEbooks: this.hasEpub ? 1 : 0, numEbooks: (this.hasEpub || this.hasMobi || this.hasPdf || this.hasComic) ? 1 : 0,
numTracks: this.tracks.length,
tags: this.tags, tags: this.tags,
book: this.bookToJSON(), book: this.bookToJSON(),
tracks: this.tracksToJSON(), tracks: this.tracksToJSON(),
@@ -363,6 +372,19 @@ class Audiobook {
this.book.setData(data) this.book.setData(data)
} }
setCoverFromFile(file) {
if (!file || !file.fullPath || !file.path) {
Logger.error(`[Audiobook] "${this.title}" Invalid file for setCoverFromFile`, file)
return false
}
var updateBookPayload = {}
updateBookPayload.coverFullPath = Path.normalize(file.fullPath)
// Set ab local static path from file relative path
var relImagePath = file.path.replace(this.path, '')
updateBookPayload.cover = Path.normalize(Path.join(`/s/book/${this.id}`, relImagePath))
return this.book.update(updateBookPayload)
}
addTrack(trackData) { addTrack(trackData) {
var track = new AudioTrack() var track = new AudioTrack()
track.setData(trackData) track.setData(trackData)
@@ -632,11 +654,22 @@ class Audiobook {
} }
isSearchMatch(search) { isSearchMatch(search) {
return this.book.isSearchMatch(search.toLowerCase().trim()) var tagMatch = this.tags.filter(tag => {
return tag.toLowerCase().includes(search.toLowerCase().trim())
})
return this.book.isSearchMatch(search.toLowerCase().trim()) || tagMatch.length
} }
searchQuery(search) { searchQuery(search) {
return this.book.getQueryMatches(search.toLowerCase().trim()) var matches = this.book.getQueryMatches(search.toLowerCase().trim())
matches.tags = this.tags.filter(tag => {
return tag.toLowerCase().includes(search.toLowerCase().trim())
})
if (!matches.book && matches.tags.length) {
matches.book = 'tags'
matches.bookMatchText = matches.tags.join(', ')
}
return matches
} }
getAudioFileByIno(ino) { getAudioFileByIno(ino) {
+14 -3
View File
@@ -132,12 +132,18 @@ class Book {
update(payload) { update(payload) {
var hasUpdates = false var hasUpdates = false
// Normalize cover paths if passed
if (payload.cover) { if (payload.cover) {
// If updating to local cover then normalize path
if (!payload.cover.startsWith('http:') && !payload.cover.startsWith('https:')) { if (!payload.cover.startsWith('http:') && !payload.cover.startsWith('https:')) {
payload.cover = Path.normalize(payload.cover) payload.cover = Path.normalize(payload.cover)
if (payload.coverFullPath) payload.coverFullPath = Path.normalize(payload.coverFullPath) if (payload.coverFullPath) payload.coverFullPath = Path.normalize(payload.coverFullPath)
else {
Logger.warn(`[Book] "${this.title}" updating book cover to "${payload.cover}" but no full path was passed`)
}
} }
} else if (payload.coverFullPath) {
Logger.warn(`[Book] "${this.title}" updating book full cover path to "${payload.coverFullPath}" but no relative path was passed`)
payload.coverFullPath = Path.normalize(payload.coverFullPath)
} }
for (const key in payload) { for (const key in payload) {
@@ -214,11 +220,16 @@ class Book {
} }
getQueryMatches(search) { getQueryMatches(search) {
var titleMatch = this._title.toLowerCase().includes(search) || this._subtitle.toLowerCase().includes(search) var titleMatch = this._title.toLowerCase().includes(search)
var subtitleMatch = this._subtitle.toLowerCase().includes(search)
var authorMatch = this._author.toLowerCase().includes(search) var authorMatch = this._author.toLowerCase().includes(search)
var seriesMatch = this._series.toLowerCase().includes(search) var seriesMatch = this._series.toLowerCase().includes(search)
var bookMatchKey = titleMatch ? 'title' : subtitleMatch ? 'subtitle' : authorMatch ? 'author' : seriesMatch ? 'series' : false
var bookMatchText = bookMatchKey ? this[bookMatchKey] : ''
return { return {
book: titleMatch ? 'title' : authorMatch ? 'author' : seriesMatch ? 'series' : false, book: bookMatchKey,
bookMatchText,
author: authorMatch ? this._author : false, author: authorMatch ? this._author : false,
series: seriesMatch ? this._series : false series: seriesMatch ? this._series : false
} }
+6
View File
@@ -37,9 +37,15 @@ class User {
get canUpload() { get canUpload() {
return !!this.permissions.upload && this.isActive return !!this.permissions.upload && this.isActive
} }
get hasPw() {
return !!this.pash && !!this.pash.length
}
getDefaultUserSettings() { getDefaultUserSettings() {
return { return {
mobileOrderBy: 'recent',
mobileOrderDesc: true,
mobileFilterBy: 'all',
orderBy: 'book.title', orderBy: 'book.title',
orderDesc: false, orderDesc: false,
filterBy: 'all', filterBy: 'all',
+23 -1
View File
@@ -111,7 +111,6 @@ async function scanAudioFiles(audiobook, newAudioFiles) {
var scanData = await scan(audioFile.fullPath) var scanData = await scan(audioFile.fullPath)
if (!scanData || scanData.error) { if (!scanData || scanData.error) {
Logger.error('[AudioFileScanner] Scan failed for', audioFile.path) Logger.error('[AudioFileScanner] Scan failed for', audioFile.path)
// audiobook.invalidAudioFiles.push(parts[i])
continue; continue;
} }
@@ -234,3 +233,26 @@ async function rescanAudioFiles(audiobook) {
return updates return updates
} }
module.exports.rescanAudioFiles = rescanAudioFiles module.exports.rescanAudioFiles = rescanAudioFiles
async function scanTrackNumbers(audiobook) {
var tracks = audiobook.tracks || []
var scannedTrackNumData = []
for (let i = 0; i < tracks.length; i++) {
var track = tracks[i]
var scanData = await scan(track.fullPath)
var trackNumFromMeta = getTrackNumberFromMeta(scanData)
var book = audiobook.book || {}
var trackNumFromFilename = getTrackNumberFromFilename(book.title, book.author, book.series, book.publishYear, track.filename)
Logger.info(`[AudioFileScanner] Track # for "${track.filename}", Metadata: "${trackNumFromMeta}", Filename: "${trackNumFromFilename}", Current: "${track.index}"`)
scannedTrackNumData.push({
filename: track.filename,
currentTrackNum: track.index,
trackNumFromFilename,
trackNumFromMeta,
scanDataTrackNum: scanData.file_tag_track
})
}
return scannedTrackNumData
}
module.exports.scanTrackNumbers = scanTrackNumbers
+1 -1
View File
@@ -1,7 +1,7 @@
const globals = { const globals = {
SupportedImageTypes: ['png', 'jpg', 'jpeg', 'webp'], SupportedImageTypes: ['png', 'jpg', 'jpeg', 'webp'],
SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'mp4'], SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'mp4'],
SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3'] SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz']
} }
module.exports = globals module.exports = globals