Fix: book id length & check duplicate ids, Change: library to lazy load book cards

This commit is contained in:
advplyr 2021-11-15 20:09:42 -06:00
parent ca6f2c01f6
commit 72f9732b67
18 changed files with 466 additions and 86 deletions

View File

@ -36,7 +36,7 @@
<cards-group-card v-else-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" :show-volume-number="!!selectedSeries" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" @edit="editBook" />
<cards-book-card v-else :ref="`book-card-${entity.id}`" :key="entity.id" is-bookshelf-book :show-volume-number="!!selectedSeries" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" @edit="editBook" @hook:mounted="mountedBookCard(entity)" />
</template>
</div>
<div class="bookshelfDivider w-full absolute bottom-0 left-0 right-0 z-10" :class="isCollections ? 'h-6' : 'h-4'" />
@ -79,7 +79,10 @@ export default {
rowPaddingX: 40,
keywordFilterTimeout: null,
scannerParseSubtitle: false,
wrapperClientWidth: 0
wrapperClientWidth: 0,
observer: null,
booksObserved: [],
booksVisible: {}
}
},
watch: {
@ -351,6 +354,61 @@ export default {
},
scan() {
this.$root.socket.emit('scan', this.$store.state.libraries.currentLibraryId)
},
mountedBookCard(entity, shouldUnobserve = false) {
if (!this.observer) {
console.error('Observer not loaded', entity.id)
return
}
var el = document.getElementById(`book-card-${entity.id}`)
if (el) {
if (shouldUnobserve) {
console.warn('Unobserving el', el)
this.observer.unobserve(el)
}
this.observer.observe(el)
this.booksObserved.push(entity.id)
// console.log('Book observed', this.booksObserved.length)
} else {
console.error('Could not get book card', entity.id)
}
},
getBookCard(id) {
if (!this.$refs[id] || !this.$refs[id].length) {
return null
}
return this.$refs[id][0]
},
observerCallback(entries, observer) {
entries.forEach((entry) => {
var bookId = entry.target.getAttribute('data-bookId')
if (!bookId) {
console.error('Invalid observe no book id', entry)
return
}
var component = this.getBookCard(entry.target.id)
if (component) {
if (entry.isIntersecting) {
if (!this.booksVisible[bookId]) {
this.booksVisible[bookId] = true
component.setShowCard(true)
}
} else if (this.booksVisible[bookId]) {
this.booksVisible[bookId] = false
component.setShowCard(false)
}
} else {
console.error('Could not get book card for id', entry.target.id)
}
})
},
initIO() {
let observerOptions = {
rootMargin: '0px',
threshold: 0.1
}
this.observer = new IntersectionObserver(this.observerCallback, observerOptions)
}
},
updated() {
@ -367,6 +425,18 @@ export default {
this.$store.commit('user/addCollectionsListener', { id: 'bookshelf', meth: this.collectionsUpdated })
this.init()
this.initIO()
setTimeout(() => {
var ids = {}
this.audiobooks.forEach((ab) => {
if (ids[ab.id]) {
console.error('FOUDN DUPLICATE ID', ids[ab.id], ab)
} else {
ids[ab.id] = ab
}
})
}, 5000)
},
beforeDestroy() {
window.removeEventListener('resize', this.resize)

View File

@ -1,70 +1,79 @@
<template>
<div ref="wrapper" class="relative">
<!-- New Book Flag -->
<div v-show="isNew" class="absolute top-4 left-0 w-4 h-10 pr-2 bg-darkgreen box-shadow-xl z-20">
<div class="absolute top-0 left-0 w-full h-full transform -rotate-90 flex items-center justify-center">
<p class="text-center text-sm">New</p>
</div>
<div class="absolute -bottom-4 left-0 triangle-right" />
</div>
<div class="rounded-sm h-full overflow-hidden relative" :style="{ padding: `${paddingY}px ${paddingX}px` }">
<nuxt-link :to="isSelectionMode ? '' : `/audiobook/${audiobookId}`" class="cursor-pointer">
<div class="w-full relative box-shadow-book" :style="{ height: height + 'px' }" @click="clickCard" @mouseover="isHovering = true" @mouseleave="isHovering = false">
<covers-book-cover :audiobook="audiobook" :author-override="authorFormat" :width="width" />
<!-- Hidden SM and DOWN -->
<div v-show="isHovering || isSelectionMode || isMoreMenuOpen" class="absolute top-0 left-0 w-full h-full bg-black rounded hidden md:block" :class="overlayWrapperClasslist">
<div v-show="showPlayButton" class="h-full flex items-center justify-center">
<div class="hover:text-gray-200 hover:scale-110 transform duration-200" @click.stop.prevent="play">
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">play_circle_filled</span>
</div>
</div>
<div v-show="showReadButton" class="h-full flex items-center justify-center">
<div class="hover:text-gray-200 hover:scale-110 transform duration-200" @click.stop.prevent="clickReadEBook">
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">auto_stories</span>
</div>
</div>
<div v-if="userCanUpdate" v-show="!isSelectionMode" class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-50" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="editClick">
<span class="material-icons" :style="{ fontSize: sizeMultiplier + 'rem' }">edit</span>
</div>
<div class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-100" :style="{ top: 0.375 * sizeMultiplier + 'rem', left: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="selectBtnClick">
<span class="material-icons" :class="selected ? 'text-yellow-400' : ''" :style="{ fontSize: 1.25 * sizeMultiplier + 'rem' }">{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}</span>
</div>
<!-- More Icon -->
<div ref="moreIcon" v-show="!isSelectionMode" class="hidden md:block absolute cursor-pointer hover:text-yellow-300" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickShowMore">
<span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span>
</div>
</div>
<div v-if="volumeNumber && showVolumeNumber && !isHovering && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ volumeNumber }}</p>
</div>
<!-- EBook Icon -->
<div
v-if="showSmallEBookIcon"
class="absolute rounded-full bg-blue-500 flex items-center justify-center bg-opacity-90 hover:scale-125 transform duration-200"
:style="{ bottom: 0.375 * sizeMultiplier + 'rem', left: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem`, width: 1.5 * sizeMultiplier + 'rem', height: 1.5 * sizeMultiplier + 'rem' }"
@click.stop.prevent="clickReadEBook"
>
<!-- <p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">EBook</p> -->
<span class="material-icons text-white" :style="{ fontSize: sizeMultiplier * 1 + 'rem' }">auto_stories</span>
</div>
<div v-show="!isSelectionMode" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full" :class="userIsRead ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
<ui-tooltip v-if="showError" :text="errorText" class="absolute bottom-4 left-0">
<div :style="{ height: 1.5 * sizeMultiplier + 'rem', width: 2.5 * sizeMultiplier + 'rem' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300">
<span class="material-icons text-red-100 pr-1" :style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span>
</div>
</ui-tooltip>
<div ref="wrapper" class="relative book-card" :data-bookId="audiobookId" :id="`book-card-${audiobookId}`">
<template v-if="!showCard">
<div class="rounded-sm h-full overflow-hidden relative" :style="{ padding: `${paddingY}px ${paddingX}px` }">
<div class="bg-bg flex items-center justify-center p-2" :style="{ height: height + 'px', width: width + 'px' }">
<p class="font-book text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
</div>
</nuxt-link>
</div>
</div>
</template>
<template v-else>
<!-- New Book Flag -->
<div v-show="isNew" class="absolute top-4 left-0 w-4 h-10 pr-2 bg-darkgreen box-shadow-xl z-20">
<div class="absolute top-0 left-0 w-full h-full transform -rotate-90 flex items-center justify-center">
<p class="text-center text-sm">New</p>
</div>
<div class="absolute -bottom-4 left-0 triangle-right" />
</div>
<div class="rounded-sm h-full overflow-hidden relative" :style="{ padding: `${paddingY}px ${paddingX}px` }">
<nuxt-link :to="isSelectionMode ? '' : `/audiobook/${audiobookId}`" class="cursor-pointer">
<div class="w-full relative box-shadow-book" :style="{ height: height + 'px' }" @click="clickCard" @mouseover="isHovering = true" @mouseleave="isHovering = false">
<covers-book-cover :audiobook="audiobook" :author-override="authorFormat" :width="width" />
<!-- Hidden SM and DOWN -->
<div v-show="isHovering || isSelectionMode || isMoreMenuOpen" class="absolute top-0 left-0 w-full h-full bg-black rounded hidden md:block" :class="overlayWrapperClasslist">
<div v-show="showPlayButton" class="h-full flex items-center justify-center">
<div class="hover:text-gray-200 hover:scale-110 transform duration-200" @click.stop.prevent="play">
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">play_circle_filled</span>
</div>
</div>
<div v-show="showReadButton" class="h-full flex items-center justify-center">
<div class="hover:text-gray-200 hover:scale-110 transform duration-200" @click.stop.prevent="clickReadEBook">
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">auto_stories</span>
</div>
</div>
<div v-if="userCanUpdate" v-show="!isSelectionMode" class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-50" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="editClick">
<span class="material-icons" :style="{ fontSize: sizeMultiplier + 'rem' }">edit</span>
</div>
<div class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-100" :style="{ top: 0.375 * sizeMultiplier + 'rem', left: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="selectBtnClick">
<span class="material-icons" :class="selected ? 'text-yellow-400' : ''" :style="{ fontSize: 1.25 * sizeMultiplier + 'rem' }">{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}</span>
</div>
<!-- More Icon -->
<div ref="moreIcon" v-show="!isSelectionMode" class="hidden md:block absolute cursor-pointer hover:text-yellow-300" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickShowMore">
<span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span>
</div>
</div>
<div v-if="volumeNumber && showVolumeNumber && !isHovering && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ volumeNumber }}</p>
</div>
<!-- EBook Icon -->
<div
v-if="showSmallEBookIcon"
class="absolute rounded-full bg-blue-500 flex items-center justify-center bg-opacity-90 hover:scale-125 transform duration-200"
:style="{ bottom: 0.375 * sizeMultiplier + 'rem', left: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem`, width: 1.5 * sizeMultiplier + 'rem', height: 1.5 * sizeMultiplier + 'rem' }"
@click.stop.prevent="clickReadEBook"
>
<!-- <p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">EBook</p> -->
<span class="material-icons text-white" :style="{ fontSize: sizeMultiplier * 1 + 'rem' }">auto_stories</span>
</div>
<div v-show="!isSelectionMode" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full" :class="userIsRead ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
<ui-tooltip v-if="showError" :text="errorText" class="absolute bottom-4 left-0">
<div :style="{ height: 1.5 * sizeMultiplier + 'rem', width: 2.5 * sizeMultiplier + 'rem' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300">
<span class="material-icons text-red-100 pr-1" :style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span>
</div>
</ui-tooltip>
</div>
</nuxt-link>
</div>
</template>
</div>
</template>
@ -90,14 +99,17 @@ export default {
type: Number,
default: 16
},
isBookshelfBook: Boolean,
showVolumeNumber: Boolean
},
data() {
return {
showCard: false,
isHovering: false,
isMoreMenuOpen: false,
isProcessingReadUpdate: false,
rescanning: false
rescanning: false,
timesVisible: 0
}
},
computed: {
@ -277,6 +289,10 @@ export default {
}
},
methods: {
setShowCard(val) {
if (val) this.timesVisible++
this.showCard = val
},
selectBtnClick() {
if (this.processingBatch) return
this.$store.commit('toggleAudiobookSelected', this.audiobookId)
@ -404,6 +420,9 @@ export default {
clickShowMore() {
this.createMoreMenu()
}
},
mounted() {
this.showCard = !this.isBookshelfBook
}
}
</script>

View File

@ -1,15 +1,30 @@
<template>
<div class="relative rounded-sm overflow-hidden" :style="{ height: width * 1.6 + 'px', width: width + 'px', maxWidth: width + 'px', minWidth: width + 'px' }">
<div class="w-full h-full relative">
<div class="w-full h-full relative bg-bg">
<div v-if="showCoverBg" class="bg-primary absolute top-0 left-0 w-full h-full">
<div class="w-full h-full z-0" ref="coverBg" />
</div>
<img ref="cover" :src="fullCoverUrl" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0" :class="showCoverBg ? 'object-contain' : 'object-cover'" />
<img ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0" :class="showCoverBg ? 'object-contain' : 'object-cover'" />
<div v-show="loading" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
<p class="font-book text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
<div class="absolute top-2 right-2">
<div class="la-ball-spin-clockwise la-sm">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
</div>
</div>
<div v-if="imageFailed" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-red-100" :style="{ padding: placeholderCoverPadding + 'rem' }">
<div class="w-full h-full border-2 border-error flex flex-col items-center justify-center">
<img src="/Logo.png" class="mb-2" :style="{ height: 64 * sizeMultiplier + 'px' }" />
<img src="/Logo.png" loading="lazy" class="mb-2" :style="{ height: 64 * sizeMultiplier + 'px' }" />
<p class="text-center font-book text-error" :style="{ fontSize: titleFontSize + 'rem' }">Invalid Cover</p>
</div>
</div>
@ -40,6 +55,7 @@ export default {
},
data() {
return {
loading: true,
imageFailed: false,
showCoverBg: false
}
@ -115,6 +131,7 @@ export default {
},
hideCoverBg() {},
imageLoaded() {
this.loading = false
if (this.$refs.cover && this.cover !== this.placeholderUrl) {
var { naturalWidth, naturalHeight } = this.$refs.cover
var aspectRatio = naturalHeight / naturalWidth
@ -130,10 +147,223 @@ export default {
}
},
imageError(err) {
this.loading = false
console.error('ImgError', err)
this.imageFailed = true
}
},
mounted() {}
}
</script>
</script>
<style>
/*!
* Load Awesome v1.1.0 (http://github.danielcardoso.net/load-awesome/)
* Copyright 2015 Daniel Cardoso <@DanielCardoso>
* Licensed under MIT
*/
.la-ball-spin-clockwise,
.la-ball-spin-clockwise > div {
position: relative;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.la-ball-spin-clockwise {
display: block;
font-size: 0;
color: #fff;
}
.la-ball-spin-clockwise.la-dark {
color: #262626;
}
.la-ball-spin-clockwise > div {
display: inline-block;
float: none;
background-color: currentColor;
border: 0 solid currentColor;
}
.la-ball-spin-clockwise {
width: 32px;
height: 32px;
}
.la-ball-spin-clockwise > div {
position: absolute;
top: 50%;
left: 50%;
width: 8px;
height: 8px;
margin-top: -4px;
margin-left: -4px;
border-radius: 100%;
-webkit-animation: ball-spin-clockwise 1s infinite ease-in-out;
-moz-animation: ball-spin-clockwise 1s infinite ease-in-out;
-o-animation: ball-spin-clockwise 1s infinite ease-in-out;
animation: ball-spin-clockwise 1s infinite ease-in-out;
}
.la-ball-spin-clockwise > div:nth-child(1) {
top: 5%;
left: 50%;
-webkit-animation-delay: -0.875s;
-moz-animation-delay: -0.875s;
-o-animation-delay: -0.875s;
animation-delay: -0.875s;
}
.la-ball-spin-clockwise > div:nth-child(2) {
top: 18.1801948466%;
left: 81.8198051534%;
-webkit-animation-delay: -0.75s;
-moz-animation-delay: -0.75s;
-o-animation-delay: -0.75s;
animation-delay: -0.75s;
}
.la-ball-spin-clockwise > div:nth-child(3) {
top: 50%;
left: 95%;
-webkit-animation-delay: -0.625s;
-moz-animation-delay: -0.625s;
-o-animation-delay: -0.625s;
animation-delay: -0.625s;
}
.la-ball-spin-clockwise > div:nth-child(4) {
top: 81.8198051534%;
left: 81.8198051534%;
-webkit-animation-delay: -0.5s;
-moz-animation-delay: -0.5s;
-o-animation-delay: -0.5s;
animation-delay: -0.5s;
}
.la-ball-spin-clockwise > div:nth-child(5) {
top: 94.9999999966%;
left: 50.0000000005%;
-webkit-animation-delay: -0.375s;
-moz-animation-delay: -0.375s;
-o-animation-delay: -0.375s;
animation-delay: -0.375s;
}
.la-ball-spin-clockwise > div:nth-child(6) {
top: 81.8198046966%;
left: 18.1801949248%;
-webkit-animation-delay: -0.25s;
-moz-animation-delay: -0.25s;
-o-animation-delay: -0.25s;
animation-delay: -0.25s;
}
.la-ball-spin-clockwise > div:nth-child(7) {
top: 49.9999750815%;
left: 5.0000051215%;
-webkit-animation-delay: -0.125s;
-moz-animation-delay: -0.125s;
-o-animation-delay: -0.125s;
animation-delay: -0.125s;
}
.la-ball-spin-clockwise > div:nth-child(8) {
top: 18.179464974%;
left: 18.1803700518%;
-webkit-animation-delay: 0s;
-moz-animation-delay: 0s;
-o-animation-delay: 0s;
animation-delay: 0s;
}
.la-ball-spin-clockwise.la-sm {
width: 16px;
height: 16px;
}
.la-ball-spin-clockwise.la-sm > div {
width: 4px;
height: 4px;
margin-top: -2px;
margin-left: -2px;
}
.la-ball-spin-clockwise.la-2x {
width: 64px;
height: 64px;
}
.la-ball-spin-clockwise.la-2x > div {
width: 16px;
height: 16px;
margin-top: -8px;
margin-left: -8px;
}
.la-ball-spin-clockwise.la-3x {
width: 96px;
height: 96px;
}
.la-ball-spin-clockwise.la-3x > div {
width: 24px;
height: 24px;
margin-top: -12px;
margin-left: -12px;
}
/*
* Animation
*/
@-webkit-keyframes ball-spin-clockwise {
0%,
100% {
opacity: 1;
-webkit-transform: scale(1);
transform: scale(1);
}
20% {
opacity: 1;
}
80% {
opacity: 0;
-webkit-transform: scale(0);
transform: scale(0);
}
}
@-moz-keyframes ball-spin-clockwise {
0%,
100% {
opacity: 1;
-moz-transform: scale(1);
transform: scale(1);
}
20% {
opacity: 1;
}
80% {
opacity: 0;
-moz-transform: scale(0);
transform: scale(0);
}
}
@-o-keyframes ball-spin-clockwise {
0%,
100% {
opacity: 1;
-o-transform: scale(1);
transform: scale(1);
}
20% {
opacity: 1;
}
80% {
opacity: 0;
-o-transform: scale(0);
transform: scale(0);
}
}
@keyframes ball-spin-clockwise {
0%,
100% {
opacity: 1;
-webkit-transform: scale(1);
-moz-transform: scale(1);
-o-transform: scale(1);
transform: scale(1);
}
20% {
opacity: 1;
}
80% {
opacity: 0;
-webkit-transform: scale(0);
-moz-transform: scale(0);
-o-transform: scale(0);
transform: scale(0);
}
}
</style>

View File

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

View File

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

View File

@ -4,7 +4,7 @@ const fs = require('fs-extra')
const date = require('date-and-time')
const Logger = require('./Logger')
const { isObject } = require('./utils/index')
const { isObject, getId } = require('./utils/index')
const audioFileScanner = require('./utils/audioFileScanner')
const BookFinder = require('./BookFinder')
@ -702,7 +702,7 @@ class ApiController {
return res.status(500).send('Username already taken')
}
account.id = (Math.trunc(Math.random() * 1000) + Date.now()).toString(36)
account.id = getId('usr')
account.pash = await this.auth.hashPass(account.password)
delete account.password
account.token = await this.auth.generateAccessToken({ userId: account.id })

View File

@ -3,6 +3,7 @@ const njodb = require("njodb")
const fs = require('fs-extra')
const jwt = require('jsonwebtoken')
const Logger = require('./Logger')
const { version } = require('../package.json')
const Audiobook = require('./objects/Audiobook')
const User = require('./objects/User')
const UserCollection = require('./objects/UserCollection')
@ -36,6 +37,9 @@ class Db {
this.collections = []
this.serverSettings = null
// Stores previous version only if upgraded
this.previousVersion = null
}
getEntityDb(entityName) {
@ -138,6 +142,11 @@ class Db {
var serverSettings = this.settings.find(s => s.id === 'server-settings')
if (serverSettings) {
this.serverSettings = new ServerSettings(serverSettings)
// Check if server was upgraded
if (!this.serverSettings.version || this.serverSettings.version !== version) {
this.previousVersion = this.serverSettings.version || '1.0.0'
}
}
}
})
@ -146,6 +155,12 @@ class Db {
Logger.info(`[DB] ${this.collections.length} Collections Loaded`)
})
await Promise.all([p1, p2, p3, p4, p5])
// Update server version in server settings
if (this.previousVersion) {
this.serverSettings.version = version
await this.updateEntity('settings', this.serverSettings)
}
}
updateAudiobook(audiobook) {

View File

@ -5,6 +5,7 @@ const archiver = require('archiver')
const workerThreads = require('worker_threads')
const Logger = require('./Logger')
const Download = require('./objects/Download')
const { getId } = require('./utils/index')
const { writeConcatFile, writeMetadataFile } = require('./utils/ffmpegHelpers')
const { getFileSize } = require('./utils/fileUtils')
const TAG = 'DownloadManager'
@ -61,7 +62,7 @@ class DownloadManager {
}
async prepareDownload(client, audiobook, options = {}) {
var downloadId = (Math.trunc(Math.random() * 1000) + Date.now()).toString(36)
var downloadId = getId('dl')
var dlpath = Path.join(this.downloadDirPath, downloadId)
Logger.info(`Start Download for ${audiobook.id} - DownloadId: ${downloadId} - ${dlpath}`)

View File

@ -6,7 +6,7 @@ const Logger = require('./Logger')
const { version } = require('../package.json')
const audioFileScanner = require('./utils/audioFileScanner')
const { groupFilesIntoAudiobookPaths, getAudiobookFileData, scanRootDir } = require('./utils/scandir')
const { comparePaths, getIno } = require('./utils/index')
const { comparePaths, getIno, getId } = require('./utils/index')
const { secondsToTimestamp } = require('./utils/fileUtils')
const { ScanResult, CoverDestination } = require('./utils/constants')
@ -752,5 +752,29 @@ class Scanner {
var result = await this.bookFinder.findCovers(query.provider, query.title, query.author || null, options)
res.json(result)
}
async fixDuplicateIds() {
var ids = {}
var audiobooksUpdated = 0
for (let i = 0; i < this.db.audiobooks.length; i++) {
var ab = this.db.audiobooks[i]
if (ids[ab.id]) {
var abCopy = new Audiobook(ab.toJSON())
abCopy.id = getId('ab')
if (abCopy.book.cover) {
abCopy.book.cover = abCopy.book.cover.replace(ab.id, abCopy.id)
}
Logger.warn('Found duplicate ID - updating from', ab.id, 'to', abCopy.id)
await this.db.removeEntity('audiobook', ab.id)
await this.db.insertEntity('audiobook', abCopy)
audiobooksUpdated++
} else {
ids[ab.id] = true
}
}
if (audiobooksUpdated) {
Logger.info(`[Scanner] Updated ${audiobooksUpdated} audiobook IDs`)
}
}
}
module.exports = Scanner

View File

@ -118,6 +118,12 @@ class Server {
await this.backupManager.init()
await this.logManager.init()
// Only fix duplicate ids once on upgrade
if (this.db.previousVersion === '1.0.0') {
Logger.info(`[Server] Running scan for duplicate book IDs`)
await this.scanner.fixDuplicateIds()
}
this.watcher.initWatcher(this.libraries)
this.watcher.on('files', this.filesChanged.bind(this))
}

View File

@ -1,7 +1,7 @@
const Path = require('path')
const fs = require('fs-extra')
const { bytesPretty, elapsedPretty, readTextFile } = require('../utils/fileUtils')
const { comparePaths, getIno } = require('../utils/index')
const { comparePaths, getIno, getId } = require('../utils/index')
const { parseOpfMetadataXML } = require('../utils/parseOpfMetadata')
const { extractCoverArt } = require('../utils/ffmpegHelpers')
const nfoGenerator = require('../utils/nfoGenerator')
@ -317,7 +317,7 @@ class Audiobook {
}
setData(data) {
this.id = (Math.trunc(Math.random() * 1000) + Date.now()).toString(36)
this.id = getId('ab')
this.libraryId = data.libraryId || 'main'
this.folderId = data.folderId || 'audiobooks'
this.ino = data.ino || null

View File

@ -1,3 +1,5 @@
const { getId } = require("../utils")
class Folder {
constructor(folder = null) {
this.id = null
@ -27,7 +29,7 @@ class Folder {
}
setData(data) {
this.id = data.id ? data.id : 'fol' + (Math.trunc(Math.random() * 1000) + Date.now()).toString(36)
this.id = data.id ? data.id : getId('fol')
this.fullPath = data.fullPath
this.libraryId = data.libraryId
this.addedAt = Date.now()

View File

@ -1,4 +1,5 @@
const Folder = require('./Folder')
const { getId } = require('../utils/index')
class Library {
constructor(library = null) {
@ -46,7 +47,7 @@ class Library {
}
setData(data) {
this.id = data.id ? data.id : 'lib' + (Math.trunc(Math.random() * 1000) + Date.now()).toString(36)
this.id = data.id ? data.id : getId('lib')
this.name = data.name
if (data.folder) {
this.folders = [

View File

@ -32,6 +32,7 @@ class ServerSettings {
this.loggerScannerLogsToKeep = 2
this.logLevel = Logger.logLevel
this.version = null
if (settings) {
this.construct(settings)
@ -56,6 +57,7 @@ class ServerSettings {
this.loggerScannerLogsToKeep = settings.loggerScannerLogsToKeep || 2
this.logLevel = settings.logLevel || Logger.logLevel
this.version = settings.version || null
if (this.logLevel !== Logger.logLevel) {
Logger.setLogLevel(this.logLevel)
@ -78,7 +80,8 @@ class ServerSettings {
backupMetadataCovers: this.backupMetadataCovers,
loggerDailyLogsToKeep: this.loggerDailyLogsToKeep,
loggerScannerLogsToKeep: this.loggerScannerLogsToKeep,
logLevel: this.logLevel
logLevel: this.logLevel,
version: this.version
}
}

View File

@ -3,6 +3,7 @@ const EventEmitter = require('events')
const Path = require('path')
const fs = require('fs-extra')
const Logger = require('../Logger')
const { getId } = require('../utils/index')
const { secondsToTimestamp } = require('../utils/fileUtils')
const { writeConcatFile } = require('../utils/ffmpegHelpers')
const hlsPlaylistGenerator = require('../utils/hlsPlaylistGenerator')
@ -13,7 +14,7 @@ class Stream extends EventEmitter {
constructor(streamPath, client, audiobook, transcodeOptions = {}) {
super()
this.id = (Date.now() + Math.trunc(Math.random() * 1000)).toString(36)
this.id = getId('str')
this.client = client
this.audiobook = audiobook

View File

@ -1,4 +1,5 @@
const Logger = require('../Logger')
const { getId } = require('../utils/index')
class UserCollection {
constructor(collection) {
@ -62,7 +63,7 @@ class UserCollection {
if (!data.userId || !data.libraryId || !data.name) {
return false
}
this.id = (Math.trunc(Math.random() * 1000) + Date.now()).toString(36)
this.id = getId('usr')
this.userId = data.userId
this.libraryId = data.libraryId
this.name = data.name

View File

@ -1,5 +1,6 @@
const Logger = require('../Logger')
const date = require('date-and-time')
const { getId } = require('../utils/index')
class UserListeningSession {
constructor(session) {
@ -58,7 +59,7 @@ class UserListeningSession {
}
setData(audiobook, user) {
this.id = 'ls_' + (Math.trunc(Math.random() * 1000) + Date.now()).toString(36)
this.id = getId('ls')
this.userId = user.id
this.audiobookId = audiobook.id
this.audiobookTitle = audiobook.title || ''

View File

@ -58,3 +58,9 @@ const xmlToJSON = (xml) => {
})
}
module.exports.xmlToJSON = xmlToJSON
module.exports.getId = (prepend = '') => {
var _id = Math.random().toString(36).substring(2, 8) + Math.random().toString(36).substring(2, 8) + Math.random().toString(36).substring(2, 8)
if (prepend) return prepend + '_' + _id
return _id
}