Compare commits

..

21 Commits

Author SHA1 Message Date
advplyr 9f29b245d7 Version bump 1.7.2 2022-03-05 15:46:06 -06:00
advplyr ecf62c5443 Add:Experimental direct play support 2022-03-05 15:37:30 -06:00
advplyr eb82d9c300 Add:Sleep timer #165 2022-03-05 12:30:46 -06:00
advplyr 45582343b8 Fix:Backups table UI 2022-03-05 10:23:30 -06:00
advplyr 6a4d3a55b1 Fix:User last activity 2022-03-05 10:10:42 -06:00
advplyr 95bacce5e5 Add:Start/complete date on audiobook page #383, Fix local hls stream url 2022-03-04 18:44:20 -06:00
advplyr 408775a25a Add:Library media type selection and library icons 2022-03-03 19:03:34 -06:00
advplyr d3a8ecc8d1 Remove unnecessary env 2022-03-01 17:59:47 -06:00
advplyr ecc425d89d Fix:Remove epub dependency from server 2022-02-27 19:03:38 -06:00
advplyr 289b3e9f94 Version bump 1.7.1 2022-02-27 19:02:07 -06:00
advplyr 74a68a4557 Add:Check metadata.abs file modified date to rescan 2022-02-27 18:58:23 -06:00
advplyr 42604331ff Add:Sort by file birthtime and modified timestamps #215 2022-02-27 18:18:38 -06:00
advplyr 428a515c6a Add:mtime,ctime,birthtime to audiobook folder and files 2022-02-27 18:07:36 -06:00
advplyr c81b12f459 Add:Book folder name starting with number and has series folder use as volume number 2022-02-27 16:52:01 -06:00
advplyr 779d22bf55 Add:Parse abmetadata metadata.abs file, save metadata.abs on all audiobook insert/update 2022-02-27 16:14:57 -06:00
advplyr 295c6b0c74 Add:Generate book metadata file when book details are changed,Add:Server setting for storing book metadata in book folder 2022-02-27 14:28:18 -06:00
advplyr aa50cc2d81 Change:Main dir paths moved to global vars, server settings stored in globals vars 2022-02-27 13:47:52 -06:00
advplyr eb109c398f Change:Server setting for coverDestination to storeCoverWithBook boolean, Add:abmetadata generator 2022-02-27 12:47:56 -06:00
advplyr 4e7d2ddc58 Fix:Quick match requiring cover 2022-02-27 11:03:10 -06:00
advplyr a5b10aac96 Readme update features 2022-02-27 11:02:33 -06:00
advplyr fcbccaeb4e Fix:Drag n drop uploader 2022-02-26 18:28:09 -06:00
59 changed files with 1163 additions and 756 deletions
+42 -26
View File
@@ -1,21 +1,27 @@
<template>
<div class="w-full -mt-6">
<div class="w-full relative mb-1">
<div v-if="chapters.length" class="hidden md:flex absolute right-20 top-0 bottom-0 h-full items-end">
<div class="cursor-pointer text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
<div class="absolute -top-10 md:top-0 right-0 md:right-2 flex items-center h-full">
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden md:block" />
<div class="cursor-pointer text-gray-300 mx-1 md:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showSleepTimer')">
<span v-if="!sleepTimerSet" class="material-icons" style="font-size: 1.7rem">snooze</span>
<div v-else class="flex items-center">
<span class="material-icons text-lg text-warning">snooze</span>
<p class="text-xl text-warning font-mono font-semibold text-center px-0.5 pb-0.5" style="min-width: 30px">{{ sleepTimerRemainingString }}</p>
</div>
</div>
<div class="cursor-pointer text-gray-300 mx-1 md:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showBookmarks')">
<span class="material-icons" style="font-size: 1.7rem">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span>
</div>
<div v-if="chapters.length" class="cursor-pointer text-gray-300 mx-1 md:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
<span class="material-icons text-3xl">format_list_bulleted</span>
</div>
</div>
<div class="absolute top-0 bottom-0 h-full hidden md:flex items-end" :class="chapters.length ? ' right-32' : 'right-20'">
<div class="cursor-pointer text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="showBookmarks">
<span class="material-icons" style="font-size: 1.7rem">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span>
</div>
</div>
<div class="absolute top-0 bottom-0 h-full hidden md:flex items-end" :class="chapters.length ? ' right-44' : 'right-32'">
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" />
</div>
<div class="flex pb-4 md:pb-2">
<div class="flex pt-4 pb-2 md:pt-0 md:pb-2">
<div class="flex-grow" />
<template v-if="!loading">
<div class="cursor-pointer flex items-center justify-center text-gray-300 mr-8" @mousedown.prevent @mouseup.prevent @click.stop="restart">
@@ -91,7 +97,9 @@ export default {
bookmarks: {
type: Array,
default: () => []
}
},
sleepTimerSet: Boolean,
sleepTimerRemaining: Number
},
data() {
return {
@@ -106,10 +114,23 @@ export default {
showChaptersModal: false,
currentTime: 0,
trackOffsetLeft: 16, // Track is 16px from edge
duration: 0
duration: 0,
chapterTicks: []
}
},
computed: {
sleepTimerRemainingString() {
var rounded = Math.round(this.sleepTimerRemaining)
if (rounded < 90) {
return `${rounded}s`
}
var minutesRounded = Math.round(rounded / 60)
if (minutesRounded < 90) {
return `${minutesRounded}m`
}
var hoursRounded = Math.round(minutesRounded / 60)
return `${hoursRounded}h`
},
token() {
return this.$store.getters['user/getToken']
},
@@ -118,7 +139,6 @@ export default {
},
timeRemainingPretty() {
if (this.timeRemaining < 0) {
console.warn('Time remaining < 0', this.duration, this.currentTime, this.timeRemaining)
return this.$secondsToTimestamp(this.timeRemaining * -1)
}
return '-' + this.$secondsToTimestamp(this.timeRemaining)
@@ -127,15 +147,6 @@ export default {
if (!this.duration) return 0
return Math.round((100 * this.currentTime) / this.duration)
},
chapterTicks() {
return this.chapters.map((chap) => {
var perc = chap.start / this.duration
return {
title: chap.title,
left: perc * this.trackWidth
}
})
},
currentChapter() {
return this.chapters.find((chapter) => chapter.start <= this.currentTime && this.currentTime < chapter.end)
},
@@ -149,6 +160,14 @@ export default {
methods: {
setDuration(duration) {
this.duration = duration
this.chapterTicks = this.chapters.map((chap) => {
var perc = chap.start / this.duration
return {
title: chap.title,
left: perc * this.trackWidth
}
})
},
setCurrentTime(time) {
this.currentTime = time
@@ -330,9 +349,6 @@ export default {
if (!this.chapters.length) return
this.showChaptersModal = !this.showChaptersModal
},
showBookmarks() {
this.$emit('showBookmarks', this.currentTime)
},
init() {
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
this.$emit('setPlaybackRate', this.playbackRate)
+76 -6
View File
@@ -1,5 +1,5 @@
<template>
<div v-if="streamAudiobook" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 sm:h-44 md:h-40 z-40 bg-primary px-4 pb-4 pt-2">
<div v-if="streamAudiobook" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 sm:h-44 md:h-40 z-40 bg-primary px-4 pb-1 md:pb-4 pt-2">
<nuxt-link :to="`/audiobook/${streamAudiobook.id}`" class="absolute left-4 cursor-pointer" :style="{ top: bookCoverPosTop + 'px' }">
<covers-book-cover :audiobook="streamAudiobook" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</nuxt-link>
@@ -22,12 +22,31 @@
</div>
</div>
<div class="flex-grow" />
<span class="material-icons p-4 cursor-pointer" @click="closePlayer">close</span>
<span class="material-icons px-2 py-1 md:p-4 cursor-pointer" @click="closePlayer">close</span>
</div>
<audio-player ref="audioPlayer" :chapters="chapters" :paused="!isPlaying" :loading="playerLoading" :bookmarks="bookmarks" @playPause="playPause" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setVolume="setVolume" @setPlaybackRate="setPlaybackRate" @seek="seek" @close="closePlayer" @showBookmarks="showBookmarks" />
<audio-player
ref="audioPlayer"
:chapters="chapters"
:paused="!isPlaying"
:loading="playerLoading"
:bookmarks="bookmarks"
:sleep-timer-set="sleepTimerSet"
:sleep-timer-remaining="sleepTimerRemaining"
@playPause="playPause"
@jumpForward="jumpForward"
@jumpBackward="jumpBackward"
@setVolume="setVolume"
@setPlaybackRate="setPlaybackRate"
@seek="seek"
@close="closePlayer"
@showBookmarks="showBookmarks"
@showSleepTimer="showSleepTimerModal = true"
/>
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :audiobook-id="bookmarkAudiobookId" :current-time="bookmarkCurrentTime" @select="selectBookmark" />
<modals-sleep-timer-modal v-model="showSleepTimerModal" :timer-set="sleepTimerSet" :timer-time="sleepTimerTime" :remaining="sleepTimerRemaining" @set="setSleepTimer" @cancel="cancelSleepTimer" @increment="incrementSleepTimer" @decrement="decrementSleepTimer" />
</div>
</template>
@@ -44,10 +63,18 @@ export default {
bookmarkAudiobookId: null,
playerLoading: false,
isPlaying: false,
currentTime: 0
currentTime: 0,
showSleepTimerModal: false,
sleepTimerSet: false,
sleepTimerTime: 0,
sleepTimerRemaining: 0,
sleepTimer: null
}
},
computed: {
showExperimentalFeatures() {
return this.$store.state.showExperimentalFeatures
},
coverAspectRatio() {
return this.$store.getters['getServerSetting']('coverAspectRatio')
},
@@ -111,6 +138,49 @@ export default {
}
},
methods: {
setSleepTimer(seconds) {
this.sleepTimerSet = true
this.sleepTimerTime = seconds
this.sleepTimerRemaining = seconds
this.runSleepTimer()
this.showSleepTimerModal = false
},
runSleepTimer() {
var lastTick = Date.now()
clearInterval(this.sleepTimer)
this.sleepTimer = setInterval(() => {
var elapsed = Date.now() - lastTick
lastTick = Date.now()
this.sleepTimerRemaining -= elapsed / 1000
if (this.sleepTimerRemaining <= 0) {
this.clearSleepTimer()
this.playerHandler.pause()
this.$toast.info('Sleep Timer Done.. zZzzZz')
}
}, 1000)
},
cancelSleepTimer() {
this.showSleepTimerModal = false
this.clearSleepTimer()
},
clearSleepTimer() {
clearInterval(this.sleepTimer)
this.sleepTimerRemaining = 0
this.sleepTimer = null
this.sleepTimerSet = false
},
incrementSleepTimer(amount) {
if (!this.sleepTimerSet) return
this.sleepTimerRemaining += amount
},
decrementSleepTimer(amount) {
if (this.sleepTimerRemaining < amount) {
this.sleepTimerRemaining = 3
return
}
this.sleepTimerRemaining = Math.max(0, this.sleepTimerRemaining - amount)
},
playPause() {
this.playerHandler.playPause()
},
@@ -146,9 +216,9 @@ export default {
this.$refs.audioPlayer.setBufferTime(buffertime)
}
},
showBookmarks(currentTime) {
showBookmarks() {
this.bookmarkAudiobookId = this.audiobookId
this.bookmarkCurrentTime = currentTime
this.bookmarkCurrentTime = this.currentTime
this.showBookmarksModal = true
},
selectBookmark(bookmark) {
+7
View File
@@ -5,6 +5,7 @@
<div class="absolute cover-bg" ref="coverBg" />
</div>
<!-- Alternative bookshelf title/author/sort -->
<div v-if="isAlternativeBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
<p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
<span v-if="volumeNumber">#{{ volumeNumber }}&nbsp;</span>{{ displayTitle }}
@@ -62,16 +63,20 @@
<span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span>
</div>
</div>
<!-- Series name overlay -->
<div v-if="booksInSeries && audiobook && isHovering" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-60 rounded flex items-center justify-center" :style="{ padding: sizeMultiplier + 'rem' }">
<p class="text-gray-200 text-center" :style="{ fontSize: 1.1 * sizeMultiplier + 'rem' }">{{ series }}</p>
</div>
<!-- Error widget -->
<ui-tooltip v-if="showError" :text="errorText" class="absolute bottom-4 left-0 z-10">
<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>
<!-- Volume number -->
<div v-if="volumeNumber && showVolumeNumber && !isHovering && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :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>
@@ -204,6 +209,8 @@ export default {
return this.authorFL
},
displaySortLine() {
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._audiobook.mtimeMs)
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._audiobook.birthtimeMs)
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._audiobook.addedAt)
if (this.orderBy === 'duration') return 'Duration: ' + this.$elapsedPrettyExtended(this._audiobook.duration, false)
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._audiobook.size)
@@ -59,6 +59,14 @@ export default {
{
text: 'Size',
value: 'size'
},
{
text: 'File Birthtime',
value: 'birthtimeMs'
},
{
text: 'File Modified',
value: 'mtimeMs'
}
]
}
@@ -0,0 +1,23 @@
<template>
<svg fill="currentColor" class="h-full w-full p-px" viewBox="0 0 1978.03 2349.44">
<path
d="M2519.5,1438.39c-12.13-10.1-31-25-56.57-42.62V1197.31c0-505.94-410.15-916.09-916.1-916.09h0c-505.94,0-916.09,410.15-916.09,916.09v198.46c-25.57,17.66-44.44,32.52-56.57,42.62a45.45,45.45,0,0,0-16.35,34.95v237.74a45.45,45.45,0,0,0,16.35,35c28.28,23.54,93.18,72.92,194.22,123.55v23.11c0,62.32,40.21,112.85,89.8,112.85h0c49.59,0,89.8-50.53,89.8-112.85V1322.51c0-62.33-40.21-112.86-89.8-112.86h0c-47.51,0-86.4,46.38-89.58,105.07l-.22.11V1197.31c0-429.92,348.52-778.43,778.44-778.43h0c429.92,0,778.44,348.51,778.44,778.43v117.52l-.22-.11c-3.18-58.69-42.06-105.07-89.58-105.07h0c-49.59,0-89.79,50.53-89.79,112.86v570.18c0,62.32,40.2,112.85,89.79,112.85h0c49.6,0,89.8-50.53,89.8-112.85v-23.11c101.05-50.63,165.95-100,194.23-123.55a45.48,45.48,0,0,0,16.35-35V1473.34A45.48,45.48,0,0,0,2519.5,1438.39Z"
transform="translate(-557.82 -281.22)"
/>
<path d="M1227.4,2429.63a108.47,108.47,0,0,0,108.47-108.47V1106.56A108.47,108.47,0,0,0,1227.4,998.08H1115.33a108.48,108.48,0,0,0-108.48,108.48v1214.6a108.47,108.47,0,0,0,108.48,108.47ZM1047.75,1289.38H1295v25.83H1047.75Z" transform="translate(-557.82 -281.22)" />
<path d="M1602.87,2429.63a108.47,108.47,0,0,0,108.47-108.47V1106.56a108.47,108.47,0,0,0-108.47-108.48H1490.8a108.48,108.48,0,0,0-108.48,108.48v1214.6a108.47,108.47,0,0,0,108.48,108.47ZM1423.22,1289.38h247.22v25.83H1423.22Z" transform="translate(-557.82 -281.22)" />
<path d="M1978.34,2429.63a108.47,108.47,0,0,0,108.47-108.47V1106.56a108.47,108.47,0,0,0-108.47-108.48H1866.27a108.48,108.48,0,0,0-108.48,108.48v1214.6a108.47,108.47,0,0,0,108.48,108.47ZM1798.69,1289.38h247.22v25.83H1798.69Z" transform="translate(-557.82 -281.22)" />
<rect x="180.05" y="2185.95" width="1617.93" height="163.49" rx="81.74" />
</svg>
</template>
<script>
export default {
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>
+16
View File
@@ -0,0 +1,16 @@
<template>
<svg fill="currentColor" viewBox="0 0 24 24">
<path fill="currentColor" d="M9 3V18H12V3H9M12 5L16 18L19 17L15 4L12 5M5 5V18H8V5H5M3 19V21H21V19H3Z" />
</svg>
</template>
<script>
export default {
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>
+16
View File
@@ -0,0 +1,16 @@
<template>
<svg viewBox="0 0 24 24">
<path fill="currentColor" d="M6,19L9,15.14L11.14,17.72L14.14,13.86L18,19H6M6,4H11V12L8.5,10.5L6,12M18,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V4A2,2 0 0,0 18,2Z" />
</svg>
</template>
<script>
export default {
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>
+16
View File
@@ -0,0 +1,16 @@
<template>
<svg class="p-px" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
</svg>
</template>
<script>
export default {
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>
+19
View File
@@ -0,0 +1,19 @@
<template>
<svg class="p-px" viewBox="0 0 122.877 120.596">
<path
fill="currentColor"
d="M68.925,69.906v50.689H53.953V69.906c-4.918-2.662-8.259-7.867-8.259-13.854 c0-8.694,7.05-15.744,15.745-15.744c8.694,0,15.745,7.05,15.745,15.744C77.184,62.039,73.843,67.244,68.925,69.906L68.925,69.906z M39.32,11.165c2.916-1.438,4.111-4.969,2.673-7.882c-1.438-2.914-4.966-4.111-7.88-2.674C22.213,6.479,12.958,16.19,7.11,27.625 c-4.32,8.445-6.783,17.842-7.08,27.325c-0.299,9.563,1.587,19.223,5.973,28.114c5.401,10.953,14.558,20.695,28.039,27.592 c2.889,1.477,6.429,0.33,7.905-2.559c1.477-2.889,0.331-6.428-2.558-7.904c-11.037-5.645-18.486-13.525-22.833-22.334 c-3.506-7.111-5.014-14.857-4.774-22.539c0.243-7.757,2.256-15.442,5.79-22.348C22.304,23.721,29.76,15.879,39.32,11.165 L39.32,11.165z M88.765,0.608c-2.914-1.438-6.443-0.24-7.881,2.674c-1.438,2.914-0.242,6.445,2.674,7.882 c9.561,4.715,17.017,12.556,21.747,21.808c3.533,6.905,5.547,14.59,5.789,22.348c0.24,7.682-1.268,15.428-4.773,22.539 c-4.347,8.809-11.796,16.689-22.833,22.334c-2.889,1.477-4.034,5.016-2.558,7.904c1.476,2.889,5.016,4.035,7.905,2.559 c13.48-6.896,22.638-16.639,28.039-27.592c4.386-8.891,6.272-18.551,5.973-28.114c-0.297-9.483-2.76-18.88-7.079-27.325 C109.919,16.19,100.665,6.479,88.765,0.608L88.765,0.608z M82.791,26.505c-2.195-1.581-5.256-1.082-6.837,1.113 c-1.58,2.195-1.082,5.256,1.113,6.837c0.885,0.637,1.753,1.352,2.604,2.134c4.971,4.583,7.919,10.694,8.538,17.16 c0.626,6.524-1.111,13.437-5.518,19.552c-0.748,1.039-1.61,2.092-2.585,3.15c-1.835,1.992-1.708,5.098,0.287,6.932 c1.994,1.834,5.099,1.705,6.933-0.287c1.18-1.279,2.286-2.641,3.315-4.072c5.862-8.139,8.166-17.4,7.322-26.197 c-0.848-8.853-4.871-17.208-11.648-23.457C85.249,28.387,84.074,27.431,82.791,26.505L82.791,26.505z M45.81,34.458 c2.195-1.581,2.694-4.642,1.113-6.837c-1.581-2.195-4.642-2.694-6.837-1.114c-1.284,0.926-2.458,1.882-3.524,2.864 c-6.778,6.25-10.801,14.604-11.649,23.457c-0.844,8.796,1.46,18.06,7.323,26.199c1.031,1.43,2.136,2.791,3.315,4.07 c1.834,1.992,4.939,2.121,6.932,0.287c1.996-1.834,2.123-4.939,0.288-6.932c-0.975-1.059-1.837-2.111-2.585-3.15 c-4.406-6.115-6.144-13.027-5.518-19.551c0.619-6.465,3.567-12.577,8.538-17.16C44.058,35.81,44.926,35.095,45.81,34.458 L45.81,34.458z"
/>
</svg>
</template>
<script>
export default {
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>
@@ -0,0 +1,115 @@
<template>
<modals-modal v-model="show" name="sleep-timer" :width="350" :height="'unset'">
<template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
<p class="font-book text-3xl text-white truncate pointer-events-none">Sleep Timer</p>
</div>
</template>
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
<div v-if="!timerSet" class="w-full">
<template v-for="time in sleepTimes">
<div :key="time.text" class="flex items-center px-6 py-3 justify-center cursor-pointer hover:bg-bg relative" @click="setTime(time)">
<p class="text-xl text-center">{{ time.text }}</p>
</div>
</template>
</div>
<div v-else class="w-full p-4">
<div class="mb-4 flex items-center justify-center">
<ui-btn :padding-x="2" small :disabled="remaining < 30 * 60" class="flex items-center mr-4" @click="decrement(30 * 60)">
<span class="material-icons text-lg">remove</span>
<span class="pl-1 text-base font-mono">30m</span>
</ui-btn>
<ui-icon-btn icon="remove" @click="decrement(60 * 5)" />
<p class="mx-6 text-2xl font-mono">{{ $secondsToTimestamp(remaining) }}</p>
<ui-icon-btn icon="add" @click="increment(60 * 5)" />
<ui-btn :padding-x="2" small class="flex items-center ml-4" @click="increment(30 * 60)">
<span class="material-icons text-lg">add</span>
<span class="pl-1 text-base font-mono">30m</span>
</ui-btn>
</div>
<ui-btn class="w-full" @click="$emit('cancel')">Cancel</ui-btn>
</div>
</div>
</modals-modal>
</template>
<script>
export default {
props: {
value: Boolean,
timerSet: Boolean,
timerTime: Number,
remaining: Number
},
data() {
return {
sleepTimes: [
{
seconds: 10,
text: '10 seconds'
},
{
seconds: 60 * 5,
text: '5 minutes'
},
{
seconds: 60 * 30,
text: '30 minutes'
},
{
seconds: 60 * 60,
text: '60 minutes'
},
{
seconds: 60 * 90,
text: '90 minutes'
},
{
seconds: 60 * 120,
text: '2 hours'
},
{
seconds: 60 * 180,
text: '3 hours'
}
]
}
},
watch: {
show(newVal) {
if (newVal) {
}
}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
}
},
methods: {
setTime(time) {
this.$emit('set', time.seconds)
},
increment(amount) {
this.$emit('increment', amount)
},
decrement(amount) {
if (amount > this.remaining) {
if (this.remaining > 60) amount = 60
else amount = 5
}
this.$emit('decrement', amount)
}
}
}
</script>
@@ -6,11 +6,14 @@
</div>
<div v-if="!showDirectoryPicker" class="w-full h-full py-4">
<div class="flex flex-wrap -mx-1">
<div class="w-full md:w-2/3 px-1">
<div class="flex flex-wrap md:flex-nowrap -mx-1">
<div class="w-full md:flex-grow px-1 py-1 md:py-0">
<ui-text-input-with-label v-model="name" label="Library Name" />
</div>
<div class="w-full md:w-1/3 px-1">
<div class="w-1/2 md:w-72 px-1 py-1 md:py-0">
<ui-media-type-picker v-model="mediaType" />
</div>
<div class="w-1/2 md:w-72 px-1 py-1 md:py-0">
<ui-dropdown v-model="provider" :items="providers" label="Metadata Provider" small />
</div>
</div>
@@ -18,7 +21,6 @@
<div class="w-full py-4">
<p class="px-1 text-sm font-semibold">Folders</p>
<div v-for="(folder, index) in folders" :key="index" class="w-full flex items-center py-1 px-2">
<!-- <ui-text-input :value="folder.fullPath" type="text" class="w-full" /> -->
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
<ui-editable-text v-model="folder.fullPath" type="text" class="w-full" />
<span v-show="folders.length > 1" class="material-icons ml-2 cursor-pointer hover:text-error" @click="removeFolder(folder)">close</span>
@@ -60,6 +62,7 @@ export default {
return {
name: '',
provider: '',
mediaType: '',
folders: [],
showDirectoryPicker: false,
disableWatcher: false
@@ -80,7 +83,7 @@ export default {
var newfolderpaths = this.folderPaths.join(',')
var origfolderpaths = this.library.folders.map((f) => f.fullPath).join(',')
return newfolderpaths === origfolderpaths && this.name === this.library.name && this.provider === this.library.provider && this.disableWatcher === this.library.disableWatcher
return newfolderpaths === origfolderpaths && this.name === this.library.name && this.provider === this.library.provider && this.disableWatcher === this.library.disableWatcher && this.mediaType === this.library.mediaType
},
providers() {
return this.$store.state.scanners.providers
@@ -103,6 +106,7 @@ export default {
this.provider = this.library ? this.library.provider : ''
this.folders = this.library ? this.library.folders.map((p) => ({ ...p })) : []
this.disableWatcher = this.library ? !!this.library.disableWatcher : false
this.mediaType = this.library ? this.library.mediaType : 'default'
this.showDirectoryPicker = false
},
selectFolder(fullPath) {
@@ -129,6 +133,8 @@ export default {
name: this.name,
provider: this.provider,
folders: this.folders,
mediaType: this.mediaType,
icon: this.mediaType,
disableWatcher: this.disableWatcher
}
@@ -163,6 +169,8 @@ export default {
name: this.name,
provider: this.provider,
folders: this.folders,
mediaType: this.mediaType,
icon: this.mediaType,
disableWatcher: this.disableWatcher
}
@@ -1,9 +1,7 @@
<template>
<div class="w-full px-4 h-12 border border-white border-opacity-10 flex items-center relative -mt-px" :class="selected ? 'bg-primary bg-opacity-50' : 'hover:bg-primary hover:bg-opacity-25'" @mouseover="mouseover = true" @mouseleave="mouseover = false">
<div v-show="selected" class="absolute top-0 left-0 h-full w-0.5 bg-warning z-10" />
<svg v-if="!libraryScan" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-white" :class="isHovering ? 'text-opacity-90' : 'text-opacity-50'" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
</svg>
<widgets-library-icon v-if="!libraryScan" :icon="library.icon" :size="6" class="text-white" :class="isHovering ? 'text-opacity-90' : 'text-opacity-50'" />
<svg v-else viewBox="0 0 24 24" class="h-6 w-6 text-white text-opacity-50 animate-spin">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
+5 -5
View File
@@ -9,21 +9,21 @@
<table id="backups">
<tr>
<th>File</th>
<th class="hidden sm:block w-32 md:w-56">Datetime</th>
<th class="hidden sm:block w-20 md:w-28">Size</th>
<th class="hidden sm:table-cell w-32 md:w-56">Datetime</th>
<th class="hidden sm:table-cell w-20 md:w-28">Size</th>
<th class="w-36"></th>
</tr>
<tr v-for="backup in backups" :key="backup.id">
<td>
<p class="truncate text-xs sm:text-sm md:text-base">/{{ backup.path.replace(/\\/g, '/') }}</p>
</td>
<td class="hidden sm:block font-sans text-base">{{ backup.datePretty }}</td>
<td class="hidden sm:block font-mono md:text-base text-xs">{{ $bytesPretty(backup.fileSize) }}</td>
<td class="hidden sm:table-cell font-sans text-base">{{ backup.datePretty }}</td>
<td class="hidden sm:table-cell font-mono md:text-base text-xs">{{ $bytesPretty(backup.fileSize) }}</td>
<td>
<div class="w-full flex flex-row items-center justify-center">
<ui-btn small color="primary" @click="applyBackup(backup)">Apply</ui-btn>
<a :href="`/metadata/${backup.path.replace(/%/g, '%25').replace(/#/g, '%23')}?token=${userToken}`" class="mx-1 pt-0.5 hover:text-opacity-100 text-opacity-70 text-white" download><span class="material-icons text-xl">download</span></a>
<a :href="`/metadata/${backup.path.replace(/%/g, '%25').replace(/#/g, '%23')}?token=${userToken}`" class="mx-1 pt-1 hover:text-opacity-100 text-opacity-70 text-white" download><span class="material-icons text-xl">download</span></a>
<span class="material-icons text-xl hover:text-error hover:text-opacity-100 text-opacity-70 text-white cursor-pointer mx-1" @click="deleteBackupClick(backup)">delete</span>
</div>
+4 -2
View File
@@ -87,9 +87,11 @@ export default {
},
methods: {
getLastRead(audiobooks) {
var abs = Object.values(audiobooks)
var abs = Object.values(audiobooks).filter((ab) => {
return ab.progress > 0
})
if (abs.length) {
abs = abs.sort((a, b) => a.lastUpdate - b.lastUpdate)
abs = abs.sort((a, b) => b.lastUpdate - a.lastUpdate)
// Book object is attached on request
if (abs[0].book) return abs[0].book.title
return abs[0].audiobookTitle ? abs[0].audiobookTitle : null
+1 -1
View File
@@ -12,7 +12,7 @@
<template v-for="library in librariesFiltered">
<li :key="library.id" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="selectLibrary(library)">
<div class="flex items-center px-3">
<widgets-library-icon :icon="currentLibraryIcon" class="mr-2" />
<widgets-library-icon :icon="library.icon" class="mr-2" />
<span class="font-normal block truncate font-sans text-sm">{{ library.name }}</span>
</div>
</li>
+104
View File
@@ -0,0 +1,104 @@
<template>
<div class="relative w-full h-9" v-click-outside="clickOutsideObj">
<p class="text-sm font-semibold">{{ label }}</p>
<button type="button" :disabled="disabled" class="relative h-full w-full border border-gray-600 rounded shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer bg-primary text-gray-100 hover:text-gray-200" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
<span class="flex items-center">
<widgets-library-icon :icon="selected" class="mr-2" />
<span class="block truncate text-sm">{{ selectedName }}</span>
</span>
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<span class="material-icons text-gray-100">expand_more</span>
</span>
</button>
<transition name="menu">
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox">
<template v-for="type in types">
<li :key="type.id" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="select(type)">
<div class="flex items-center px-3">
<widgets-library-icon :icon="type.id" class="mr-2" />
<span class="font-normal block truncate font-sans text-sm">{{ type.name }}</span>
</div>
</li>
</template>
</ul>
</transition>
</div>
</template>
<script>
export default {
props: {
value: String,
disabled: Boolean,
label: {
type: String,
default: 'Media Type'
}
},
data() {
return {
clickOutsideObj: {
handler: this.clickedOutside,
events: ['mousedown'],
isActive: true
},
showMenu: false,
types: [
{
id: 'default',
name: 'Default'
},
{
id: 'audiobooks',
name: 'Audiobooks'
},
{
id: 'books',
name: 'Books'
},
{
id: 'podcasts',
name: 'Podcasts'
},
{
id: 'comics',
name: 'Comics'
}
]
}
},
computed: {
selected: {
get() {
return this.value || 'default'
},
set(val) {
this.$emit('input', val)
}
},
selectedItem() {
return this.types.find((t) => t.id === this.selected)
},
selectedName() {
return this.selectedItem ? this.selectedItem.name : 'Default'
}
},
methods: {
clickShowMenu() {
if (this.disabled) return
this.showMenu = !this.showMenu
},
clickedOutside() {
this.showMenu = false
},
select(type) {
if (this.disabled) return
this.selected = type.id
this.showMenu = false
}
},
mounted() {}
}
</script>
+13 -6
View File
@@ -1,20 +1,27 @@
<template>
<div class="h-4 w-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
</svg>
<div :class="`h-${size} w-${size}`">
<component :is="iconComponentName" />
</div>
</template>
<script>
export default {
props: {
icon: String
icon: String,
size: {
type: Number,
default: 5
}
},
data() {
return {}
},
computed: {},
computed: {
iconComponentName() {
if (this.icon === 'default') return `icons-database-svg`
return `icons-${this.icon}-svg`
}
},
methods: {},
mounted() {}
}
-1
View File
@@ -175,7 +175,6 @@ export default {
}).map(ab => this.cleanBook(ab, index++))
return {
books,
invalidBooks,
ignoredFiles
}
},
+1 -2
View File
@@ -7,8 +7,7 @@ module.exports = {
dev: process.env.NODE_ENV !== 'production',
env: {
serverUrl: process.env.NODE_ENV === 'production' ? '' : 'http://localhost:3333',
chromecastReceiver: 'FD1F76C5',
baseUrl: process.env.BASE_URL || 'http://0.0.0.0'
chromecastReceiver: 'FD1F76C5'
},
// rootDir: process.env.NODE_ENV !== 'production' ? 'client/' : '',
telemetry: false,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
"version": "1.7.0",
"version": "1.7.2",
"description": "Audiobook manager and player",
"main": "index.js",
"scripts": {
+14 -5
View File
@@ -47,7 +47,6 @@
<nuxt-link :key="narrator" :to="`/library/${libraryId}/bookshelf?filter=narrators.${$encode(narrator)}`" class="hover:underline">{{ narrator }}</nuxt-link
><span :key="index" v-if="index < narrators.length - 1">,&nbsp;</span>
</template>
<!-- <nuxt-link :to="`/library/${libraryId}/bookshelf?filter=narrators.${$encode(narrator)}`" class="hover:underline">{{ narrator }}</nuxt-link> -->
</div>
</div>
<div v-if="publishYear" class="flex py-0.5">
@@ -95,9 +94,13 @@
<p class="ml-4">Book has no audio tracks but has valid ebook files. The e-reader is experimental and can be turned on in config.</p>
</div>
<div v-if="progressPercent > 0 && progressPercent < 1" class="px-4 py-2 mt-4 bg-primary text-sm font-semibold rounded-md text-gray-200 relative max-w-max mx-auto md:mx-0" :class="resettingProgress ? 'opacity-25' : ''">
<p class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
<p class="text-gray-400 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p>
<!-- Progress -->
<div v-if="progressPercent > 0" class="px-4 py-2 mt-4 bg-primary text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0" :class="resettingProgress ? 'opacity-25' : ''">
<p v-if="progressPercent < 1" class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
<p v-else class="text-xs">Finished {{ $formatDate(userProgressFinishedAt, 'MM/dd/yyyy') }}</p>
<p v-if="progressPercent < 1" class="text-gray-200 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p>
<p class="text-gray-400 text-xs pt-1">Started {{ $formatDate(userProgressStartedAt, 'MM/dd/yyyy') }}</p>
<div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick">
<span class="material-icons text-sm">close</span>
</div>
@@ -372,7 +375,13 @@ export default {
return this.duration - this.userCurrentTime
},
progressPercent() {
return this.userAudiobook ? this.userAudiobook.progress : 0
return this.userAudiobook ? Math.max(Math.min(1, this.userAudiobook.progress), 0) : 0
},
userProgressStartedAt() {
return this.userAudiobook ? this.userAudiobook.startedAt : 0
},
userProgressFinishedAt() {
return this.userAudiobook ? this.userAudiobook.finishedAt : 0
},
streamAudiobook() {
return this.$store.state.streamAudiobook
-10
View File
@@ -57,16 +57,6 @@ export default {
this.$store.commit('setDeveloperMode', value)
this.$toast.info(`Developer Mode ${value ? 'Enabled' : 'Disabled'}`)
}
// saveMetadataComplete(result) {
// this.savingMetadata = false
// if (!result) return
// this.$toast.success(`Metadata saved for ${result.success} audiobooks`)
// },
// saveMetadataFiles() {
// this.savingMetadata = true
// this.$root.socket.once('save_metadata_complete', this.saveMetadataComplete)
// this.$root.socket.emit('save_metadata')
// }
},
mounted() {}
}
+16 -14
View File
@@ -8,10 +8,20 @@
</div>
<div class="flex items-center py-2">
<ui-toggle-switch v-model="storeCoversInAudiobookDir" :disabled="updatingServerSettings" @input="updateCoverStorageDestination" />
<ui-tooltip :text="tooltips.coverDestination">
<ui-toggle-switch v-model="newServerSettings.storeCoverWithBook" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeCoverWithBook', val)" />
<ui-tooltip :text="tooltips.storeCoverWithBook">
<p class="pl-4 text-lg">
Store covers with audiobook
Store covers with book
<span class="material-icons icon-text">info_outlined</span>
</p>
</ui-tooltip>
</div>
<div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.storeMetadataWithBook" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeMetadataWithBook', val)" />
<ui-tooltip :text="tooltips.storeMetadataWithBook">
<p class="pl-4 text-lg">
Store metadata with book
<span class="material-icons icon-text">info_outlined</span>
</p>
</ui-tooltip>
@@ -172,7 +182,6 @@ export default {
data() {
return {
isResettingAudiobooks: false,
storeCoversInAudiobookDir: false,
updatingServerSettings: false,
useSquareBookCovers: false,
useAlternativeBookshelfView: false,
@@ -182,10 +191,11 @@ export default {
scannerDisableWatcher: 'Disables the automatic adding/updating of audiobooks when file changes are detected. *Requires server restart',
scannerPreferOpfMetadata: 'OPF file metadata will be used for book details over folder names',
scannerPreferAudioMetadata: 'Audio file ID3 meta tags will be used for book details over folder names',
scannerParseSubtitle: 'Extract subtitles from audiobook directory names.<br>Subtitle must be seperated by " - "<br>i.e. "Book Title - A Subtitle Here" has the subtitle "A Subtitle Here"',
scannerParseSubtitle: 'Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by " - "<br>i.e. "Book Title - A Subtitle Here" has the subtitle "A Subtitle Here"',
scannerFindCovers: 'If your audiobook does not have an embedded cover or a cover image inside the folder, the scanner will attempt to find a cover.<br>Note: This will extend scan time',
bookshelfView: 'Alternative bookshelf view that shows title & author under book covers',
coverDestination: 'By default covers are stored in /metadata/books, enabling this setting will store covers inside your audiobooks directory. Only one file named "cover" will be kept.',
storeCoverWithBook: 'By default covers are stored in /metadata/books, enabling this setting will store covers in the books folder. Only one file named "cover" will be kept',
storeMetadataWithBook: 'By default metadata files are stored in /metadata/books, enabling this setting will store metadata files in the books folder. Uses .abs file extension',
coverAspectRatio: 'Prefer to use square covers over standard 1.6:1 book covers'
}
}
@@ -226,12 +236,6 @@ export default {
scannerCoverProvider: val
})
},
updateCoverStorageDestination(val) {
this.newServerSettings.coverDestination = val ? this.$constants.CoverDestination.AUDIOBOOK : this.$constants.CoverDestination.METADATA
this.updateServerSettings({
coverDestination: this.newServerSettings.coverDestination
})
},
updateBookCoverAspectRatio(val) {
this.updateServerSettings({
coverAspectRatio: val ? this.$constants.BookCoverAspectRatio.SQUARE : this.$constants.BookCoverAspectRatio.STANDARD
@@ -263,8 +267,6 @@ export default {
initServerSettings() {
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
this.storeCoversInAudiobookDir = this.newServerSettings.coverDestination === this.$constants.CoverDestination.AUDIOBOOK
this.useSquareBookCovers = this.newServerSettings.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE
this.useAlternativeBookshelfView = this.newServerSettings.bookshelfView === this.$constants.BookshelfView.TITLES
+3 -3
View File
@@ -15,18 +15,18 @@
</div>
<div v-if="showExperimentalFeatures" class="w-full h-px bg-white bg-opacity-10 my-2" />
<div v-if="showExperimentalFeatures" class="py-2">
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Listening Stats <span class="pl-2 text-xs text-error">(web app only)</span></h1>
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Listening Stats <span class="pl-2 text-xs text-error">(experimental)</span></h1>
<p class="text-sm text-gray-300">
Total Time Listened:&nbsp;
<span class="font-mono text-base">{{ listeningTimePretty }}</span>
</p>
<p class="text-sm text-gray-300">
<p v-if="timeListenedToday" class="text-sm text-gray-300">
Time Listened Today:&nbsp;
<span class="font-mono text-base">{{ $elapsedPrettyExtended(timeListenedToday) }}</span>
</p>
<div v-if="latestSession" class="mt-4">
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Last Listening Session <span class="pl-2 text-xs text-error">(web app only)</span></h1>
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Last Listening Session</h1>
<p class="text-sm text-gray-300">{{ latestSession.audiobookTitle }} {{ $dateDistanceFromNow(latestSession.lastUpdate) }} for {{ $elapsedPrettyExtended(this.latestSession.timeListening) }}</p>
</div>
</div>
+10
View File
@@ -16,4 +16,14 @@ export default class AudioTrack {
}
return `${window.location.origin}${this.contentUrl}`
}
get relativeContentUrl() {
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
if (process.env.NODE_ENV === 'development') {
return `${process.env.serverUrl}${this.contentUrl}`
}
return this.contentUrl
}
}
+2
View File
@@ -17,6 +17,8 @@ export default class CastPlayer extends EventEmitter {
this.playWhenReady = false
this.defaultPlaybackRate = 1
this.playableMimetypes = {}
this.coverUrl = ''
this.castPlayerState = 'IDLE'
+85 -9
View File
@@ -14,10 +14,13 @@ export default class LocalPlayer extends EventEmitter {
this.hlsStreamId = null
this.hlsInstance = null
this.usingNativeplayer = false
this.currentTime = 0
this.startTime = 0
this.trackStartTime = 0
this.playWhenReady = false
this.defaultPlaybackRate = 1
this.playableMimetypes = {}
this.initialize()
}
@@ -38,9 +41,16 @@ export default class LocalPlayer extends EventEmitter {
this.player.addEventListener('play', this.evtPlay.bind(this))
this.player.addEventListener('pause', this.evtPause.bind(this))
this.player.addEventListener('progress', this.evtProgress.bind(this))
this.player.addEventListener('ended', this.evtEnded.bind(this))
this.player.addEventListener('error', this.evtError.bind(this))
this.player.addEventListener('loadedmetadata', this.evtLoadedMetadata.bind(this))
this.player.addEventListener('timeupdate', this.evtTimeupdate.bind(this))
var mimeTypes = ['audio/flac', 'audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/aac']
mimeTypes.forEach((mt) => {
this.playableMimetypes[mt] = this.player.canPlayType(mt)
})
console.log(`[LocalPlayer] Supported mime types`, this.playableMimetypes)
}
evtPlay() {
@@ -53,11 +63,27 @@ export default class LocalPlayer extends EventEmitter {
var lastBufferTime = this.getLastBufferedTime()
this.emit('buffertimeUpdate', lastBufferTime)
}
evtEnded() {
if (this.currentTrackIndex < this.audioTracks.length - 1) {
console.log(`[LocalPlayer] Track ended - loading next track ${this.currentTrackIndex + 1}`)
// Has next track
this.currentTrackIndex++
this.playWhenReady = true
this.startTime = this.currentTrack.startOffset
this.loadCurrentTrack()
} else {
console.log(`[LocalPlayer] Ended`)
}
}
evtError(error) {
console.error('Player error', error)
this.emit('error', error)
}
evtLoadedMetadata(data) {
console.log('Audio Loaded Metadata', data)
if (!this.hlsStreamId) {
this.player.currentTime = this.trackStartTime
}
this.emit('stateChange', 'LOADED')
if (this.playWhenReady) {
this.playWhenReady = false
@@ -89,23 +115,33 @@ export default class LocalPlayer extends EventEmitter {
this.audioTracks = tracks
this.hlsStreamId = hlsStreamId
this.playWhenReady = playWhenReady
this.startTime = startTime
if (this.hlsInstance) {
this.destroyHlsInstance()
}
this.currentTime = startTime
if (this.hlsStreamId) {
this.setHlsStream()
} else {
this.setDirectPlay()
}
}
setHlsStream() {
this.trackStartTime = 0
// iOS does not support Media Elements but allows for HLS in the native audio player
if (!Hls.isSupported()) {
console.warn('HLS is not supported - fallback to using audio element')
this.usingNativeplayer = true
this.player.src = this.currentTrack.contentUrl
this.player.currentTime = this.currentTime
this.player.src = this.currentTrack.relativeContentUrl
this.player.currentTime = this.startTime
return
}
var hlsOptions = {
startPosition: this.currentTime || -1
startPosition: this.startTime || -1
// No longer needed because token is put in a query string
// xhrSetup: (xhr) => {
// xhr.setRequestHeader('Authorization', `Bearer ${this.token}`)
@@ -115,7 +151,7 @@ export default class LocalPlayer extends EventEmitter {
this.hlsInstance.attachMedia(this.player)
this.hlsInstance.on(Hls.Events.MEDIA_ATTACHED, () => {
this.hlsInstance.loadSource(this.currentTrack.contentUrl)
this.hlsInstance.loadSource(this.currentTrack.relativeContentUrl)
this.hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
console.log('[HLS] Manifest Parsed')
@@ -133,6 +169,23 @@ export default class LocalPlayer extends EventEmitter {
})
}
setDirectPlay() {
// Set initial track and track time offset
var trackIndex = this.audioTracks.findIndex(t => this.startTime >= t.startOffset && this.startTime < (t.startOffset + t.duration))
this.currentTrackIndex = trackIndex >= 0 ? trackIndex : 0
this.loadCurrentTrack()
}
loadCurrentTrack() {
if (!this.currentTrack) return
// When direct play track is loaded current time needs to be set
this.trackStartTime = Math.max(0, this.startTime - (this.currentTrack.startOffset || 0))
this.player.src = this.currentTrack.relativeContentUrl
console.log(`[LocalPlayer] Loading track src ${this.currentTrack.relativeContentUrl}`)
this.player.load()
}
destroyHlsInstance() {
if (!this.hlsInstance) return
if (this.hlsInstance.destroy) {
@@ -181,8 +234,31 @@ export default class LocalPlayer extends EventEmitter {
seek(time) {
if (!this.player) return
var offsetTime = time - (this.currentTrack.startOffset || 0)
this.player.currentTime = Math.max(0, offsetTime)
if (this.hlsStreamId) {
// Seeking HLS stream
var offsetTime = time - (this.currentTrack.startOffset || 0)
this.player.currentTime = Math.max(0, offsetTime)
} else {
// Seeking Direct play
if (time < this.currentTrack.startOffset || time > this.currentTrack.startOffset + this.currentTrack.duration) {
// Change Track
var trackIndex = this.audioTracks.findIndex(t => time >= t.startOffset && time < (t.startOffset + t.duration))
if (trackIndex >= 0) {
this.startTime = time
this.currentTrackIndex = trackIndex
if (!this.player.paused) {
// audio player playing so play when track loads
this.playWhenReady = true
}
this.loadCurrentTrack()
}
} else {
var offsetTime = time - (this.currentTrack.startOffset || 0)
this.player.currentTime = Math.max(0, offsetTime)
}
}
}
setVolume(volume) {
+58 -16
View File
@@ -38,7 +38,6 @@ export default class PlayerHandler {
load(audiobook, playWhenReady, startTime = 0) {
if (!this.player) this.switchPlayer()
console.log('Load audiobook', audiobook)
this.audiobook = audiobook
this.startTime = startTime
this.playWhenReady = playWhenReady
@@ -88,6 +87,15 @@ export default class PlayerHandler {
this.player.on('stateChange', this.playerStateChange.bind(this))
this.player.on('timeupdate', this.playerTimeupdate.bind(this))
this.player.on('buffertimeUpdate', this.playerBufferTimeUpdate.bind(this))
this.player.on('error', this.playerError.bind(this))
}
playerError() {
// Switch to HLS stream on error
if (!this.isCasting && !this.currentStreamId && (this.player instanceof LocalPlayer)) {
console.log(`[PlayerHandler] Audio player error switching to HLS stream`)
this.prepare(true)
}
}
playerStateChange(state) {
@@ -117,8 +125,36 @@ export default class PlayerHandler {
this.ctx.setBufferTime(buffertime)
}
async prepare() {
var useHls = !this.isCasting
async prepare(forceHls = false) {
var useHls = false
var runningTotal = 0
var audioTracks = (this.audiobook.tracks || []).map((track) => {
var audioTrack = new AudioTrack(track)
audioTrack.startOffset = runningTotal
audioTrack.contentUrl = `/lib/${this.audiobook.libraryId}/${this.audiobook.folderId}/${track.path}?token=${this.userToken}`
audioTrack.mimeType = this.getMimeTypeForTrack(track)
audioTrack.canDirectPlay = !!this.player.playableMimetypes[audioTrack.mimeType]
runningTotal += audioTrack.duration
return audioTrack
})
// All html5 audio player plays use HLS unless experimental features is on
if (!this.isCasting) {
if (forceHls || !this.ctx.showExperimentalFeatures) {
useHls = true
} else {
// Use HLS if any audio track cannot be direct played
useHls = !!audioTracks.find(at => !at.canDirectPlay)
if (useHls) {
console.warn(`[PlayerHandler] An audio track cannot be direct played`, audioTracks.find(at => !at.canDirectPlay))
}
}
}
if (useHls) {
var stream = await this.ctx.$axios.$get(`/api/books/${this.audiobook.id}/stream`).catch((error) => {
console.error('Failed to start stream', error)
@@ -126,23 +162,30 @@ export default class PlayerHandler {
if (stream) {
console.log(`[PlayerHandler] prepare hls stream`, stream)
this.setHlsStream(stream)
} else {
console.error(`[PlayerHandler] Failed to start HLS stream`)
}
} else {
// Setup tracks
var runningTotal = 0
var audioTracks = (this.audiobook.tracks || []).map((track) => {
var audioTrack = new AudioTrack(track)
audioTrack.startOffset = runningTotal
audioTrack.contentUrl = `/lib/${this.audiobook.libraryId}/${this.audiobook.folderId}/${track.path}?token=${this.userToken}`
audioTrack.mimeType = (track.codec === 'm4b' || track.codec === 'm4a') ? 'audio/mp4' : `audio/${track.codec}`
runningTotal += audioTrack.duration
return audioTrack
})
this.setDirectPlay(audioTracks)
}
}
getMimeTypeForTrack(track) {
var ext = track.ext
if (ext === '.mp3' || ext === '.m4b' || ext === '.m4a') {
return 'audio/mpeg'
} else if (ext === '.mp4') {
return 'audio/mp4'
} else if (ext === '.ogg') {
return 'audio/ogg'
} else if (ext === '.aac' || ext === '.m4p') {
return 'audio/aac'
} else if (ext === '.flac') {
return 'audio/flac'
}
return 'audio/mpeg'
}
closePlayer() {
console.log('[PlayerHandler] CLose Player')
if (this.player) {
@@ -255,8 +298,7 @@ export default class PlayerHandler {
}
play() {
if (!this.player) return
this.player.play()
if (this.player) this.player.play()
}
pause() {
-6
View File
@@ -14,11 +14,6 @@ const DownloadStatus = {
FAILED: 3
}
const CoverDestination = {
METADATA: 0,
AUDIOBOOK: 1
}
const BookCoverAspectRatio = {
STANDARD: 0,
SQUARE: 1
@@ -32,7 +27,6 @@ const BookshelfView = {
const Constants = {
SupportedFileTypes,
DownloadStatus,
CoverDestination,
BookCoverAspectRatio,
BookshelfView
}
+1 -415
View File
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
"version": "1.6.66",
"version": "1.7.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -77,12 +77,6 @@
"@types/node": "*"
}
},
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"optional": true
},
"aborter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/aborter/-/aborter-1.1.0.tgz",
@@ -97,23 +91,6 @@
"negotiator": "0.6.2"
}
},
"adm-zip": {
"version": "0.4.16",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz",
"integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg=="
},
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"optional": true
},
"aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
"optional": true
},
"archiver": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.0.tgz",
@@ -169,33 +146,6 @@
"is-primitive": "^3.0.1"
}
},
"are-we-there-yet": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz",
"integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==",
"optional": true,
"requires": {
"delegates": "^1.0.0",
"readable-stream": "^2.0.6"
},
"dependencies": {
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"optional": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
}
}
},
"array-back": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz",
@@ -343,12 +293,6 @@
"responselike": "^2.0.0"
}
},
"chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"optional": true
},
"clone-response": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
@@ -357,12 +301,6 @@
"mimic-response": "^1.0.0"
}
},
"code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"optional": true
},
"command-line-args": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.0.tgz",
@@ -395,12 +333,6 @@
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
"optional": true
},
"content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
@@ -489,23 +421,11 @@
}
}
},
"deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"optional": true
},
"defer-to-connect": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="
},
"delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
"optional": true
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@@ -516,12 +436,6 @@
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
"optional": true
},
"dicer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz",
@@ -598,16 +512,6 @@
"base64-arraybuffer": "0.1.4"
}
},
"epub": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/epub/-/epub-1.2.1.tgz",
"integrity": "sha512-2GDDr2qcH3dvwX1lgwCQ3gki0CwwrxELLI005SauhT2TacJUiDqZrQuGuOSWEYIHX6ox5kXHpn1ZjsHqkNCb+g==",
"requires": {
"adm-zip": "^0.4.11",
"xml2js": "^0.4.23",
"zipfile": "^0.5.11"
}
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -744,36 +648,11 @@
"universalify": "^2.0.0"
}
},
"fs-minipass": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz",
"integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
"optional": true,
"requires": {
"minipass": "^2.6.0"
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"gauge": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
"optional": true,
"requires": {
"aproba": "^1.0.3",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.0",
"object-assign": "^4.1.0",
"signal-exit": "^3.0.0",
"string-width": "^1.0.1",
"strip-ansi": "^3.0.1",
"wide-align": "^1.1.0"
}
},
"get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
@@ -819,12 +698,6 @@
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
"integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ=="
},
"has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
"optional": true
},
"html-entities": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.2.tgz",
@@ -869,15 +742,6 @@
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
},
"ignore-walk": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz",
"integrity": "sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==",
"optional": true,
"requires": {
"minimatch": "^3.0.4"
}
},
"image-type": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/image-type/-/image-type-4.1.0.tgz",
@@ -900,12 +764,6 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"optional": true
},
"ip": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
@@ -916,15 +774,6 @@
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
},
"is-fullwidth-code-point": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
},
"is-primitive": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz",
@@ -1163,40 +1012,6 @@
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"optional": true
},
"minipass": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
}
},
"minizlib": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz",
"integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
"optional": true,
"requires": {
"minipass": "^2.9.0"
}
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"optional": true,
"requires": {
"minimist": "^1.2.5"
}
},
"moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
@@ -1215,40 +1030,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"nan": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==",
"optional": true
},
"needle": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz",
"integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==",
"optional": true,
"requires": {
"debug": "^3.2.6",
"iconv-lite": "^0.4.4",
"sax": "^1.2.4"
},
"dependencies": {
"debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"optional": true,
"requires": {
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"optional": true
}
}
},
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
@@ -1275,39 +1056,11 @@
"resolved": "https://registry.npmjs.org/node-ffprobe/-/node-ffprobe-3.0.0.tgz",
"integrity": "sha512-2LNTLStz2hw/urwo4xJ00TIOvthgepcl3tF4HB8BWnhJ4nhJ7S08YThapBHkGLYV+GUuY9pML/kX76+dqY2iUg=="
},
"node-pre-gyp": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz",
"integrity": "sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A==",
"optional": true,
"requires": {
"detect-libc": "^1.0.2",
"mkdirp": "^0.5.1",
"needle": "^2.2.1",
"nopt": "^4.0.1",
"npm-packlist": "^1.1.6",
"npmlog": "^4.0.2",
"rc": "^1.2.7",
"rimraf": "^2.6.1",
"semver": "^5.3.0",
"tar": "^4"
}
},
"node-stream-zip": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz",
"integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw=="
},
"nopt": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
"integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
"optional": true,
"requires": {
"abbrev": "1",
"osenv": "^0.1.4"
}
},
"normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -1318,50 +1071,6 @@
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="
},
"npm-bundled": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz",
"integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==",
"optional": true,
"requires": {
"npm-normalize-package-bin": "^1.0.1"
}
},
"npm-normalize-package-bin": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz",
"integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==",
"optional": true
},
"npm-packlist": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz",
"integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==",
"optional": true,
"requires": {
"ignore-walk": "^3.0.1",
"npm-bundled": "^1.0.1",
"npm-normalize-package-bin": "^1.0.1"
}
},
"npmlog": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"optional": true,
"requires": {
"are-we-there-yet": "~1.1.2",
"console-control-strings": "~1.1.0",
"gauge": "~2.7.3",
"set-blocking": "~2.0.0"
}
},
"number-is-nan": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"optional": true
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -1383,28 +1092,6 @@
"wrappy": "1"
}
},
"os-homedir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
"optional": true
},
"os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
"optional": true
},
"osenv": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
"integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
"optional": true,
"requires": {
"os-homedir": "^1.0.0",
"os-tmpdir": "^1.0.0"
}
},
"p-cancelable": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
@@ -1555,18 +1242,6 @@
"unpipe": "1.0.0"
}
},
"rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"optional": true,
"requires": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
}
},
"read-chunk": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/read-chunk/-/read-chunk-3.1.0.tgz",
@@ -1622,15 +1297,6 @@
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
"integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs="
},
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"optional": true,
"requires": {
"glob": "^7.1.3"
}
},
"ripstat": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ripstat/-/ripstat-1.1.1.tgz",
@@ -1721,12 +1387,6 @@
"send": "0.17.1"
}
},
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
"optional": true
},
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
@@ -1854,17 +1514,6 @@
"@babel/runtime": "^7.14.0"
}
},
"string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
"strip-ansi": "^3.0.0"
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
@@ -1873,44 +1522,6 @@
"safe-buffer": "~5.1.0"
}
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
},
"strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
"optional": true
},
"tar": {
"version": "4.4.19",
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz",
"integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==",
"optional": true,
"requires": {
"chownr": "^1.1.4",
"fs-minipass": "^1.2.7",
"minipass": "^2.9.0",
"minizlib": "^1.3.3",
"mkdirp": "^0.5.5",
"safe-buffer": "^5.2.1",
"yallist": "^3.1.1"
},
"dependencies": {
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"optional": true
}
}
},
"tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
@@ -1996,15 +1607,6 @@
"isexe": "^2.0.0"
}
},
"wide-align": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
"optional": true,
"requires": {
"string-width": "^1.0.2 || 2"
}
},
"with-open-file": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/with-open-file/-/with-open-file-0.1.7.tgz",
@@ -2044,12 +1646,6 @@
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
},
"yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"optional": true
},
"zip-stream": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz",
@@ -2059,16 +1655,6 @@
"compress-commons": "^4.1.0",
"readable-stream": "^3.6.0"
}
},
"zipfile": {
"version": "0.5.12",
"resolved": "https://registry.npmjs.org/zipfile/-/zipfile-0.5.12.tgz",
"integrity": "sha512-zA60gW+XgQBu/Q4qV3BCXNIDRald6Xi5UOPj3jWGlnkjmBHaKDwIz7kyXWV3kq7VEsQN/2t/IWjdXdKeVNm6Eg==",
"optional": true,
"requires": {
"nan": "~2.10.0",
"node-pre-gyp": "~0.10.2"
}
}
}
}
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
"version": "1.7.0",
"version": "1.7.2",
"description": "Self-hosted audiobook server for managing and playing audiobooks",
"main": "index.js",
"scripts": {
@@ -32,7 +32,6 @@
"bcryptjs": "^2.4.3",
"command-line-args": "^5.2.0",
"date-and-time": "^2.0.1",
"epub": "^1.2.1",
"express": "^4.17.1",
"express-fileupload": "^1.2.1",
"express-rate-limit": "^5.3.0",
+4 -1
View File
@@ -23,8 +23,11 @@ Audiobookshelf is a self-hosted audiobook server for managing and playing your a
* Multi-user support w/ custom permissions
* Keeps progress per user and syncs across devices
* Auto-detects library updates, no need to re-scan
* Upload full audiobooks and covers
* Upload audiobooks w/ bulk upload drag and drop folders
* Backup your metadata + automated daily backups
* Progressive Web App (PWA)
* Chromecast support on the web app
* Fetch metadata and cover art from several sources
Is there a feature you are looking for? [Suggest it](https://github.com/advplyr/audiobookshelf/issues/new/choose)
+3 -4
View File
@@ -18,7 +18,7 @@ const AuthorFinder = require('./AuthorFinder')
const FileSystemController = require('./controllers/FileSystemController')
class ApiController {
constructor(MetadataPath, db, auth, scanner, streamManager, rssFeeds, downloadManager, coverController, backupManager, watcher, cacheManager, emitter, clientEmitter) {
constructor(db, auth, scanner, streamManager, rssFeeds, downloadManager, coverController, backupManager, watcher, cacheManager, emitter, clientEmitter) {
this.db = db
this.auth = auth
this.scanner = scanner
@@ -31,10 +31,9 @@ class ApiController {
this.cacheManager = cacheManager
this.emitter = emitter
this.clientEmitter = clientEmitter
this.MetadataPath = MetadataPath
this.bookFinder = new BookFinder()
this.authorFinder = new AuthorFinder(this.MetadataPath)
this.authorFinder = new AuthorFinder()
this.router = express()
this.init()
@@ -287,7 +286,7 @@ class ApiController {
this.backupManager.updateCronSchedule()
}
await this.db.updateEntity('settings', this.db.serverSettings)
await this.db.updateServerSettings()
}
return res.json({
success: true,
+2 -3
View File
@@ -7,9 +7,8 @@ const Audnexus = require('./providers/Audnexus')
const { downloadFile } = require('./utils/fileUtils')
class AuthorFinder {
constructor(MetadataPath) {
this.MetadataPath = MetadataPath
this.AuthorPath = Path.join(MetadataPath, 'authors')
constructor() {
this.AuthorPath = Path.join(global.MetadataPath, 'authors')
this.audnexus = new Audnexus()
}
+7 -8
View File
@@ -13,9 +13,9 @@ const Logger = require('./Logger')
const Backup = require('./objects/Backup')
class BackupManager {
constructor(MetadataPath, Uid, Gid, db) {
this.MetadataPath = MetadataPath
this.BackupPath = Path.join(this.MetadataPath, 'backups')
constructor(Uid, Gid, db) {
this.BackupPath = Path.join(global.MetadataPath, 'backups')
this.MetadataBooksPath = Path.join(global.MetadataPath, 'books')
this.Uid = Uid
this.Gid = Gid
@@ -142,10 +142,9 @@ class BackupManager {
return
}
const zip = new StreamZip.async({ file: backup.fullPath })
await zip.extract('config/', this.db.ConfigPath)
await zip.extract('config/', global.ConfigPath)
if (backup.backupMetadataCovers) {
var metadataBooksPath = Path.join(this.MetadataPath, 'books')
await zip.extract('metadata-books/', metadataBooksPath)
await zip.extract('metadata-books/', this.MetadataBooksPath)
}
await this.db.reinit()
socket.emit('apply_backup_complete', true)
@@ -157,7 +156,7 @@ class BackupManager {
var lastBackup = this.backups.shift()
const zip = new StreamZip.async({ file: lastBackup.fullPath })
await zip.extract('config/', this.db.ConfigPath)
await zip.extract('config/', global.ConfigPath)
console.log('Set Last Backup')
await this.db.reinit()
}
@@ -196,7 +195,7 @@ class BackupManager {
async runBackup() {
// Check if Metadata Path is inside Config Path (otherwise there will be an infinite loop as the archiver tries to zip itself)
Logger.info(`[BackupManager] Running Backup`)
var metadataBooksPath = this.serverSettings.backupMetadataCovers ? Path.join(this.MetadataPath, 'books') : null
var metadataBooksPath = this.serverSettings.backupMetadataCovers ? this.MetadataBooksPath : null
var newBackup = new Backup()
+2 -3
View File
@@ -5,9 +5,8 @@ const Logger = require('./Logger')
const { resizeImage } = require('./utils/ffmpegHelpers')
class CacheManager {
constructor(MetadataPath) {
this.MetadataPath = MetadataPath
this.CachePath = Path.join(this.MetadataPath, 'cache')
constructor() {
this.CachePath = Path.join(global.MetadataPath, 'cache')
this.CoverCachePath = Path.join(this.CachePath, 'covers')
}
+3 -6
View File
@@ -6,21 +6,18 @@ const readChunk = require('read-chunk')
const imageType = require('image-type')
const globals = require('./utils/globals')
const { CoverDestination } = require('./utils/constants')
const { downloadFile } = require('./utils/fileUtils')
class CoverController {
constructor(db, cacheManager, MetadataPath, AudiobookPath) {
constructor(db, cacheManager) {
this.db = db
this.cacheManager = cacheManager
this.MetadataPath = MetadataPath.replace(/\\/g, '/')
this.BookMetadataPath = Path.posix.join(this.MetadataPath, 'books')
this.AudiobookPath = AudiobookPath
this.BookMetadataPath = Path.posix.join(global.MetadataPath, 'books')
}
getCoverDirectory(audiobook) {
if (this.db.serverSettings.coverDestination === CoverDestination.AUDIOBOOK) {
if (this.db.serverSettings.storeCoverWithBook) {
return {
fullPath: audiobook.fullPath,
relPath: '/s/book/' + audiobook.id
+54 -14
View File
@@ -12,17 +12,14 @@ const Author = require('./objects/Author')
const ServerSettings = require('./objects/ServerSettings')
class Db {
constructor(ConfigPath, AudiobookPath) {
this.ConfigPath = ConfigPath
this.AudiobookPath = AudiobookPath
this.AudiobooksPath = Path.join(ConfigPath, 'audiobooks')
this.UsersPath = Path.join(ConfigPath, 'users')
this.SessionsPath = Path.join(ConfigPath, 'sessions')
this.LibrariesPath = Path.join(ConfigPath, 'libraries')
this.SettingsPath = Path.join(ConfigPath, 'settings')
this.CollectionsPath = Path.join(ConfigPath, 'collections')
this.AuthorsPath = Path.join(ConfigPath, 'authors')
constructor() {
this.AudiobooksPath = Path.join(global.ConfigPath, 'audiobooks')
this.UsersPath = Path.join(global.ConfigPath, 'users')
this.SessionsPath = Path.join(global.ConfigPath, 'sessions')
this.LibrariesPath = Path.join(global.ConfigPath, 'libraries')
this.SettingsPath = Path.join(global.ConfigPath, 'settings')
this.CollectionsPath = Path.join(global.ConfigPath, 'collections')
this.AuthorsPath = Path.join(global.ConfigPath, 'authors')
this.audiobooksDb = new njodb.Database(this.AudiobooksPath)
this.usersDb = new njodb.Database(this.UsersPath)
@@ -88,7 +85,7 @@ class Db {
name: 'Main',
folder: { // Generates default folder
id: 'audiobooks',
fullPath: this.AudiobookPath,
fullPath: global.AudiobookPath,
libraryId: 'main'
}
})
@@ -128,6 +125,7 @@ class Db {
this.serverSettings = new ServerSettings()
await this.insertEntity('settings', this.serverSettings)
}
global.ServerSettings = this.serverSettings.toJSON()
}
async load() {
@@ -170,11 +168,19 @@ class Db {
// Update server version in server settings
if (this.previousVersion) {
this.serverSettings.version = version
await this.updateEntity('settings', this.serverSettings)
await this.updateServerSettings()
}
}
updateAudiobook(audiobook) {
async updateAudiobook(audiobook) {
if (audiobook && audiobook.saveAbMetadata) {
// TODO: Book may have updates where this save is not necessary
// add check first if metadata update is needed
await audiobook.saveAbMetadata()
} else {
Logger.error(`[Db] Invalid audiobook object passed to updateAudiobook`, audiobook)
}
return this.audiobooksDb.update((record) => record.id === audiobook.id, () => audiobook).then((results) => {
Logger.debug(`[DB] Audiobook updated ${results.updated}`)
return true
@@ -184,6 +190,28 @@ class Db {
})
}
insertAudiobook(audiobook) {
return this.insertAudiobooks([audiobook])
}
async insertAudiobooks(audiobooks) {
// TODO: Books may have updates where this save is not necessary
// add check first if metadata update is needed
await Promise.all(audiobooks.map(async (ab) => {
if (ab && ab.saveAbMetadata) return ab.saveAbMetadata()
return null
}))
return this.audiobooksDb.insert(audiobooks).then((results) => {
Logger.debug(`[DB] Audiobooks inserted ${results.inserted}`)
this.audiobooks = this.audiobooks.concat(audiobooks)
return true
}).catch((error) => {
Logger.error(`[DB] Audiobooks insert failed ${error}`)
return false
})
}
updateUserStream(userId, streamId) {
return this.usersDb.update((record) => record.id === userId, (user) => {
user.stream = streamId
@@ -201,6 +229,11 @@ class Db {
})
}
updateServerSettings() {
global.ServerSettings = this.serverSettings.toJSON()
return this.updateEntity('settings', this.serverSettings)
}
insertEntities(entityName, entities) {
var entityDb = this.getEntityDb(entityName)
return entityDb.insert(entities).then((results) => {
@@ -308,5 +341,12 @@ class Db {
return []
})
}
// Check if server was updated and previous version was earlier than param
checkPreviousVersionIsBefore(version) {
if (!this.previousVersion) return false
// true if version > previousVersion
return version.localeCompare(this.previousVersion) >= 0
}
}
module.exports = Db
+3 -5
View File
@@ -11,14 +11,12 @@ const { writeConcatFile, writeMetadataFile } = require('./utils/ffmpegHelpers')
const { getFileSize } = require('./utils/fileUtils')
const TAG = 'DownloadManager'
class DownloadManager {
constructor(db, MetadataPath, AudiobookPath, Uid, Gid) {
constructor(db, Uid, Gid) {
this.Uid = Uid
this.Gid = Gid
this.db = db
this.MetadataPath = MetadataPath
this.AudiobookPath = AudiobookPath
this.downloadDirPath = Path.join(this.MetadataPath, 'downloads')
this.downloadDirPath = Path.join(global.MetadataPath, 'downloads')
this.pendingDownloads = []
this.downloads = []
@@ -248,7 +246,7 @@ class DownloadManager {
// Supporting old local file prefix
var bookCoverPath = audiobook.book.cover ? audiobook.book.cover.replace(/\\/g, '/') : null
if (!_cover && bookCoverPath && bookCoverPath.startsWith('/local')) {
_cover = Path.posix.join(this.AudiobookPath.replace(/\\/g, '/'), _cover.replace('/local', ''))
_cover = Path.posix.join(global.AudiobookPath, _cover.replace('/local', ''))
Logger.debug('Local cover url', _cover)
}
+2 -3
View File
@@ -4,12 +4,11 @@ const fs = require('fs-extra')
const Logger = require('./Logger')
class HlsController {
constructor(db, auth, streamManager, emitter, StreamsPath) {
constructor(db, auth, streamManager, emitter) {
this.db = db
this.auth = auth
this.streamManager = streamManager
this.emitter = emitter
this.StreamsPath = StreamsPath
this.router = express()
this.init()
@@ -27,7 +26,7 @@ class HlsController {
async streamFileRequest(req, res) {
var streamId = req.params.stream
var fullFilePath = Path.join(this.StreamsPath, streamId, req.params.file)
var fullFilePath = Path.join(this.streamManager.StreamsPath, streamId, req.params.file)
// development test stream - ignore
if (streamId === 'test') {
+2 -3
View File
@@ -8,11 +8,10 @@ const Logger = require('./Logger')
const TAG = '[LogManager]'
class LogManager {
constructor(MetadataPath, db) {
constructor(db) {
this.db = db
this.MetadataPath = MetadataPath
this.logDirPath = Path.join(this.MetadataPath, 'logs')
this.logDirPath = Path.join(global.MetadataPath, 'logs')
this.dailyLogDirPath = Path.join(this.logDirPath, 'daily')
this.currentDailyLog = null
+34 -22
View File
@@ -35,27 +35,35 @@ class Server {
this.Uid = isNaN(UID) ? 0 : Number(UID)
this.Gid = isNaN(GID) ? 0 : Number(GID)
this.Host = '0.0.0.0'
this.ConfigPath = Path.normalize(CONFIG_PATH)
this.AudiobookPath = Path.normalize(AUDIOBOOK_PATH)
this.MetadataPath = Path.normalize(METADATA_PATH)
global.Uid = this.Uid
global.Gid = this.Gid
global.ConfigPath = Path.normalize(CONFIG_PATH)
global.AudiobookPath = Path.normalize(AUDIOBOOK_PATH)
global.MetadataPath = Path.normalize(METADATA_PATH)
// Fix backslash if not on Windows
if (process.platform !== 'win32') {
global.ConfigPath = global.ConfigPath.replace(/\\/g, '/')
global.AudiobookPath = global.AudiobookPath.replace(/\\/g, '/')
global.MetadataPath = global.MetadataPath.replace(/\\/g, '/')
}
fs.ensureDirSync(CONFIG_PATH, 0o774)
fs.ensureDirSync(METADATA_PATH, 0o774)
fs.ensureDirSync(AUDIOBOOK_PATH, 0o774)
fs.ensureDirSync(global.ConfigPath, 0o774)
fs.ensureDirSync(global.MetadataPath, 0o774)
fs.ensureDirSync(global.AudiobookPath, 0o774)
this.db = new Db(this.ConfigPath, this.AudiobookPath)
this.db = new Db()
this.auth = new Auth(this.db)
this.backupManager = new BackupManager(this.MetadataPath, this.Uid, this.Gid, this.db)
this.logManager = new LogManager(this.MetadataPath, this.db)
this.cacheManager = new CacheManager(this.MetadataPath)
this.watcher = new Watcher(this.AudiobookPath)
this.coverController = new CoverController(this.db, this.cacheManager, this.MetadataPath, this.AudiobookPath)
this.scanner = new Scanner(this.AudiobookPath, this.MetadataPath, this.db, this.coverController, this.emitter.bind(this))
this.backupManager = new BackupManager(this.Uid, this.Gid, this.db)
this.logManager = new LogManager(this.db)
this.cacheManager = new CacheManager()
this.watcher = new Watcher()
this.coverController = new CoverController(this.db, this.cacheManager)
this.scanner = new Scanner(this.db, this.coverController, this.emitter.bind(this))
this.streamManager = new StreamManager(this.db, this.MetadataPath, this.emitter.bind(this), this.clientEmitter.bind(this))
this.streamManager = new StreamManager(this.db, this.emitter.bind(this), this.clientEmitter.bind(this))
this.rssFeeds = new RssFeeds(this.Port, this.db)
this.downloadManager = new DownloadManager(this.db, this.MetadataPath, this.AudiobookPath, this.Uid, this.Gid)
this.apiController = new ApiController(this.MetadataPath, this.db, this.auth, this.scanner, this.streamManager, this.rssFeeds, this.downloadManager, this.coverController, this.backupManager, this.watcher, this.cacheManager, this.emitter.bind(this), this.clientEmitter.bind(this))
this.downloadManager = new DownloadManager(this.db, this.Uid, this.Gid)
this.apiController = new ApiController(this.db, this.auth, this.scanner, this.streamManager, this.rssFeeds, this.downloadManager, this.coverController, this.backupManager, this.watcher, this.cacheManager, this.emitter.bind(this), this.clientEmitter.bind(this))
this.hlsController = new HlsController(this.db, this.auth, this.streamManager, this.emitter.bind(this), this.streamManager.StreamsPath)
Logger.logManager = this.logManager
@@ -126,6 +134,10 @@ class Server {
Logger.info(`[Server] Running scan for duplicate book IDs`)
await this.scanner.fixDuplicateIds()
}
// If server upgrade and last version was 1.7.0 or earlier - add abmetadata files
// if (this.db.checkPreviousVersionIsBefore('1.7.1')) {
// TODO: wait until stable
// }
if (this.db.serverSettings.scannerDisableWatcher) {
Logger.info(`[Server] Watcher is disabled`)
@@ -155,10 +167,10 @@ class Server {
app.use(express.static(distPath))
// Old static path for covers
app.use('/local', this.authMiddleware.bind(this), express.static(this.AudiobookPath))
app.use('/local', this.authMiddleware.bind(this), express.static(global.AudiobookPath))
// Metadata folder static path
app.use('/metadata', this.authMiddleware.bind(this), express.static(this.MetadataPath))
app.use('/metadata', this.authMiddleware.bind(this), express.static(global.MetadataPath))
// Downloads folder static path
app.use('/downloads', this.authMiddleware.bind(this), express.static(this.downloadManager.downloadDirPath))
@@ -349,7 +361,7 @@ class Server {
// Remove unused /metadata/books/{id} folders
async purgeMetadata() {
var booksMetadata = Path.join(this.MetadataPath, 'books')
var booksMetadata = Path.join(global.MetadataPath, 'books')
var booksMetadataExists = await fs.pathExists(booksMetadata)
if (!booksMetadataExists) return
var foldersInBooksMetadata = await fs.readdir(booksMetadata)
@@ -622,9 +634,9 @@ class Server {
const initialPayload = {
serverSettings: this.serverSettings.toJSON(),
audiobookPath: this.AudiobookPath,
metadataPath: this.MetadataPath,
configPath: this.ConfigPath,
audiobookPath: global.AudiobookPath,
metadataPath: global.MetadataPath,
configPath: global.ConfigPath,
user: client.user.toJSONForBrowser(),
stream: client.stream || null,
librariesScanning: this.scanner.librariesScanning,
+4 -5
View File
@@ -5,15 +5,14 @@ const fs = require('fs-extra')
const Path = require('path')
class StreamManager {
constructor(db, MetadataPath, emitter, clientEmitter) {
constructor(db, emitter, clientEmitter) {
this.db = db
this.emitter = emitter
this.clientEmitter = clientEmitter
this.MetadataPath = MetadataPath
this.streams = []
this.StreamsPath = Path.join(this.MetadataPath, 'streams')
this.StreamsPath = Path.join(global.MetadataPath, 'streams')
}
get audiobooks() {
@@ -68,12 +67,12 @@ class StreamManager {
async tempCheckStrayStreams() {
try {
var dirs = await fs.readdir(this.MetadataPath)
var dirs = await fs.readdir(global.MetadataPath)
if (!dirs || !dirs.length) return true
await Promise.all(dirs.map(async (dirname) => {
if (dirname !== 'streams' && dirname !== 'books' && dirname !== 'downloads' && dirname !== 'backups' && dirname !== 'logs' && dirname !== 'cache') {
var fullPath = Path.join(this.MetadataPath, dirname)
var fullPath = Path.join(global.MetadataPath, dirname)
Logger.warn(`Removing OLD Orphan Stream ${dirname}`)
return fs.remove(fullPath)
}
+1 -1
View File
@@ -264,7 +264,7 @@ class BookController {
return res.sendStatus(403)
}
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
if (!audiobook || !audiobook.book.cover) return res.sendStatus(404)
if (!audiobook) return res.sendStatus(404)
// Check user can access this audiobooks library
if (!req.user.checkCanAccessLibrary(audiobook.libraryId)) {
+12
View File
@@ -11,6 +11,9 @@ class AudioFile {
this.ext = null
this.path = null
this.fullPath = null
this.mtimeMs = null
this.ctimeMs = null
this.birthtimeMs = null
this.addedAt = null
this.trackNumFromMeta = null
@@ -51,6 +54,9 @@ class AudioFile {
ext: this.ext,
path: this.path,
fullPath: this.fullPath,
mtimeMs: this.mtimeMs,
ctimeMs: this.ctimeMs,
birthtimeMs: this.birthtimeMs,
addedAt: this.addedAt,
trackNumFromMeta: this.trackNumFromMeta,
discNumFromMeta: this.discNumFromMeta,
@@ -82,6 +88,9 @@ class AudioFile {
this.ext = data.ext
this.path = data.path
this.fullPath = data.fullPath
this.mtimeMs = data.mtimeMs || 0
this.ctimeMs = data.ctimeMs || 0
this.birthtimeMs = data.birthtimeMs || 0
this.addedAt = data.addedAt
this.manuallyVerified = !!data.manuallyVerified
this.invalid = !!data.invalid
@@ -124,6 +133,9 @@ class AudioFile {
this.ext = fileData.ext
this.path = fileData.path
this.fullPath = fileData.fullPath
this.mtimeMs = fileData.mtimeMs || 0
this.ctimeMs = fileData.ctimeMs || 0
this.birthtimeMs = fileData.birthtimeMs || 0
this.addedAt = Date.now()
this.trackNumFromMeta = fileData.trackNumFromMeta
+119 -25
View File
@@ -1,10 +1,11 @@
const Path = require('path')
const fs = require('fs-extra')
const { bytesPretty, readTextFile } = require('../utils/fileUtils')
const { comparePaths, getIno, getId, elapsedPretty } = require('../utils/index')
const { bytesPretty, readTextFile, getIno } = require('../utils/fileUtils')
const { comparePaths, getId, elapsedPretty } = require('../utils/index')
const { parseOpfMetadataXML } = require('../utils/parseOpfMetadata')
const { extractCoverArt } = require('../utils/ffmpegHelpers')
const nfoGenerator = require('../utils/nfoGenerator')
const abmetadataGenerator = require('../utils/abmetadataGenerator')
const Logger = require('../Logger')
const Book = require('./Book')
const AudioTrack = require('./AudioTrack')
@@ -21,6 +22,9 @@ class Audiobook {
this.path = null
this.fullPath = null
this.mtimeMs = null
this.ctimeMs = null
this.birthtimeMs = null
this.addedAt = null
this.lastUpdate = null
this.lastScan = null
@@ -44,6 +48,9 @@ class Audiobook {
if (audiobook) {
this.construct(audiobook)
}
// Temp flags
this.isSavingMetadata = false
}
construct(audiobook) {
@@ -53,6 +60,9 @@ class Audiobook {
this.folderId = audiobook.folderId || 'audiobooks'
this.path = audiobook.path
this.fullPath = audiobook.fullPath
this.mtimeMs = audiobook.mtimeMs || 0
this.ctimeMs = audiobook.ctimeMs || 0
this.birthtimeMs = audiobook.birthtimeMs || 0
this.addedAt = audiobook.addedAt
this.lastUpdate = audiobook.lastUpdate || this.addedAt
this.lastScan = audiobook.lastScan || null
@@ -175,6 +185,9 @@ class Audiobook {
folderId: this.folderId,
path: this.path,
fullPath: this.fullPath,
mtimeMs: this.mtimeMs,
ctimeMs: this.ctimeMs,
birthtimeMs: this.birthtimeMs,
addedAt: this.addedAt,
lastUpdate: this.lastUpdate,
lastScan: this.lastScan,
@@ -201,6 +214,9 @@ class Audiobook {
tags: this.tags,
path: this.path,
fullPath: this.fullPath,
mtimeMs: this.mtimeMs,
ctimeMs: this.ctimeMs,
birthtimeMs: this.birthtimeMs,
addedAt: this.addedAt,
lastUpdate: this.lastUpdate,
duration: this.duration,
@@ -224,6 +240,9 @@ class Audiobook {
folderId: this.folderId,
path: this.path,
fullPath: this.fullPath,
mtimeMs: this.mtimeMs,
ctimeMs: this.ctimeMs,
birthtimeMs: this.birthtimeMs,
addedAt: this.addedAt,
lastUpdate: this.lastUpdate,
duration: this.duration,
@@ -331,6 +350,9 @@ class Audiobook {
this.path = data.path
this.fullPath = data.fullPath
this.mtimeMs = data.mtimeMs || 0
this.ctimeMs = data.ctimeMs || 0
this.birthtimeMs = data.birthtimeMs || 0
this.addedAt = Date.now()
this.lastUpdate = this.addedAt
@@ -422,13 +444,8 @@ class Audiobook {
hasUpdates = true
}
if (payload.book) {
if (!this.book) {
this.setBook(payload.book)
hasUpdates = true
} else if (this.book.update(payload.book)) {
hasUpdates = true
}
if (payload.book && this.book.update(payload.book)) {
hasUpdates = true
}
if (hasUpdates) {
@@ -523,7 +540,7 @@ class Audiobook {
}
// On scan check other files found with other files saved
async syncOtherFiles(newOtherFiles, metadataPath, opfMetadataOverrideDetails, forceRescan = false) {
async syncOtherFiles(newOtherFiles, opfMetadataOverrideDetails) {
var hasUpdates = false
var currOtherFileNum = this.otherFiles.length
@@ -532,6 +549,8 @@ class Audiobook {
var alreadyHasDescTxt = otherFilenamesAlreadyInBook.includes('desc.txt')
var alreadyHasReaderTxt = otherFilenamesAlreadyInBook.includes('reader.txt')
var existingAbMetadata = this.otherFiles.find(file => file.filename === 'metadata.abs')
// Filter out other files no longer in directory
var newOtherFilePaths = newOtherFiles.map(f => f.path)
this.otherFiles = this.otherFiles.filter(f => newOtherFilePaths.includes(f.path))
@@ -540,9 +559,9 @@ class Audiobook {
hasUpdates = true
}
// If desc.txt is new or forcing rescan then read it and update description (will overwrite)
// If desc.txt is new then read it and update description (will overwrite)
var descriptionTxt = newOtherFiles.find(file => file.filename === 'desc.txt')
if (descriptionTxt && (!alreadyHasDescTxt || forceRescan)) {
if (descriptionTxt && !alreadyHasDescTxt) {
var newDescription = await readTextFile(descriptionTxt.fullPath)
if (newDescription) {
Logger.debug(`[Audiobook] Sync Other File desc.txt: ${newDescription}`)
@@ -550,9 +569,9 @@ class Audiobook {
hasUpdates = true
}
}
// If reader.txt is new or forcing rescan then read it and update narrator (will overwrite)
// If reader.txt is new then read it and update narrator (will overwrite)
var readerTxt = newOtherFiles.find(file => file.filename === 'reader.txt')
if (readerTxt && (!alreadyHasReaderTxt || forceRescan)) {
if (readerTxt && !alreadyHasReaderTxt) {
var newReader = await readTextFile(readerTxt.fullPath)
if (newReader) {
Logger.debug(`[Audiobook] Sync Other File reader.txt: ${newReader}`)
@@ -561,7 +580,28 @@ class Audiobook {
}
}
// If OPF file and was not already there
// If metadata.abs is new OR modified then read it and set all defined keys (will overwrite)
var metadataAbs = newOtherFiles.find(file => file.filename === 'metadata.abs')
var shouldUpdateAbs = !!metadataAbs && (metadataAbs.modified || !existingAbMetadata)
if (metadataAbs && metadataAbs.modified) {
Logger.debug(`[Audiobook] metadata.abs file was modified for "${this.title}"`)
}
if (shouldUpdateAbs) {
var abmetadataText = await readTextFile(metadataAbs.fullPath)
if (abmetadataText) {
var metadataUpdateObject = abmetadataGenerator.parse(abmetadataText)
if (metadataUpdateObject && metadataUpdateObject.book) {
if (this.update(metadataUpdateObject)) {
Logger.debug(`[Audiobook] Some details were updated from metadata.abs for "${this.title}"`, metadataUpdateObject)
hasUpdates = true
}
}
}
}
// If OPF file and was not already there OR prefer opf metadata
var metadataOpf = newOtherFiles.find(file => file.ext === '.opf' || file.filename === 'metadata.xml')
if (metadataOpf && (!otherFilenamesAlreadyInBook.includes(metadataOpf.filename) || opfMetadataOverrideDetails)) {
var xmlText = await readTextFile(metadataOpf.fullPath)
@@ -640,7 +680,7 @@ class Audiobook {
if (bookCoverPath && bookCoverPath.startsWith('/metadata')) {
// Fixing old cover paths
if (!this.book.coverFullPath) {
this.book.coverFullPath = Path.join(metadataPath, this.book.cover.substr('/metadata/'.length)).replace(/\\/g, '/').replace(/\/\//g, '/')
this.book.coverFullPath = Path.join(global.MetadataPath, this.book.cover.substr('/metadata/'.length)).replace(/\\/g, '/').replace(/\/\//g, '/')
Logger.debug(`[Audiobook] Metadata cover full path set "${this.book.coverFullPath}" for "${this.title}"`)
hasUpdates = true
}
@@ -797,9 +837,10 @@ class Audiobook {
return false
}
// Look for desc.txt and reader.txt and update details if found
// Look for desc.txt, reader.txt, metadata.abs and opf file then update details if found
async saveDataFromTextFiles(opfMetadataOverrideDetails) {
var bookUpdatePayload = {}
var descriptionText = await this.fetchTextFromTextFile('desc.txt')
if (descriptionText) {
Logger.debug(`[Audiobook] "${this.title}" found desc.txt updating description with "${descriptionText.slice(0, 20)}..."`)
@@ -811,6 +852,22 @@ class Audiobook {
bookUpdatePayload.narrator = readerText
}
// abmetadata will always overwrite
var abmetadataText = await this.fetchTextFromTextFile('metadata.abs')
if (abmetadataText) {
var metadataUpdateObject = abmetadataGenerator.parse(abmetadataText)
if (metadataUpdateObject && metadataUpdateObject.book) {
Logger.debug(`[Audiobook] "${this.title}" found metadata.abs file`)
for (const key in metadataUpdateObject.book) {
var value = metadataUpdateObject.book[key]
if (key && value !== undefined) {
bookUpdatePayload[key] = value
}
}
}
}
// Opf only overwrites if detail is empty
var metadataOpf = this.otherFiles.find(file => file.isOPFFile || file.filename === 'metadata.xml')
if (metadataOpf) {
var xmlText = await readTextFile(metadataOpf.fullPath)
@@ -870,12 +927,6 @@ class Audiobook {
}
}
if (existingFile.filename !== fileFound.filename) {
existingFile.filename = fileFound.filename
existingFile.ext = fileFound.ext
hasUpdated = true
}
if (existingFile.path !== fileFound.path) {
existingFile.path = fileFound.path
existingFile.fullPath = fileFound.fullPath
@@ -885,6 +936,20 @@ class Audiobook {
hasUpdated = true
}
var keysToCheck = ['filename', 'ext', 'mtimeMs', 'ctimeMs', 'birthtimeMs', 'size']
keysToCheck.forEach((key) => {
if (existingFile[key] !== fileFound[key]) {
// Add modified flag on file data object if exists and was changed
if (key === 'mtimeMs' && existingFile[key]) {
fileFound.modified = true
}
existingFile[key] = fileFound[key]
hasUpdated = true
}
})
if (!isAudioFile && existingFile.filetype !== fileFound.filetype) {
existingFile.filetype = fileFound.filetype
hasUpdated = true
@@ -924,6 +989,14 @@ class Audiobook {
hasUpdated = true
}
var keysToCheck = ['mtimeMs', 'ctimeMs', 'birthtimeMs']
keysToCheck.forEach((key) => {
if (dataFound[key] != this[key]) {
this[key] = dataFound[key] || 0
hasUpdated = true
}
})
var newAudioFileData = []
var newOtherFileData = []
var existingAudioFileData = []
@@ -1014,14 +1087,14 @@ class Audiobook {
}
// Temp fix for cover is set but coverFullPath is not set
fixFullCoverPath(metadataPath) {
fixFullCoverPath() {
if (!this.book.cover) return
var bookCoverPath = this.book.cover.replace(/\\/g, '/')
var newFullCoverPath = null
if (bookCoverPath.startsWith('/s/book/')) {
newFullCoverPath = Path.join(this.fullPath, bookCoverPath.substr(`/s/book/${this.id}`.length)).replace(/\/\//g, '/')
} else if (bookCoverPath.startsWith('/metadata/')) {
newFullCoverPath = Path.join(metadataPath, bookCoverPath.substr('/metadata/'.length)).replace(/\/\//g, '/')
newFullCoverPath = Path.join(global.MetadataPath, bookCoverPath.substr('/metadata/'.length)).replace(/\/\//g, '/')
}
if (newFullCoverPath) {
Logger.debug(`[Audiobook] "${this.title}" fixing full cover path "${this.book.cover}" => "${newFullCoverPath}"`)
@@ -1030,5 +1103,26 @@ class Audiobook {
}
return false
}
async saveAbMetadata() {
if (this.isSavingMetadata) return
this.isSavingMetadata = true
var metadataPath = Path.join(global.MetadataPath, 'books', this.id)
if (global.ServerSettings.storeMetadataWithBook) {
metadataPath = this.fullPath
} else {
// Make sure metadata book dir exists
await fs.ensureDir(metadataPath)
}
metadataPath = Path.join(metadataPath, 'metadata.abs')
return abmetadataGenerator.generate(this, metadataPath).then((success) => {
this.isSavingMetadata = false
if (!success) Logger.error(`[Audiobook] Failed saving abmetadata to "${metadataPath}"`)
else Logger.debug(`[Audiobook] Success saving abmetadata to "${metadataPath}"`)
return success
})
}
}
module.exports = Audiobook
+17
View File
@@ -6,6 +6,11 @@ class AudiobookFile {
this.ext = null
this.path = null
this.fullPath = null
this.size = null
this.mtimeMs = null
this.ctimeMs = null
this.birthtimeMs = null
this.addedAt = null
if (data) {
@@ -25,6 +30,10 @@ class AudiobookFile {
ext: this.ext,
path: this.path,
fullPath: this.fullPath,
size: this.size,
mtimeMs: this.mtimeMs,
ctimeMs: this.ctimeMs,
birthtimeMs: this.birthtimeMs,
addedAt: this.addedAt
}
}
@@ -36,6 +45,10 @@ class AudiobookFile {
this.ext = data.ext
this.path = data.path
this.fullPath = data.fullPath
this.size = data.size || 0
this.mtimeMs = data.mtimeMs || 0
this.ctimeMs = data.ctimeMs || 0
this.birthtimeMs = data.birthtimeMs || 0
this.addedAt = data.addedAt
}
@@ -46,6 +59,10 @@ class AudiobookFile {
this.ext = data.ext
this.path = data.path
this.fullPath = data.fullPath
this.size = data.size || 0
this.mtimeMs = data.mtimeMs || 0
this.ctimeMs = data.ctimeMs || 0
this.birthtimeMs = data.birthtimeMs || 0
this.addedAt = Date.now()
}
}
+13
View File
@@ -8,6 +8,7 @@ class Library {
this.folders = []
this.displayOrder = 1
this.icon = 'database'
this.mediaType = 'default'
this.provider = 'google'
this.disableWatcher = false
@@ -31,6 +32,7 @@ class Library {
this.folders = (library.folders || []).map(f => new Folder(f))
this.displayOrder = library.displayOrder || 1
this.icon = library.icon || 'database'
this.mediaType = library.mediaType || 'default'
this.provider = library.provider || 'google'
this.disableWatcher = !!library.disableWatcher
@@ -45,6 +47,7 @@ class Library {
folders: (this.folders || []).map(f => f.toJSON()),
displayOrder: this.displayOrder,
icon: this.icon,
mediaType: this.mediaType,
provider: this.provider,
disableWatcher: this.disableWatcher,
createdAt: this.createdAt,
@@ -71,6 +74,7 @@ class Library {
}
this.displayOrder = data.displayOrder || 1
this.icon = data.icon || 'database'
this.mediaType = data.mediaType || 'default'
this.disableWatcher = !!data.disableWatcher
this.createdAt = Date.now()
this.lastUpdate = Date.now()
@@ -86,6 +90,15 @@ class Library {
this.provider = payload.provider
hasUpdates = true
}
if (payload.mediaType && payload.mediaType !== this.mediaType) {
this.mediaType = payload.mediaType
hasUpdates = true
}
if (payload.icon && payload.icon !== this.icon) {
this.icon = payload.icon
hasUpdates = true
}
if (payload.disableWatcher !== this.disableWatcher) {
this.disableWatcher = !!payload.disableWatcher
hasUpdates = true
+11 -7
View File
@@ -1,4 +1,4 @@
const { CoverDestination, BookCoverAspectRatio, BookshelfView } = require('../utils/constants')
const { BookCoverAspectRatio, BookshelfView } = require('../utils/constants')
const Logger = require('../Logger')
class ServerSettings {
@@ -18,8 +18,8 @@ class ServerSettings {
this.scannerDisableWatcher = false
// Metadata
this.coverDestination = CoverDestination.METADATA
this.saveMetadataFile = false
this.storeCoverWithBook = false
this.storeMetadataWithBook = false
// Security/Rate limits
this.rateLimitLoginRequests = 10
@@ -59,8 +59,12 @@ class ServerSettings {
this.scannerPreferOpfMetadata = !!settings.scannerPreferOpfMetadata
this.scannerDisableWatcher = !!settings.scannerDisableWatcher
this.coverDestination = settings.coverDestination || CoverDestination.METADATA
this.saveMetadataFile = !!settings.saveMetadataFile
this.storeCoverWithBook = settings.storeCoverWithBook
if (this.storeCoverWithBook == undefined) { // storeCoverWithBook added in 1.7.1 to replace coverDestination
this.storeCoverWithBook = !!settings.coverDestination
}
this.storeMetadataWithBook = !!settings.storeCoverWithBook
this.rateLimitLoginRequests = !isNaN(settings.rateLimitLoginRequests) ? Number(settings.rateLimitLoginRequests) : 10
this.rateLimitLoginWindow = !isNaN(settings.rateLimitLoginWindow) ? Number(settings.rateLimitLoginWindow) : 10 * 60 * 1000 // 10 Minutes
@@ -95,8 +99,8 @@ class ServerSettings {
scannerPreferAudioMetadata: this.scannerPreferAudioMetadata,
scannerPreferOpfMetadata: this.scannerPreferOpfMetadata,
scannerDisableWatcher: this.scannerDisableWatcher,
coverDestination: this.coverDestination,
saveMetadataFile: !!this.saveMetadataFile,
storeCoverWithBook: this.storeCoverWithBook,
storeMetadataWithBook: this.storeMetadataWithBook,
rateLimitLoginRequests: this.rateLimitLoginRequests,
rateLimitLoginWindow: this.rateLimitLoginWindow,
backupSchedule: this.backupSchedule,
+2 -3
View File
@@ -1,10 +1,9 @@
const AuthorFinder = require('../AuthorFinder')
class AuthorScanner {
constructor(db, MetadataPath) {
constructor(db) {
this.db = db
this.MetadataPath = MetadataPath
this.authorFinder = new AuthorFinder(MetadataPath)
this.authorFinder = new AuthorFinder()
}
getUniqueAuthors() {
+3 -5
View File
@@ -1,5 +1,3 @@
const { CoverDestination } = require('../utils/constants')
class ScanOptions {
constructor(options) {
this.forceRescan = false
@@ -7,7 +5,7 @@ class ScanOptions {
// Server settings
this.parseSubtitles = false
this.findCovers = false
this.coverDestination = CoverDestination.METADATA
this.storeCoverWithBook = false
this.preferAudioMetadata = false
this.preferOpfMetadata = false
@@ -32,7 +30,7 @@ class ScanOptions {
metadataPrecedence: this.metadataPrecedence,
parseSubtitles: this.parseSubtitles,
findCovers: this.findCovers,
coverDestination: this.coverDestination,
storeCoverWithBook: this.storeCoverWithBook,
preferAudioMetadata: this.preferAudioMetadata,
preferOpfMetadata: this.preferOpfMetadata
}
@@ -43,7 +41,7 @@ class ScanOptions {
this.parseSubtitles = !!serverSettings.scannerParseSubtitle
this.findCovers = !!serverSettings.scannerFindCovers
this.coverDestination = serverSettings.coverDestination
this.storeCoverWithBook = serverSettings.storeCoverWithBook
this.preferAudioMetadata = serverSettings.scannerPreferAudioMetadata
this.preferOpfMetadata = serverSettings.scannerPreferOpfMetadata
}
+15 -17
View File
@@ -5,8 +5,8 @@ const Path = require('path')
const Logger = require('../Logger')
const { version } = require('../../package.json')
const { groupFilesIntoAudiobookPaths, getAudiobookFileData, scanRootDir } = require('../utils/scandir')
const { comparePaths, getIno, getId, msToTimestamp } = require('../utils/index')
const { ScanResult, CoverDestination, LogLevel } = require('../utils/constants')
const { comparePaths, getId } = require('../utils/index')
const { ScanResult, LogLevel } = require('../utils/constants')
const AudioFileScanner = require('./AudioFileScanner')
const BookFinder = require('../BookFinder')
@@ -15,12 +15,9 @@ const LibraryScan = require('./LibraryScan')
const ScanOptions = require('./ScanOptions')
class Scanner {
constructor(AUDIOBOOK_PATH, METADATA_PATH, db, coverController, emitter) {
this.AudiobookPath = AUDIOBOOK_PATH
this.MetadataPath = METADATA_PATH
this.BookMetadataPath = Path.posix.join(this.MetadataPath.replace(/\\/g, '/'), 'books')
var LogDirPath = Path.join(this.MetadataPath, 'logs')
this.ScanLogPath = Path.join(LogDirPath, 'scans')
constructor(db, coverController, emitter) {
this.BookMetadataPath = Path.posix.join(global.MetadataPath, 'books')
this.ScanLogPath = Path.posix.join(global.MetadataPath, 'logs', 'scans')
this.db = db
this.coverController = coverController
@@ -33,7 +30,7 @@ class Scanner {
}
getCoverDirectory(audiobook) {
if (this.db.serverSettings.coverDestination === CoverDestination.AUDIOBOOK) {
if (this.db.serverSettings.storeCoverWithBook) {
return {
fullPath: audiobook.fullPath,
relPath: '/s/book/' + audiobook.id
@@ -88,8 +85,8 @@ class Scanner {
// Sync other files first so that local images are used as cover art
// TODO: Cleanup other file sync
var allOtherFiles = checkRes.newOtherFileData.concat(audiobook._otherFiles)
if (await audiobook.syncOtherFiles(allOtherFiles, this.MetadataPath, this.db.serverSettings.scannerPreferOpfMetadata)) {
var allOtherFiles = checkRes.newOtherFileData.concat(checkRes.existingOtherFileData)
if (await audiobook.syncOtherFiles(allOtherFiles, this.db.serverSettings.scannerPreferOpfMetadata)) {
hasUpdated = true
}
@@ -120,7 +117,7 @@ class Scanner {
if (hasUpdated) {
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
await this.db.updateEntity('audiobook', audiobook)
await this.db.updateAudiobook(audiobook)
return ScanResult.UPDATED
}
return ScanResult.UPTODATE
@@ -208,6 +205,7 @@ class Scanner {
// Check for existing & removed audiobooks
for (let i = 0; i < audiobooksInLibrary.length; i++) {
var audiobook = audiobooksInLibrary[i]
// Find audiobook folder with matching inode or matching path
var dataFound = audiobookDataFound.find(abd => abd.ino === audiobook.ino || comparePaths(abd.path, audiobook.path))
if (!dataFound) {
libraryScan.addLog(LogLevel.WARN, `Audiobook "${audiobook.title}" is missing`)
@@ -317,7 +315,7 @@ class Scanner {
}))
newAudiobooks = newAudiobooks.filter(ab => ab) // Filter out nulls
libraryScan.resultsAdded += newAudiobooks.length
await this.db.insertEntities('audiobook', newAudiobooks)
await this.db.insertAudiobooks(newAudiobooks)
this.emitter('audiobooks_added', newAudiobooks.map(ab => ab.toJSONExpanded()))
}
@@ -330,7 +328,7 @@ class Scanner {
if (newOtherFileData.length || libraryScan.scanOptions.forceRescan) {
// TODO: Cleanup other file sync
var allOtherFiles = newOtherFileData.concat(existingOtherFileData)
if (await audiobook.syncOtherFiles(allOtherFiles, this.MetadataPath, libraryScan.preferOpfMetadata)) {
if (await audiobook.syncOtherFiles(allOtherFiles, libraryScan.preferOpfMetadata)) {
hasUpdated = true
}
}
@@ -525,7 +523,7 @@ class Scanner {
Logger.debug(`[Scanner] Folder update group must be a new book "${bookDir}" in library "${library.name}"`)
var newAudiobook = await this.scanPotentialNewAudiobook(folder, fullPath)
if (newAudiobook) {
await this.db.insertEntity('audiobook', newAudiobook)
await this.db.insertAudiobook(newAudiobook)
this.emitter('audiobook_added', newAudiobook.toJSONExpanded())
}
bookGroupingResults[bookDir] = newAudiobook ? ScanResult.ADDED : ScanResult.NOTHING
@@ -615,7 +613,7 @@ class Scanner {
}
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)
await this.db.insertAudiobook(abCopy)
audiobooksUpdated++
} else {
ids[ab.id] = true
@@ -668,7 +666,7 @@ class Scanner {
}
if (hasUpdated) {
await this.db.updateEntity('audiobook', audiobook)
await this.db.updateAudiobook(audiobook)
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
}
+102
View File
@@ -0,0 +1,102 @@
const fs = require('fs-extra')
const filePerms = require('./filePerms')
const package = require('../../package.json')
const Logger = require('../Logger')
const bookKeyMap = {
title: 'title',
subtitle: 'subtitle',
author: 'authorFL',
narrator: 'narratorFL',
series: 'series',
volumeNumber: 'volumeNumber',
publishYear: 'publishYear',
publisher: 'publisher',
description: 'description',
isbn: 'isbn',
asin: 'asin',
language: 'language',
genres: 'genresCommaSeparated'
}
function generate(audiobook, outputPath) {
var fileString = ';ABMETADATA1\n'
fileString += `#audiobookshelf v${package.version}\n\n`
for (const key in bookKeyMap) {
const value = audiobook.book[bookKeyMap[key]] || ''
fileString += `${key}=${value}\n`
}
if (audiobook.chapters.length) {
fileString += '\n'
audiobook.chapters.forEach((chapter) => {
fileString += `[CHAPTER]\n`
fileString += `start=${chapter.start}\n`
fileString += `end=${chapter.end}\n`
fileString += `title=${chapter.title}\n`
})
}
return fs.writeFile(outputPath, fileString).then(() => {
return filePerms(outputPath, 0o774, global.Uid, global.Gid, true).then((data) => true)
}).catch((error) => {
Logger.error(`[absMetaFileGenerator] Failed to save abs file`, error)
return false
})
}
module.exports.generate = generate
function parseAbMetadataText(text) {
if (!text) return null
var lines = text.split(/\r?\n/)
// Check first line and get abmetadata version number
var firstLine = lines.shift().toLowerCase()
if (!firstLine.startsWith(';abmetadata')) {
Logger.error(`Invalid abmetadata file first line is not ;abmetadata "${firstLine}"`)
return null
}
var abmetadataVersion = Number(firstLine.replace(';abmetadata', '').trim())
if (isNaN(abmetadataVersion)) {
Logger.warn(`Invalid abmetadata version ${abmetadataVersion} - using 1`)
abmetadataVersion = 1
}
// Remove comments and empty lines
const ignoreFirstChars = [' ', '#', ';'] // Ignore any line starting with the following
lines = lines.filter(line => !!line.trim() && !ignoreFirstChars.includes(line[0]))
// Get lines that map to book details (all lines before the first chapter section)
var firstSectionLine = lines.findIndex(l => l.startsWith('['))
var detailLines = firstSectionLine > 0 ? lines.slice(0, firstSectionLine) : lines
// Put valid book detail values into map
const bookDetails = {}
for (let i = 0; i < detailLines.length; i++) {
var line = detailLines[i]
var keyValue = line.split('=')
if (keyValue.length < 2) {
Logger.warn('abmetadata invalid line has no =', line)
} else if (!bookKeyMap[keyValue[0].trim()]) {
Logger.warn(`abmetadata key "${keyValue[0].trim()}" is not a valid book detail key`)
} else {
var key = keyValue[0].trim()
bookDetails[key] = keyValue[1].trim()
// Genres convert to array of strings
if (key === 'genres') {
bookDetails[key] = bookDetails[key] ? bookDetails[key].split(',').map(genre => genre.trim()) : []
} else if (!bookDetails[key]) { // Use null for empty details
bookDetails[key] = null
}
}
}
// TODO: Chapter support
return {
book: bookDetails
}
}
module.exports.parse = parseAbMetadataText
-37
View File
@@ -1,37 +0,0 @@
const fs = require('fs-extra')
const filePerms = require('./filePerms')
const package = require('../../package.json')
const Logger = require('../Logger')
const bookKeyMap = {
title: 'title',
subtitle: 'subtitle',
author: 'authorFL',
narrator: 'narratorFL',
series: 'series',
volumeNumber: 'volumeNumber',
publishYear: 'publishYear',
publisher: 'publisher',
description: 'description',
isbn: 'isbn',
asin: 'asin',
language: 'language',
genres: 'genresCommaSeparated'
}
function generate(audiobook, outputPath, uid, gid) {
var fileString = `[audiobookshelf v${package.version}]\n`
for (const key in bookKeyMap) {
const value = audiobook.book[bookKeyMap[key]] || ''
fileString += `${key}=${value}\n`
}
return fs.writeFile(outputPath, fileString).then(() => {
return filePerms(outputPath, 0o774, uid, gid).then(() => true)
}).catch((error) => {
Logger.error(`[absMetaFileGenerator] Failed to save abs file`, error)
return false
})
}
module.exports.generate = generate
-5
View File
@@ -6,11 +6,6 @@ module.exports.ScanResult = {
UPTODATE: 4
}
module.exports.CoverDestination = {
METADATA: 0,
AUDIOBOOK: 1
}
module.exports.BookCoverAspectRatio = {
STANDARD: 0, // 1.6:1
SQUARE: 1
+3 -3
View File
@@ -50,7 +50,7 @@ const chmodr = (p, mode, uid, gid, cb) => {
// any error other than ENOTDIR means it's not readable, or
// doesn't exist. give up.
if (er && er.code !== 'ENOTDIR') return cb(er)
if (er) {
if (er) { // Is a file
return fs.chmod(p, mode).then(() => {
fs.chown(p, uid, gid, cb)
})
@@ -77,9 +77,9 @@ const chmodr = (p, mode, uid, gid, cb) => {
})
}
module.exports = (path, mode, uid, gid) => {
module.exports = (path, mode, uid, gid, silent = false) => {
return new Promise((resolve) => {
Logger.debug(`[FilePerms] Setting permission "${mode}" for uid ${uid} and gid ${gid} | "${path}"`)
if (!silent) Logger.debug(`[FilePerms] Setting permission "${mode}" for uid ${uid} and gid ${gid} | "${path}"`)
chmodr(path, mode, uid, gid, resolve)
})
}
+26
View File
@@ -20,6 +20,23 @@ async function getFileStat(path) {
}
module.exports.getFileStat = getFileStat
async function getFileTimestampsWithIno(path) {
try {
var stat = await fs.stat(path, { bigint: true })
return {
size: Number(stat.size),
mtimeMs: Number(stat.mtimeMs),
ctimeMs: Number(stat.ctimeMs),
birthtimeMs: Number(stat.birthtimeMs),
ino: String(stat.ino)
}
} catch (err) {
console.error('Failed to getFileTimestampsWithIno', err)
return false
}
}
module.exports.getFileTimestampsWithIno = getFileTimestampsWithIno
async function getFileSize(path) {
var stat = await getFileStat(path)
if (!stat) return 0
@@ -27,6 +44,15 @@ async function getFileSize(path) {
}
module.exports.getFileSize = getFileSize
function getIno(path) {
return fs.stat(path, { bigint: true }).then((data => String(data.ino))).catch((err) => {
Logger.error('[Utils] Failed to get ino for path', path, err)
return null
})
}
module.exports.getIno = getIno
async function readTextFile(path) {
try {
var data = await fs.readFile(path)
-7
View File
@@ -38,13 +38,6 @@ module.exports.comparePaths = (path1, path2) => {
return path1 === path2 || Path.normalize(path1) === Path.normalize(path2)
}
module.exports.getIno = (path) => {
return fs.promises.stat(path, { bigint: true }).then((data => String(data.ino))).catch((err) => {
Logger.error('[Utils] Failed to get ino for path', path, err)
return null
})
}
module.exports.isNullOrNaN = (num) => {
return num === null || isNaN(num)
}
+48 -31
View File
@@ -1,8 +1,7 @@
const Path = require('path')
const fs = require('fs-extra')
const Logger = require('../Logger')
const { getIno } = require('./index')
const { recurseFiles } = require('./fileUtils')
const { recurseFiles, getFileTimestampsWithIno } = require('./fileUtils')
const globals = require('./globals')
function isBookFile(path) {
@@ -114,16 +113,20 @@ function groupFileItemsIntoBooks(fileItems) {
}
function cleanFileObjects(basepath, abrelpath, files) {
return files.map((file) => {
return Promise.all(files.map(async (file) => {
var fullPath = Path.posix.join(basepath, file)
var fileTsData = await getFileTimestampsWithIno(fullPath)
var ext = Path.extname(file)
return {
filetype: getFileType(ext),
filename: Path.basename(file),
path: Path.posix.join(abrelpath, file), // /AUDIOBOOK/PATH/filename.mp3
fullPath: Path.posix.join(basepath, file), // /audiobooks/AUDIOBOOK/PATH/filename.mp3
ext: ext
fullPath, // /audiobooks/AUDIOBOOK/PATH/filename.mp3
ext: ext,
...fileTsData
}
})
}))
}
function getFileType(ext) {
@@ -162,15 +165,15 @@ async function scanRootDir(folder, serverSettings = {}) {
for (const audiobookPath in audiobookGrouping) {
var audiobookData = getAudiobookDataFromDir(folderPath, audiobookPath, parseSubtitle)
var fileObjs = cleanFileObjects(audiobookData.fullPath, audiobookPath, audiobookGrouping[audiobookPath])
for (let i = 0; i < fileObjs.length; i++) {
fileObjs[i].ino = await getIno(fileObjs[i].fullPath)
}
var audiobookIno = await getIno(audiobookData.fullPath)
var fileObjs = await cleanFileObjects(audiobookData.fullPath, audiobookPath, audiobookGrouping[audiobookPath])
var audiobookFolderStats = await getFileTimestampsWithIno(audiobookData.fullPath)
audiobooks.push({
folderId: folder.id,
libraryId: folder.libraryId,
ino: audiobookIno,
ino: audiobookFolderStats.ino,
mtimeMs: audiobookFolderStats.mtimeMs || 0,
ctimeMs: audiobookFolderStats.ctimeMs || 0,
birthtimeMs: audiobookFolderStats.birthtimeMs || 0,
...audiobookData,
audioFiles: fileObjs.filter(f => f.filetype === 'audio'),
otherFiles: fileObjs.filter(f => f.filetype !== 'audio')
@@ -196,32 +199,42 @@ function getAudiobookDataFromDir(folderPath, dir, parseSubtitle = false) {
// If in a series directory check for volume number match
/* ACCEPTS:
/* ACCEPTS
Book 2 - Title Here - Subtitle Here
Title Here - Subtitle Here - Vol 12
Title Here - volume 9 - Subtitle Here
Vol. 3 Title Here - Subtitle Here
1980 - Book 2-Title Here
Title Here-Volume 999-Subtitle Here
2 - Book Title
100 - Book Title
0.5 - Book Title
*/
var volumeNumber = null
if (series) {
// New volume regex to match volumes with decimal (OLD: /(-? ?)\b((?:Book|Vol.?|Volume) (\d{1,3}))\b( ?-?)/i)
var volumeMatch = title.match(/(-? ?)\b((?:Book|Vol.?|Volume) (\d{0,3}(?:\.\d{1,2})?))\b( ?-?)/i)
if (volumeMatch && volumeMatch.length > 3 && volumeMatch[2] && volumeMatch[3]) {
volumeNumber = volumeMatch[3]
var replaceChunk = volumeMatch[2]
// Added 1.7.1: If title starts with a # that is 3 digits or less (or w/ 2 decimal), then use as volume number
var volumeMatch = title.match(/^(\d{1,3}(?:\.\d{1,2})?) - ./)
if (volumeMatch && volumeMatch.length > 1) {
volumeNumber = volumeMatch[1]
title = title.replace(`${volumeNumber} - `, '')
} else {
// Match volumes with decimal (OLD: /(-? ?)\b((?:Book|Vol.?|Volume) (\d{1,3}))\b( ?-?)/i)
var volumeMatch = title.match(/(-? ?)\b((?:Book|Vol.?|Volume) (\d{0,3}(?:\.\d{1,2})?))\b( ?-?)/i)
if (volumeMatch && volumeMatch.length > 3 && volumeMatch[2] && volumeMatch[3]) {
volumeNumber = volumeMatch[3]
var replaceChunk = volumeMatch[2]
// "1980 - Book 2-Title Here"
// Group 1 would be "- "
// Group 3 would be "-"
// Only remove the first group
if (volumeMatch[1]) {
replaceChunk = volumeMatch[1] + replaceChunk
} else if (volumeMatch[4]) {
replaceChunk += volumeMatch[4]
// "1980 - Book 2-Title Here"
// Group 1 would be "- "
// Group 3 would be "-"
// Only remove the first group
if (volumeMatch[1]) {
replaceChunk = volumeMatch[1] + replaceChunk
} else if (volumeMatch[4]) {
replaceChunk += volumeMatch[4]
}
title = title.replace(replaceChunk, '').trim()
}
title = title.replace(replaceChunk, '').trim()
}
}
@@ -272,8 +285,12 @@ async function getAudiobookFileData(folder, audiobookPath, serverSettings = {})
var audiobookDir = audiobookPath.replace(folderFullPath, '').slice(1)
var audiobookData = getAudiobookDataFromDir(folderFullPath, audiobookDir, parseSubtitle)
var audiobookFolderStats = await getFileTimestampsWithIno(audiobookData.fullPath)
var audiobook = {
ino: await getIno(audiobookData.fullPath),
ino: audiobookFolderStats.ino,
mtimeMs: audiobookFolderStats.mtimeMs || 0,
ctimeMs: audiobookFolderStats.ctimeMs || 0,
birthtimeMs: audiobookFolderStats.birthtimeMs || 0,
folderId: folder.id,
libraryId: folder.libraryId,
...audiobookData,
@@ -284,14 +301,14 @@ async function getAudiobookFileData(folder, audiobookPath, serverSettings = {})
for (let i = 0; i < fileItems.length; i++) {
var fileItem = fileItems[i]
var ino = await getIno(fileItem.fullpath)
var fileStatData = await getFileTimestampsWithIno(fileItem.fullpath)
var fileObj = {
ino,
filetype: getFileType(fileItem.extension),
filename: fileItem.name,
path: fileItem.path,
fullPath: fileItem.fullpath,
ext: fileItem.extension
ext: fileItem.extension,
...fileStatData
}
if (fileObj.filetype === 'audio') {
audiobook.audioFiles.push(fileObj)