mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-15 06:54:24 +02:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b6ae6d86fa | |||
| 9f66054a72 | |||
| fffa02e7e8 | |||
| 6fbd9dc260 | |||
| c6614aba05 | |||
| ee62385980 | |||
| 806017175d | |||
| 1a3a7c5823 | |||
| 550873ff87 | |||
| 4dd9f779e2 | |||
| 580b961c4a | |||
| 28b1132171 | |||
| 8b31c6555a | |||
| 8ffb4f88c9 | |||
| c5eafdfa8a | |||
| f9bf846b30 | |||
| 335bbac81d | |||
| 874c910e24 | |||
| aca88f73ad | |||
| ed80e15b7d | |||
| 3f13d35241 | |||
| 05be496817 | |||
| 1cca288031 |
@@ -20,7 +20,7 @@
|
||||
-webkit-font-feature-settings: 'liga';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.material-icons:not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl) {
|
||||
.material-icons:not(.text-xs):not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl) {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div class="w-full relative mb-4">
|
||||
<div class="absolute left-2 top-0 bottom-0 h-full flex items-center">
|
||||
<p ref="currentTimestamp" class="font-mono text-sm">00:00:00</p>
|
||||
</div>
|
||||
<div class="absolute right-2 top-0 bottom-0 h-full flex items-center">
|
||||
<p class="font-mono text-sm">{{ totalDurationPretty }}</p>
|
||||
<div class="w-full -mt-4">
|
||||
<div class="w-full relative mb-2">
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-red flex items-end pointer-events-none">
|
||||
<p ref="currentTimestamp" class="font-mono text-sm text-gray-100 pointer-events-auto">00:00:00</p>
|
||||
<p class="font-mono text-sm text-gray-100 pointer-events-auto"> / {{ progressPercent }}%</p>
|
||||
<div class="flex-grow" />
|
||||
<p class="font-mono text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p>
|
||||
</div>
|
||||
|
||||
<div class="absolute right-20 top-0 bottom-0">
|
||||
<div v-if="chapters.length" class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
|
||||
<div v-if="chapters.length" class="absolute right-20 top-0 bottom-0 h-full flex items-end">
|
||||
<div class="cursor-pointer text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
|
||||
<span class="material-icons text-3xl">format_list_bulleted</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="absolute right-32 top-0 bottom-0">
|
||||
<div v-if="showExperimentalFeatures" class="absolute top-0 bottom-0 h-full 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 flex items-end" :class="!showExperimentalFeatures ? (chapters.length ? ' right-32' : 'right-20') : chapters.length ? ' right-44' : 'right-32'">
|
||||
<controls-volume-control ref="volumeControl" v-model="volume" @input="updateVolume" />
|
||||
</div>
|
||||
<div class="flex my-2">
|
||||
<div class="flex-grow" />
|
||||
|
||||
<div class="flex 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">
|
||||
<span class="material-icons text-3xl">first_page</span>
|
||||
@@ -85,6 +89,10 @@ export default {
|
||||
chapters: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
bookmarks: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -116,6 +124,17 @@ export default {
|
||||
totalDurationPretty() {
|
||||
return this.$secondsToTimestamp(this.totalDuration)
|
||||
},
|
||||
timeRemaining() {
|
||||
if (!this.audioEl) return 0
|
||||
return this.totalDuration - this.currentTime
|
||||
},
|
||||
timeRemainingPretty() {
|
||||
return '-' + this.$secondsToTimestamp(this.timeRemaining)
|
||||
},
|
||||
progressPercent() {
|
||||
if (!this.totalDuration) return 0
|
||||
return Math.round((100 * this.currentTime) / this.totalDuration)
|
||||
},
|
||||
chapterTicks() {
|
||||
return this.chapters.map((chap) => {
|
||||
var perc = chap.start / this.totalDuration
|
||||
@@ -127,6 +146,9 @@ export default {
|
||||
},
|
||||
currentChapter() {
|
||||
return this.chapters.find((chapter) => chapter.start <= this.currentTime && this.currentTime < chapter.end)
|
||||
},
|
||||
showExperimentalFeatures() {
|
||||
return this.$store.state.showExperimentalFeatures
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -134,6 +156,11 @@ export default {
|
||||
this.seek(chapter.start)
|
||||
this.showChaptersModal = false
|
||||
},
|
||||
selectBookmark(bookmark) {
|
||||
if (bookmark) {
|
||||
this.seek(bookmark.time)
|
||||
}
|
||||
},
|
||||
seek(time) {
|
||||
if (this.loading) {
|
||||
return
|
||||
@@ -232,6 +259,7 @@ export default {
|
||||
},
|
||||
restart() {
|
||||
this.seek(0)
|
||||
this.$nextTick(this.sendStreamUpdate)
|
||||
},
|
||||
backward10() {
|
||||
var newTime = this.audioEl.currentTime - 10
|
||||
@@ -346,7 +374,7 @@ export default {
|
||||
return
|
||||
}
|
||||
var lastbuff = this.getLastBufferedTime()
|
||||
this.sendStreamUpdate()
|
||||
|
||||
var bufferlen = (lastbuff / this.audioEl.duration) * this.trackWidth
|
||||
bufferlen = Math.round(bufferlen)
|
||||
if (this.bufferTrackWidth === bufferlen || !this.$refs.bufferTrack) return
|
||||
@@ -373,6 +401,13 @@ export default {
|
||||
|
||||
this.updateTimestamp()
|
||||
|
||||
// Send update to server when currentTime > 0
|
||||
// this prevents errors when seeking to position not yet transcoded
|
||||
// seeking to position not yet transcoded will cause audio element to set currentTime to 0
|
||||
if (this.audioEl.currentTime) {
|
||||
this.sendStreamUpdate()
|
||||
}
|
||||
|
||||
this.currentTime = this.audioEl.currentTime
|
||||
|
||||
var perc = this.audioEl.currentTime / this.audioEl.duration
|
||||
@@ -399,6 +434,7 @@ export default {
|
||||
},
|
||||
audioLoadedData() {
|
||||
this.totalDuration = this.audioEl.duration
|
||||
this.$emit('loaded', this.totalDuration)
|
||||
},
|
||||
set(url, currentTime, playOnLoad = false) {
|
||||
if (this.hlsInstance) {
|
||||
@@ -454,6 +490,9 @@ export default {
|
||||
if (!this.chapters.length) return
|
||||
this.showChaptersModal = !this.showChaptersModal
|
||||
},
|
||||
showBookmarks() {
|
||||
this.$emit('showBookmarks', this.currentTime)
|
||||
},
|
||||
play() {
|
||||
if (!this.$refs.audio) {
|
||||
console.error('No Audio ref')
|
||||
@@ -535,16 +574,16 @@ export default {
|
||||
this.$emit('close')
|
||||
},
|
||||
hotkey(action) {
|
||||
if (action === 'Space') this.playPauseClick()
|
||||
else if (action === 'ArrowRight') this.forward10()
|
||||
else if (action === 'ArrowLeft') this.backward10()
|
||||
else if (action === 'ArrowUp') this.volumeUp()
|
||||
else if (action === 'ArrowDown') this.volumeDown()
|
||||
else if (action === 'KeyM') this.toggleMute()
|
||||
else if (action === 'KeyL') this.showChapters()
|
||||
else if (action === 'Shift-ArrowUp') this.increasePlaybackRate()
|
||||
else if (action === 'Shift-ArrowDown') this.decreasePlaybackRate()
|
||||
else if (action === 'Escape') this.closePlayer()
|
||||
if (action === this.$hotkeys.AudioPlayer.PLAY_PAUSE) this.playPauseClick()
|
||||
else if (action === this.$hotkeys.AudioPlayer.JUMP_FORWARD) this.forward10()
|
||||
else if (action === this.$hotkeys.AudioPlayer.JUMP_BACKWARD) this.backward10()
|
||||
else if (action === this.$hotkeys.AudioPlayer.VOLUME_UP) this.volumeUp()
|
||||
else if (action === this.$hotkeys.AudioPlayer.VOLUME_DOWN) this.volumeDown()
|
||||
else if (action === this.$hotkeys.AudioPlayer.MUTE_UNMUTE) this.toggleMute()
|
||||
else if (action === this.$hotkeys.AudioPlayer.SHOW_CHAPTERS) this.showChapters()
|
||||
else if (action === this.$hotkeys.AudioPlayer.INCREASE_PLAYBACK_RATE) this.increasePlaybackRate()
|
||||
else if (action === this.$hotkeys.AudioPlayer.DECREASE_PLAYBACK_RATE) this.decreasePlaybackRate()
|
||||
else if (action === this.$hotkeys.AudioPlayer.CLOSE) this.closePlayer()
|
||||
},
|
||||
windowResize() {
|
||||
this.setTrackWidth()
|
||||
|
||||
@@ -89,6 +89,8 @@ export default {
|
||||
'$route.query.filter'() {
|
||||
if (this.$route.query.filter && this.$route.query.filter !== this.filterBy) {
|
||||
this.$store.dispatch('user/updateUserSettings', { filterBy: this.$route.query.filter })
|
||||
} else if (!this.$route.query.filter && this.filterBy) {
|
||||
this.$store.dispatch('user/updateUserSettings', { filterBy: 'all' })
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -11,14 +11,14 @@
|
||||
<div v-show="homePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === '' && !homePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="showLibrary ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||
</svg>
|
||||
|
||||
<p class="font-book pt-1.5" style="font-size: 1rem">Library</p>
|
||||
|
||||
<div v-show="paramId === '' && !homePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
<div v-show="showLibrary" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf/series`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'series' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
@@ -31,6 +31,16 @@
|
||||
<div v-show="paramId === 'series'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : ' bg-error bg-opacity-20'">
|
||||
<span class="material-icons text-2xl">warning</span>
|
||||
|
||||
<p class="font-book pt-1.5" style="font-size: 1rem">Issues</p>
|
||||
|
||||
<div v-show="showingIssues" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
<div class="absolute top-1 right-1 w-4 h-4 rounded-full bg-white bg-opacity-30 flex items-center justify-center">
|
||||
<p class="text-xs font-mono pb-0.5">{{ numIssues }}</p>
|
||||
</div>
|
||||
</nuxt-link>
|
||||
<!-- <nuxt-link to="/library/collections" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
@@ -80,6 +90,19 @@ export default {
|
||||
},
|
||||
homePage() {
|
||||
return this.$route.name === 'library-library'
|
||||
},
|
||||
libraryBookshelfPage() {
|
||||
return this.$route.name === 'library-library-bookshelf-id'
|
||||
},
|
||||
showLibrary() {
|
||||
return this.libraryBookshelfPage && this.paramId === '' && !this.showingIssues
|
||||
},
|
||||
showingIssues() {
|
||||
if (!this.$route.query) return false
|
||||
return this.libraryBookshelfPage && this.$route.query.filter === 'issues'
|
||||
},
|
||||
numIssues() {
|
||||
return this.$store.getters['audiobooks/getAudiobooksWithIssues'].length
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
<template>
|
||||
<div v-if="streamAudiobook" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-40 z-40 bg-primary p-4">
|
||||
<div v-if="streamAudiobook" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-40 z-40 bg-primary px-4 pb-4 pt-2">
|
||||
<nuxt-link :to="`/audiobook/${streamAudiobook.id}`" class="absolute -top-16 left-4 cursor-pointer">
|
||||
<cards-book-cover :audiobook="streamAudiobook" :width="88" />
|
||||
</nuxt-link>
|
||||
<div class="flex items-center pl-24">
|
||||
<div>
|
||||
<nuxt-link :to="`/audiobook/${streamAudiobook.id}`" class="hover:underline cursor-pointer">
|
||||
<nuxt-link :to="`/audiobook/${streamAudiobook.id}`" class="hover:underline cursor-pointer text-lg">
|
||||
{{ title }} <span v-if="stream && $isDev" class="text-xs text-gray-400">({{ stream.id }})</span>
|
||||
</nuxt-link>
|
||||
<p class="text-gray-400 text-sm hover:underline cursor-pointer" @click="filterByAuthor">by {{ author }}</p>
|
||||
<div class="text-gray-400 flex items-center">
|
||||
<span class="material-icons text-sm">person</span>
|
||||
<p class="text-base hover:underline cursor-pointer pl-2" @click="filterByAuthor">{{ author }}</p>
|
||||
</div>
|
||||
|
||||
<div class="text-gray-400 flex items-center">
|
||||
<span class="material-icons text-xs">schedule</span>
|
||||
<p class="font-mono text-sm pl-2 pb-px">{{ totalDurationPretty }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<span v-if="stream" class="material-icons px-4 cursor-pointer" @click="cancelStream">close</span>
|
||||
</div>
|
||||
|
||||
<audio-player ref="audioPlayer" :chapters="chapters" :loading="isLoading" @close="cancelStream" @updateTime="updateTime" @hook:mounted="audioPlayerMounted" />
|
||||
<audio-player ref="audioPlayer" :chapters="chapters" :loading="isLoading" :bookmarks="bookmarks" @close="cancelStream" @updateTime="updateTime" @loaded="(d) => (totalDuration = d)" @showBookmarks="showBookmarks" @hook:mounted="audioPlayerMounted" />
|
||||
|
||||
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :audiobook-id="bookmarkAudiobookId" :current-time="bookmarkCurrentTime" @select="selectBookmark" @create="createBookmark" @update="updateBookmark" @delete="deleteBookmark" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -24,7 +34,11 @@ export default {
|
||||
return {
|
||||
audioPlayerReady: false,
|
||||
lastServerUpdateSentSeconds: 0,
|
||||
stream: null
|
||||
stream: null,
|
||||
totalDuration: 0,
|
||||
showBookmarksModal: false,
|
||||
bookmarkCurrentTime: 0,
|
||||
bookmarkAudiobookId: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -35,6 +49,14 @@ export default {
|
||||
user() {
|
||||
return this.$store.state.user.user
|
||||
},
|
||||
userAudiobook() {
|
||||
if (!this.audiobookId) return
|
||||
return this.$store.getters['user/getUserAudiobook'](this.audiobookId)
|
||||
},
|
||||
bookmarks() {
|
||||
if (!this.userAudiobook) return []
|
||||
return this.userAudiobook.bookmarks || []
|
||||
},
|
||||
isLoading() {
|
||||
if (!this.streamAudiobook) return false
|
||||
if (this.stream) {
|
||||
@@ -46,6 +68,9 @@ export default {
|
||||
streamAudiobook() {
|
||||
return this.$store.state.streamAudiobook
|
||||
},
|
||||
audiobookId() {
|
||||
return this.streamAudiobook ? this.streamAudiobook.id : null
|
||||
},
|
||||
book() {
|
||||
return this.streamAudiobook ? this.streamAudiobook.book || {} : {}
|
||||
},
|
||||
@@ -66,9 +91,49 @@ export default {
|
||||
},
|
||||
libraryId() {
|
||||
return this.streamAudiobook ? this.streamAudiobook.libraryId : null
|
||||
},
|
||||
totalDurationPretty() {
|
||||
return this.$secondsToTimestamp(this.totalDuration)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showBookmarks(currentTime) {
|
||||
this.bookmarkAudiobookId = this.audiobookId
|
||||
this.bookmarkCurrentTime = currentTime
|
||||
this.showBookmarksModal = true
|
||||
},
|
||||
// bookmarkCreated(time) {
|
||||
// if (time === this.bookmarkTimeProcessing) {
|
||||
// this.bookmarkTimeProcessing = 0
|
||||
// this.$toast.success(`${this.$secondsToTimestamp(time)} Bookmarked`)
|
||||
// }
|
||||
// },
|
||||
createBookmark(bookmark) {
|
||||
// this.bookmarkTimeProcessing = bookmark.time
|
||||
this.$root.socket.emit('create_bookmark', bookmark)
|
||||
this.showBookmarksModal = false
|
||||
},
|
||||
// bookmarkUpdated(time) {
|
||||
// if (time === this.bookmarkTimeProcessing) {
|
||||
// this.bookmarkTimeProcessing = 0
|
||||
// this.$toast.success(`Bookmark @${this.$secondsToTimestamp(time)} Updated`)
|
||||
// }
|
||||
// },
|
||||
updateBookmark(bookmark) {
|
||||
// this.bookmarkTimeProcessing = bookmark.time
|
||||
this.$root.socket.emit('update_bookmark', bookmark)
|
||||
this.showBookmarksModal = false
|
||||
},
|
||||
selectBookmark(bookmark) {
|
||||
if (this.$refs.audioPlayer) {
|
||||
this.$refs.audioPlayer.selectBookmark(bookmark)
|
||||
}
|
||||
this.showBookmarksModal = false
|
||||
},
|
||||
deleteBookmark(bookmark) {
|
||||
this.$root.socket.emit('delete_bookmark', bookmark)
|
||||
this.showBookmarksModal = false
|
||||
},
|
||||
filterByAuthor() {
|
||||
if (this.$route.name !== 'index') {
|
||||
this.$router.push(`/library/${this.libraryId || this.$store.state.libraries.currentLibraryId}/bookshelf`)
|
||||
|
||||
@@ -101,6 +101,11 @@ export default {
|
||||
text: 'Progress',
|
||||
value: 'progress',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Issues',
|
||||
value: 'issues',
|
||||
sublist: false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<modals-modal v-model="show" name="bookmarks" :width="500" :height="'unset'">
|
||||
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||
<div class="w-full h-full px-6 py-6" v-show="showBookmarkTitleInput">
|
||||
<div class="flex mb-4 items-center">
|
||||
<div class="w-9 h-9 flex items-center justify-center rounded-full hover:bg-white hover:bg-opacity-10 cursor-pointer" @click="showBookmarkTitleInput = false">
|
||||
<span class="material-icons text-3xl">arrow_back</span>
|
||||
</div>
|
||||
<p class="text-xl pl-2">{{ selectedBookmark ? 'Edit Bookmark' : 'New Bookmark' }}</p>
|
||||
<div class="flex-grow" />
|
||||
<p class="text-xl font-mono">
|
||||
{{ this.$secondsToTimestamp(currentTime) }}
|
||||
</p>
|
||||
</div>
|
||||
<form @submit.prevent="submitBookmark">
|
||||
<ui-text-input-with-label v-model="newBookmarkTitle" label="Note" />
|
||||
<div class="flex justify-end mt-6">
|
||||
<ui-btn color="success" class="w-1/2" type="submit">{{ selectedBookmark ? 'Update' : 'Create' }} Bookmark</ui-btn>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="w-full h-full" v-show="!showBookmarkTitleInput">
|
||||
<template v-for="bookmark in bookmarks">
|
||||
<modals-bookmarks-bookmark-item :key="bookmark.id" :highlight="currentTime === bookmark.time" :bookmark="bookmark" @click="clickBookmark" @edit="editBookmark" @delete="deleteBookmark" />
|
||||
</template>
|
||||
<div v-if="!bookmarks.length" class="flex h-32 items-center justify-center">
|
||||
<p class="text-xl">No Bookmarks</p>
|
||||
</div>
|
||||
<div v-show="canCreateBookmark" class="flex px-4 py-2 items-center text-center justify-between border-b border-white border-opacity-10 bg-blue-500 bg-opacity-20 cursor-pointer text-white text-opacity-80 hover:bg-opacity-40 hover:text-opacity-100" @click="createBookmark">
|
||||
<span class="material-icons">add</span>
|
||||
<p class="text-base pl-2">Create Bookmark</p>
|
||||
<p class="text-sm font-mono">
|
||||
{{ this.$secondsToTimestamp(currentTime) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: Boolean,
|
||||
bookmarks: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
currentTime: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
audiobookId: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedBookmark: null,
|
||||
showBookmarkTitleInput: false,
|
||||
newBookmarkTitle: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show(newVal) {
|
||||
if (newVal) {
|
||||
this.showBookmarkTitleInput = false
|
||||
this.newBookmarkTitle = ''
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
show: {
|
||||
get() {
|
||||
return this.value
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('input', val)
|
||||
}
|
||||
},
|
||||
canCreateBookmark() {
|
||||
return !this.bookmarks.find((bm) => bm.time === this.currentTime)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
editBookmark(bm) {
|
||||
this.selectedBookmark = bm
|
||||
this.newBookmarkTitle = bm.title
|
||||
this.showBookmarkTitleInput = true
|
||||
},
|
||||
deleteBookmark(bm) {
|
||||
var bookmark = { ...bm, audiobookId: this.audiobookId }
|
||||
this.$emit('delete', bookmark)
|
||||
},
|
||||
clickBookmark(bm) {
|
||||
this.$emit('select', bm)
|
||||
},
|
||||
createBookmark() {
|
||||
this.selectedBookmark = null
|
||||
this.newBookmarkTitle = this.$formatDate(Date.now(), 'MMM dd, yyyy HH:mm')
|
||||
this.showBookmarkTitleInput = true
|
||||
},
|
||||
submitBookmark() {
|
||||
if (this.selectedBookmark) {
|
||||
if (this.selectedBookmark.title !== this.newBookmarkTitle) {
|
||||
var bookmark = { ...this.selectedBookmark }
|
||||
bookmark.audiobookId = this.audiobookId
|
||||
bookmark.title = this.newBookmarkTitle
|
||||
this.$emit('update', bookmark)
|
||||
}
|
||||
} else {
|
||||
var bookmark = {
|
||||
audiobookId: this.audiobookId,
|
||||
title: this.newBookmarkTitle,
|
||||
time: this.currentTime
|
||||
}
|
||||
this.$emit('create', bookmark)
|
||||
}
|
||||
this.newBookmarkTitle = ''
|
||||
this.showBookmarkTitleInput = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -218,9 +218,9 @@ export default {
|
||||
}
|
||||
},
|
||||
hotkey(action) {
|
||||
if (action === 'ArrowRight') {
|
||||
if (action === this.$hotkeys.Modal.NEXT_PAGE) {
|
||||
this.goNextBook()
|
||||
} else if (action === 'ArrowLeft') {
|
||||
} else if (action === this.$hotkeys.Modal.PREV_PAGE) {
|
||||
this.goPrevBook()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -84,7 +84,7 @@ export default {
|
||||
}
|
||||
},
|
||||
hotkey(action) {
|
||||
if (action === 'Escape') {
|
||||
if (action === this.$hotkeys.Modal.CLOSE) {
|
||||
this.show = false
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div :key="bookmark.id" :id="`bookmark-row-${bookmark.id}`" class="flex items-center px-4 py-4 justify-start cursor-pointer hover:bg-bg relative" :class="highlight ? 'bg-bg bg-opacity-60' : ' bg-opacity-20'" @click="click" @mouseover="isHovering = true" @mouseleave="isHovering = false">
|
||||
<span class="material-icons" :class="highlight ? 'text-success' : 'text-white text-opacity-60'">{{ highlight ? 'bookmark' : 'bookmark_border' }}</span>
|
||||
<div class="flex-grow overflow-hidden">
|
||||
<p class="pl-2 pr-2 truncate">{{ bookmark.title }}</p>
|
||||
</div>
|
||||
<div class="h-full flex items-center w-16 justify-end">
|
||||
<span class="font-mono text-sm text-gray-300">{{ $secondsToTimestamp(bookmark.time) }}</span>
|
||||
</div>
|
||||
<div class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'">
|
||||
<span class="material-icons text-lg mr-2 text-gray-200 hover:text-yellow-400" @click.stop="editClick">edit</span>
|
||||
<span class="material-icons text-lg text-gray-200 hover:text-error cursor-pointer" @click.stop="deleteClick">delete</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
bookmark: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
highlight: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isHovering: false
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
click() {
|
||||
this.$emit('click', this.bookmark)
|
||||
},
|
||||
deleteClick() {
|
||||
this.$emit('delete', this.bookmark)
|
||||
},
|
||||
editClick() {
|
||||
this.$emit('edit', this.bookmark)
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -53,18 +53,19 @@
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-0 left-0 w-full py-4 bg-bg" :class="isScrollable ? 'box-shadow-md-up' : 'box-shadow-sm-up border-t border-primary border-opacity-50'">
|
||||
<div class="flex px-4">
|
||||
<ui-btn v-if="userCanDelete" color="error" type="button" small @click.stop.prevent="deleteAudiobook">Remove</ui-btn>
|
||||
<div class="flex items-center px-4">
|
||||
<ui-btn v-if="userCanDelete" color="error" type="button" class="h-8" :padding-x="3" small @click.stop.prevent="deleteAudiobook">Remove</ui-btn>
|
||||
|
||||
<ui-tooltip text="(Root User Only) Save a NFO metadata file in your audiobooks directory" direction="bottom" class="ml-4">
|
||||
<div class="flex-grow" />
|
||||
|
||||
<ui-tooltip v-if="!isMissing" text="(Root User Only) Save a NFO metadata file in your audiobooks directory" direction="bottom" class="mr-4">
|
||||
<ui-btn v-if="isRootUser" :loading="savingMetadata" color="bg" type="button" class="h-full" small @click.stop.prevent="saveMetadata">Save Metadata</ui-btn>
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-tooltip :disabled="!!libraryScan" text="(Root User Only) Rescan audiobook including metadata" direction="bottom" class="ml-4">
|
||||
<ui-tooltip :disabled="!!libraryScan" text="(Root User Only) Rescan audiobook including metadata" direction="bottom" class="mr-4">
|
||||
<ui-btn v-if="isRootUser" :loading="rescanning" :disabled="!!libraryScan" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">Re-Scan</ui-btn>
|
||||
</ui-tooltip>
|
||||
|
||||
<div class="flex-grow" />
|
||||
<ui-btn type="submit">Submit</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
@@ -121,6 +122,9 @@ export default {
|
||||
isRootUser() {
|
||||
return this.$store.getters['user/getIsRoot']
|
||||
},
|
||||
isMissing() {
|
||||
return !!this.audiobook && !!this.audiobook.isMissing
|
||||
},
|
||||
audiobookId() {
|
||||
return this.audiobook ? this.audiobook.id : null
|
||||
},
|
||||
|
||||
@@ -91,11 +91,11 @@ export default {
|
||||
console.log('Reader hotkey', action)
|
||||
if (!this.$refs.readerComponent) return
|
||||
|
||||
if (action === 'ArrowRight') {
|
||||
if (action === this.$hotkeys.EReader.NEXT_PAGE) {
|
||||
if (this.$refs.readerComponent.next) this.$refs.readerComponent.next()
|
||||
} else if (action === 'ArrowLeft') {
|
||||
} else if (action === this.$hotkeys.EReader.PREV_PAGE) {
|
||||
if (this.$refs.readerComponent.prev) this.$refs.readerComponent.prev()
|
||||
} else if (action === 'Escape') {
|
||||
} else if (action === this.$hotkeys.EReader.CLOSE) {
|
||||
this.close()
|
||||
}
|
||||
},
|
||||
@@ -114,21 +114,17 @@ export default {
|
||||
this.ebookType = 'pdf'
|
||||
} else if (this.selectedAudiobookFile.ext === '.mobi' || this.selectedAudiobookFile.ext === '.azw3') {
|
||||
this.ebookType = 'mobi'
|
||||
// this.initMobi()
|
||||
} else if (this.selectedAudiobookFile.ext === '.epub') {
|
||||
this.ebookType = 'epub'
|
||||
// this.initEpub()
|
||||
} else if (this.selectedAudiobookFile.ext === '.cbr' || this.selectedAudiobookFile.ext === '.cbz') {
|
||||
this.ebookType = 'comic'
|
||||
}
|
||||
} else if (this.epubEbook) {
|
||||
this.ebookType = 'epub'
|
||||
this.ebookUrl = this.getEbookUrl(this.epubEbook.path)
|
||||
// this.initEpub()
|
||||
} else if (this.mobiEbook) {
|
||||
this.ebookType = 'mobi'
|
||||
this.ebookUrl = this.getEbookUrl(this.mobiEbook.path)
|
||||
// this.initMobi()
|
||||
} else if (this.pdfEbook) {
|
||||
this.ebookType = 'pdf'
|
||||
this.ebookUrl = this.getEbookUrl(this.pdfEbook.path)
|
||||
|
||||
+33
-12
@@ -177,6 +177,10 @@ export default {
|
||||
userStreamUpdate(user) {
|
||||
this.$store.commit('users/updateUser', user)
|
||||
},
|
||||
currentUserAudiobookUpdate(payload) {
|
||||
// console.log('Received user audiobook update', payload)
|
||||
this.$store.commit('user/updateUserAudiobook', payload)
|
||||
},
|
||||
downloadToastClick(download) {
|
||||
if (!download || !download.audiobookId) {
|
||||
return console.error('Invalid download object', download)
|
||||
@@ -235,6 +239,12 @@ export default {
|
||||
download.status = this.$constants.DownloadStatus.EXPIRED
|
||||
this.$store.commit('downloads/addUpdateDownload', download)
|
||||
},
|
||||
showErrorToast(message) {
|
||||
this.$toast.error(message)
|
||||
},
|
||||
showSuccessToast(message) {
|
||||
this.$toast.success(message)
|
||||
},
|
||||
logEvtReceived(payload) {
|
||||
this.$store.commit('logs/logEvt', payload)
|
||||
},
|
||||
@@ -285,6 +295,7 @@ export default {
|
||||
this.socket.on('user_online', this.userOnline)
|
||||
this.socket.on('user_offline', this.userOffline)
|
||||
this.socket.on('user_stream_update', this.userStreamUpdate)
|
||||
this.socket.on('current_user_audiobook_update', this.currentUserAudiobookUpdate)
|
||||
|
||||
// Scan Listeners
|
||||
this.socket.on('scan_start', this.scanStart)
|
||||
@@ -298,6 +309,10 @@ export default {
|
||||
this.socket.on('download_killed', this.downloadKilled)
|
||||
this.socket.on('download_expired', this.downloadExpired)
|
||||
|
||||
// Toast Listeners
|
||||
this.socket.on('show_error_toast', this.showErrorToast)
|
||||
this.socket.on('show_success_toast', this.showSuccessToast)
|
||||
|
||||
this.socket.on('log', this.logEvtReceived)
|
||||
|
||||
this.socket.on('backup_applied', this.backupApplied)
|
||||
@@ -331,20 +346,24 @@ export default {
|
||||
var inputs = ['input', 'select', 'button', 'textarea']
|
||||
return activeElement && inputs.indexOf(activeElement.tagName.toLowerCase()) !== -1
|
||||
},
|
||||
keyUp(e) {
|
||||
getHotkeyName(e) {
|
||||
var keyCode = e.keyCode || e.which
|
||||
if (!this.$keynames[keyCode]) {
|
||||
// Unused hotkey
|
||||
return
|
||||
return null
|
||||
}
|
||||
|
||||
var keyName = this.$keynames[keyCode]
|
||||
var name = keyName
|
||||
if (e.shiftKey) name = 'Shift-' + keyName
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log('Hotkey command', name)
|
||||
}
|
||||
return name
|
||||
},
|
||||
keyDown(e) {
|
||||
var name = this.getHotkeyName(e)
|
||||
if (!name) return
|
||||
|
||||
// Input is focused then ignore key press
|
||||
if (this.checkActiveElementIsInput()) {
|
||||
@@ -352,34 +371,36 @@ export default {
|
||||
}
|
||||
|
||||
// Modal is open
|
||||
if (this.$store.state.openModal) {
|
||||
if (this.$store.state.openModal && Object.values(this.$hotkeys.Modal).includes(name)) {
|
||||
this.$eventBus.$emit('modal-hotkey', name)
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
// EReader is open
|
||||
if (this.$store.state.showEReader) {
|
||||
if (this.$store.state.showEReader && Object.values(this.$hotkeys.EReader).includes(name)) {
|
||||
this.$eventBus.$emit('reader-hotkey', name)
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
// Batch selecting
|
||||
if (this.$store.getters['getNumAudiobooksSelected']) {
|
||||
if (this.$store.getters['getNumAudiobooksSelected'] && name === 'Escape') {
|
||||
// ESCAPE key cancels batch selection
|
||||
if (name === 'Escape') {
|
||||
this.$store.commit('setSelectedAudiobooks', [])
|
||||
}
|
||||
this.$store.commit('setSelectedAudiobooks', [])
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
// Playing audiobook
|
||||
if (this.$store.state.streamAudiobook) {
|
||||
if (this.$store.state.streamAudiobook && Object.values(this.$hotkeys.AudioPlayer).includes(name)) {
|
||||
this.$eventBus.$emit('player-hotkey', name)
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('keyup', this.keyUp)
|
||||
window.addEventListener('keydown', this.keyDown)
|
||||
|
||||
this.initializeSocket()
|
||||
this.$store.dispatch('libraries/load')
|
||||
@@ -403,7 +424,7 @@ export default {
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keyup', this.keyUp)
|
||||
window.removeEventListener('keydown', this.keyDown)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "1.4.16",
|
||||
"version": "1.5.5",
|
||||
"description": "Audiobook manager and player",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -397,16 +397,6 @@ export default {
|
||||
this.$store.commit('setBookshelfBookIds', [])
|
||||
this.$store.commit('showEditModal', this.audiobook)
|
||||
},
|
||||
lookupMetadata(index) {
|
||||
this.$axios
|
||||
.$get(`/api/metadata/${this.audiobookId}/${index}`)
|
||||
.then((metadata) => {
|
||||
console.log('Metadata for ' + index, metadata)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
})
|
||||
},
|
||||
audiobookUpdated() {
|
||||
console.log('Audiobook Updated - Fetch full audiobook')
|
||||
this.$axios
|
||||
|
||||
@@ -31,12 +31,24 @@ export default {
|
||||
return {
|
||||
updatingServerSettings: false,
|
||||
dailyBackups: true,
|
||||
backupsToKeep: 2
|
||||
backupsToKeep: 2,
|
||||
newServerSettings: {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
serverSettings(newVal, oldVal) {
|
||||
if (newVal && !oldVal) {
|
||||
this.newServerSettings = { ...this.serverSettings }
|
||||
this.initServerSettings()
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dailyBackupsTooltip() {
|
||||
return 'Runs at 1am every day (your server time). Saved in /metadata/backups.'
|
||||
},
|
||||
serverSettings() {
|
||||
return this.$store.state.serverSettings
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -63,8 +75,16 @@ export default {
|
||||
console.error('Failed to update server settings', error)
|
||||
this.updatingServerSettings = false
|
||||
})
|
||||
},
|
||||
initServerSettings() {
|
||||
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
||||
|
||||
this.backupsToKeep = this.newServerSettings.backupsToKeep || 2
|
||||
this.dailyBackups = !!this.newServerSettings.backupSchedule
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
mounted() {
|
||||
this.initServerSettings()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -145,8 +145,6 @@ export default {
|
||||
|
||||
this.storeCoversInAudiobookDir = this.newServerSettings.coverDestination === this.$constants.CoverDestination.AUDIOBOOK
|
||||
this.storeCoversInAudiobookDir = this.newServerSettings.coverDestination === this.$constants.CoverDestination.AUDIOBOOK
|
||||
this.backupsToKeep = this.newServerSettings.backupsToKeep || 2
|
||||
this.dailyBackups = !!this.newServerSettings.backupSchedule
|
||||
},
|
||||
resetAudiobooks() {
|
||||
if (confirm('WARNING! This action will remove all audiobooks from the database including any updates or matches you have made. This does not do anything to your actual files. Shall we continue?')) {
|
||||
|
||||
@@ -64,6 +64,17 @@ export default {
|
||||
if (this.$route.query.query !== this.searchQuery) {
|
||||
this.newQuery()
|
||||
}
|
||||
} else if (this.id === 'series') {
|
||||
if (this.selectedSeries && this.$route.query.series && this.$route.query.series !== this.$encode(this.selectedSeries)) {
|
||||
// Series changed
|
||||
this.selectedSeries = this.$decode(this.$route.query.series)
|
||||
} else if (!this.selectedSeries && this.$route.query.series) {
|
||||
// Series selected
|
||||
this.selectedSeries = this.$decode(this.$route.query.series)
|
||||
} else if (this.selectedSeries && !this.$route.query.series) {
|
||||
// Series unselected
|
||||
this.selectedSeries = null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -25,8 +25,33 @@ const KeyNames = {
|
||||
76: 'KeyL',
|
||||
77: 'KeyM'
|
||||
}
|
||||
const Hotkeys = {
|
||||
AudioPlayer: {
|
||||
PLAY_PAUSE: 'Space',
|
||||
JUMP_FORWARD: 'ArrowRight',
|
||||
JUMP_BACKWARD: 'ArrowLeft',
|
||||
VOLUME_UP: 'ArrowUp',
|
||||
VOLUME_DOWN: 'ArrowDown',
|
||||
MUTE_UNMUTE: 'KeyM',
|
||||
SHOW_CHAPTERS: 'KeyL',
|
||||
INCREASE_PLAYBACK_RATE: 'Shift-ArrowUp',
|
||||
DECREASE_PLAYBACK_RATE: 'Shift-ArrowDown',
|
||||
CLOSE: 'Escape'
|
||||
},
|
||||
EReader: {
|
||||
NEXT_PAGE: 'ArrowRight',
|
||||
PREV_PAGE: 'ArrowLeft',
|
||||
CLOSE: 'Escape'
|
||||
},
|
||||
Modal: {
|
||||
NEXT_PAGE: 'ArrowRight',
|
||||
PREV_PAGE: 'ArrowLeft',
|
||||
CLOSE: 'Escape'
|
||||
}
|
||||
}
|
||||
|
||||
export default ({ app }, inject) => {
|
||||
inject('constants', Constants)
|
||||
inject('keynames', KeyNames)
|
||||
inject('hotkeys', Hotkeys)
|
||||
}
|
||||
@@ -44,7 +44,7 @@ Vue.prototype.$secondsToTimestamp = (seconds) => {
|
||||
_seconds -= _minutes * 60
|
||||
var _hours = Math.floor(_minutes / 60)
|
||||
_minutes -= _hours * 60
|
||||
_seconds = Math.round(_seconds)
|
||||
_seconds = Math.floor(_seconds)
|
||||
if (!_hours) {
|
||||
return `${_minutes}:${_seconds.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
@@ -22,6 +22,11 @@ export const getters = {
|
||||
getAudiobook: (state) => id => {
|
||||
return state.audiobooks.find(ab => ab.id === id)
|
||||
},
|
||||
getAudiobooksWithIssues: (state) => {
|
||||
return state.audiobooks.filter(ab => {
|
||||
return ab.hasMissingParts || ab.hasInvalidParts || ab.isMissing || ab.isIncomplete
|
||||
})
|
||||
},
|
||||
getEntitiesShowing: (state, getters, rootState, rootGetters) => () => {
|
||||
if (!state.libraryPage) {
|
||||
return getters.getFiltered()
|
||||
@@ -66,7 +71,12 @@ export const getters = {
|
||||
return false
|
||||
})
|
||||
}
|
||||
} else if (filterBy === 'issues') {
|
||||
filtered = filtered.filter(ab => {
|
||||
return ab.hasMissingParts || ab.hasInvalidParts || ab.isMissing || ab.isIncomplete
|
||||
})
|
||||
}
|
||||
|
||||
if (state.keywordFilter) {
|
||||
const keywordFilterKeys = ['title', 'subtitle', 'author', 'series', 'narrator']
|
||||
const keyworkFilter = state.keywordFilter.toLowerCase()
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
|
||||
import Vue from 'vue'
|
||||
|
||||
export const state = () => ({
|
||||
user: null,
|
||||
settings: {
|
||||
@@ -80,6 +82,13 @@ export const mutations = {
|
||||
localStorage.removeItem('token')
|
||||
}
|
||||
},
|
||||
updateUserAudiobook(state, { id, data }) {
|
||||
if (!state.user) return
|
||||
if (!state.user.audiobooks) {
|
||||
Vue.set(state.user, 'audiobooks', {})
|
||||
}
|
||||
Vue.set(state.user.audiobooks, id, data)
|
||||
},
|
||||
setSettings(state, settings) {
|
||||
if (!settings) return
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "1.4.16",
|
||||
"version": "1.5.5",
|
||||
"description": "Self-hosted audiobook server for managing and playing audiobooks",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -39,8 +39,6 @@ Android app is in beta, try it out on the [Google Play Store](https://play.googl
|
||||
See [documentation](https://audiobookshelf.org/docs) for supported directory structure, folder naming conventions, and audio file metadata usage.
|
||||
|
||||
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
** Default username is "root" with no password
|
||||
@@ -103,23 +101,11 @@ System Service: `/lib/systemd/system/audiobookshelf.service`
|
||||
|
||||
Ffmpeg static build: `/usr/lib/audiobookshelf-ffmpeg/`
|
||||
|
||||
## Run from source
|
||||
## Reverse Proxy Set Up
|
||||
|
||||
Note: you will need `npm`, `node12`, and `ffmpeg` to run this project locally
|
||||
### NGINX Reverse Proxy
|
||||
|
||||
```bash
|
||||
git clone https://github.com/advplyr/audiobookshelf.git
|
||||
cd audiobookshelf
|
||||
|
||||
# All paths default to root directory. Config path is the database.
|
||||
# Directories will be created if they don't exist
|
||||
# Paths are relative to the root directory, so "../Audiobooks" would be a valid path
|
||||
npm run prod -- -p [PORT] --audiobooks [AUDIOBOOKS_PATH] --config [CONFIG_PATH] --metadata [METADATA_PATH]
|
||||
```
|
||||
|
||||
### nginx Reverse Proxy
|
||||
|
||||
Add this to the site config file on your nginx server after you have changed the relevant parts in the <> brackets, and inserted the path to you certificates.
|
||||
Add this to the site config file on your nginx server after you have changed the relevant parts in the <> brackets, and inserted your certificate paths.
|
||||
|
||||
|
||||
```bash
|
||||
@@ -149,6 +135,52 @@ server
|
||||
}
|
||||
```
|
||||
|
||||
### Apache Reverse Proxy
|
||||
|
||||
Add this to the site config file on your Apache server after you have changed the relevant parts in the <> brackets, and inserted your certificate paths.
|
||||
|
||||
For this to work you must enable at least the following mods using `a2enmod`:
|
||||
- `ssl`
|
||||
- `proxy_module`
|
||||
- `proxy_wstunnel_module`
|
||||
- `rewrite_module`
|
||||
|
||||
```bash
|
||||
<IfModule mod_ssl.c>
|
||||
<VirtualHost *:443>
|
||||
ServerName <sub>.<domain>.<tld>
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
||||
|
||||
ProxyPreserveHost On
|
||||
ProxyPass / http://localhost:<audiobookshelf_port>/
|
||||
RewriteEngine on
|
||||
RewriteCond %{HTTP:Upgrade} websocket [NC]
|
||||
RewriteCond %{HTTP:Connection} upgrade [NC]
|
||||
RewriteRule ^/?(.*) "ws://localhost:<audiobookshelf_port>/$1" [P,L]
|
||||
|
||||
# unless you're doing something special this should be generated by a
|
||||
# tool like certbot by let's encrypt
|
||||
SSLCertificateFile /path/to/cert/file
|
||||
SSLCertificateKeyFile /path/to/key/file
|
||||
</VirtualHost>
|
||||
</IfModule>
|
||||
```
|
||||
|
||||
## Run from source
|
||||
|
||||
Note: you will need `npm`, `node12`, and `ffmpeg` to run this project locally
|
||||
|
||||
```bash
|
||||
git clone https://github.com/advplyr/audiobookshelf.git
|
||||
cd audiobookshelf
|
||||
|
||||
# All paths default to root directory. Config path is the database.
|
||||
# Directories will be created if they don't exist
|
||||
# Paths are relative to the root directory, so "../Audiobooks" would be a valid path
|
||||
npm run prod -- -p [PORT] --audiobooks [AUDIOBOOKS_PATH] --config [CONFIG_PATH] --metadata [METADATA_PATH]
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
+92
-5
@@ -11,6 +11,7 @@ const { version } = require('../package.json')
|
||||
// Utils
|
||||
const { ScanResult } = require('./utils/constants')
|
||||
const filePerms = require('./utils/filePerms')
|
||||
const { secondsToTimestamp } = require('./utils/fileUtils')
|
||||
const Logger = require('./Logger')
|
||||
|
||||
// Classes
|
||||
@@ -46,7 +47,7 @@ class Server {
|
||||
this.watcher = new Watcher(this.AudiobookPath)
|
||||
this.coverController = new CoverController(this.db, this.MetadataPath, this.AudiobookPath)
|
||||
this.scanner = new Scanner(this.AudiobookPath, this.MetadataPath, this.db, this.coverController, this.emitter.bind(this))
|
||||
this.streamManager = new StreamManager(this.db, this.MetadataPath, this.emitter.bind(this))
|
||||
this.streamManager = new StreamManager(this.db, this.MetadataPath, 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.emitter.bind(this))
|
||||
this.apiController = new ApiController(this.MetadataPath, this.db, this.scanner, this.auth, this.streamManager, this.rssFeeds, this.downloadManager, this.coverController, this.backupManager, this.watcher, this.emitter.bind(this), this.clientEmitter.bind(this))
|
||||
@@ -86,7 +87,6 @@ class Server {
|
||||
clientEmitter(userId, ev, data) {
|
||||
var clients = this.getClientsForUser(userId)
|
||||
if (!clients.length) {
|
||||
console.log('clients', clients)
|
||||
return Logger.error(`[Server] clientEmitter - no clients found for user ${userId}`)
|
||||
}
|
||||
clients.forEach((client) => {
|
||||
@@ -247,7 +247,7 @@ class Server {
|
||||
socket.on('close_stream', () => this.streamManager.closeStreamRequest(socket))
|
||||
socket.on('stream_update', (payload) => this.streamManager.streamUpdate(socket, payload))
|
||||
|
||||
socket.on('progress_update', (payload) => this.audiobookProgressUpdate(socket.sheepClient, payload))
|
||||
socket.on('progress_update', (payload) => this.audiobookProgressUpdate(socket, payload))
|
||||
|
||||
// Downloading
|
||||
socket.on('download', (payload) => this.downloadManager.downloadSocketRequest(socket, payload))
|
||||
@@ -260,6 +260,11 @@ class Server {
|
||||
socket.on('create_backup', () => this.backupManager.requestCreateBackup(socket))
|
||||
socket.on('apply_backup', (id) => this.backupManager.requestApplyBackup(socket, id))
|
||||
|
||||
// Bookmarks
|
||||
socket.on('create_bookmark', (payload) => this.createBookmark(socket, payload))
|
||||
socket.on('update_bookmark', (payload) => this.updateBookmark(socket, payload))
|
||||
socket.on('delete_bookmark', (payload) => this.deleteBookmark(socket, payload))
|
||||
|
||||
socket.on('test', () => {
|
||||
socket.emit('test_received', socket.id)
|
||||
})
|
||||
@@ -451,12 +456,94 @@ class Server {
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
audiobookProgressUpdate(client, progressPayload) {
|
||||
async audiobookProgressUpdate(socket, progressPayload) {
|
||||
var client = socket.sheepClient
|
||||
if (!client || !client.user) {
|
||||
Logger.error('[Server] audiobookProgressUpdate invalid socket client')
|
||||
return
|
||||
}
|
||||
client.user.updateAudiobookProgress(progressPayload.audiobookId, progressPayload)
|
||||
var audiobookProgress = client.user.updateAudiobookProgress(progressPayload.audiobookId, progressPayload)
|
||||
if (audiobookProgress) {
|
||||
await this.db.updateEntity('user', client.user)
|
||||
|
||||
// This audiobook progress is out of date, why?
|
||||
// var userAudiobook = client.user.getAudiobookJSON(progressPayload.audiobookId)
|
||||
// Logger.debug(`[Server] Emitting audiobook progress update to clients ${this.getClientsForUser(client.user.id).length}: ${JSON.stringify(userAudiobook)}`)
|
||||
|
||||
this.clientEmitter(client.user.id, 'current_user_audiobook_update', {
|
||||
id: progressPayload.audiobookId,
|
||||
data: audiobookProgress || null
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async createBookmark(socket, payload) {
|
||||
var client = socket.sheepClient
|
||||
if (!client || !client.user) {
|
||||
Logger.error('[Server] createBookmark invalid socket client')
|
||||
return
|
||||
}
|
||||
var userAudiobook = client.user.createBookmark(payload)
|
||||
if (!userAudiobook || userAudiobook.error) {
|
||||
var failMessage = (userAudiobook ? userAudiobook.error : null) || 'Unknown Error'
|
||||
socket.emit('show_error_toast', `Failed to create Bookmark: ${failMessage}`)
|
||||
return
|
||||
}
|
||||
|
||||
await this.db.updateEntity('user', client.user)
|
||||
|
||||
socket.emit('show_success_toast', `${secondsToTimestamp(payload.time)} Bookmarked`)
|
||||
|
||||
this.clientEmitter(client.user.id, 'current_user_audiobook_update', {
|
||||
id: userAudiobook.audiobookId,
|
||||
data: userAudiobook || null
|
||||
})
|
||||
}
|
||||
|
||||
async updateBookmark(socket, payload) {
|
||||
var client = socket.sheepClient
|
||||
if (!client || !client.user) {
|
||||
Logger.error('[Server] updateBookmark invalid socket client')
|
||||
return
|
||||
}
|
||||
var userAudiobook = client.user.updateBookmark(payload)
|
||||
if (!userAudiobook || userAudiobook.error) {
|
||||
var failMessage = (userAudiobook ? userAudiobook.error : null) || 'Unknown Error'
|
||||
socket.emit('show_error_toast', `Failed to update Bookmark: ${failMessage}`)
|
||||
return
|
||||
}
|
||||
|
||||
await this.db.updateEntity('user', client.user)
|
||||
|
||||
socket.emit('show_success_toast', `Bookmark ${secondsToTimestamp(payload.time)} Updated`)
|
||||
|
||||
this.clientEmitter(client.user.id, 'current_user_audiobook_update', {
|
||||
id: userAudiobook.audiobookId,
|
||||
data: userAudiobook || null
|
||||
})
|
||||
}
|
||||
|
||||
async deleteBookmark(socket, payload) {
|
||||
var client = socket.sheepClient
|
||||
if (!client || !client.user) {
|
||||
Logger.error('[Server] deleteBookmark invalid socket client')
|
||||
return
|
||||
}
|
||||
var userAudiobook = client.user.deleteBookmark(payload)
|
||||
if (!userAudiobook || userAudiobook.error) {
|
||||
var failMessage = (userAudiobook ? userAudiobook.error : null) || 'Unknown Error'
|
||||
socket.emit('show_error_toast', `Failed to delete Bookmark: ${failMessage}`)
|
||||
return
|
||||
}
|
||||
|
||||
await this.db.updateEntity('user', client.user)
|
||||
|
||||
socket.emit('show_success_toast', `Bookmark ${secondsToTimestamp(payload.time)} Removed`)
|
||||
|
||||
this.clientEmitter(client.user.id, 'current_user_audiobook_update', {
|
||||
id: userAudiobook.audiobookId,
|
||||
data: userAudiobook || null
|
||||
})
|
||||
}
|
||||
|
||||
async authenticateSocket(socket, token) {
|
||||
|
||||
+10
-2
@@ -5,10 +5,11 @@ const fs = require('fs-extra')
|
||||
const Path = require('path')
|
||||
|
||||
class StreamManager {
|
||||
constructor(db, MetadataPath, emitter) {
|
||||
constructor(db, MetadataPath, emitter, clientEmitter) {
|
||||
this.db = db
|
||||
|
||||
this.emitter = emitter
|
||||
this.clientEmitter = clientEmitter
|
||||
|
||||
this.MetadataPath = MetadataPath
|
||||
this.streams = []
|
||||
@@ -153,8 +154,15 @@ class StreamManager {
|
||||
Logger.error('Invalid User for client', client)
|
||||
return
|
||||
}
|
||||
client.user.updateAudiobookProgressFromStream(client.stream)
|
||||
var userAudiobook = client.user.updateAudiobookProgressFromStream(client.stream)
|
||||
this.db.updateEntity('user', client.user)
|
||||
|
||||
if (userAudiobook) {
|
||||
this.clientEmitter(client.user.id, 'current_user_audiobook_update', {
|
||||
id: userAudiobook.audiobookId,
|
||||
data: userAudiobook.toJSON()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports = StreamManager
|
||||
@@ -0,0 +1,32 @@
|
||||
class AudioBookmark {
|
||||
constructor(bookmark) {
|
||||
this.title = null
|
||||
this.time = null
|
||||
this.createdAt = null
|
||||
|
||||
if (bookmark) {
|
||||
this.construct(bookmark)
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
title: this.title || '',
|
||||
time: this.time,
|
||||
createdAt: this.createdAt
|
||||
}
|
||||
}
|
||||
|
||||
construct(bookmark) {
|
||||
this.title = bookmark.title || ''
|
||||
this.time = bookmark.time || 0
|
||||
this.createdAt = bookmark.createdAt
|
||||
}
|
||||
|
||||
setData(time, title) {
|
||||
this.title = title
|
||||
this.time = time
|
||||
this.createdAt = Date.now()
|
||||
}
|
||||
}
|
||||
module.exports = AudioBookmark
|
||||
@@ -193,16 +193,14 @@ class Audiobook {
|
||||
lastUpdate: this.lastUpdate,
|
||||
duration: this.totalDuration,
|
||||
size: this.totalSize,
|
||||
hasBookMatch: !!this.book,
|
||||
hasMissingParts: this.missingParts ? this.missingParts.length : 0,
|
||||
hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0,
|
||||
// numEbooks: this.ebooks.length,
|
||||
ebooks: this.ebooks.map(ebook => ebook.toJSON()),
|
||||
numEbooks: this.ebooks.length,
|
||||
numTracks: this.tracks.length,
|
||||
chapters: this.chapters || [],
|
||||
isMissing: !!this.isMissing,
|
||||
isInvalid: !!this.isInvalid
|
||||
isInvalid: !!this.isInvalid,
|
||||
hasMissingParts: this.missingParts ? this.missingParts.length : 0,
|
||||
hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,7 +230,9 @@ class Audiobook {
|
||||
tracks: this.tracksToJSON(),
|
||||
chapters: this.chapters || [],
|
||||
isMissing: !!this.isMissing,
|
||||
isInvalid: !!this.isInvalid
|
||||
isInvalid: !!this.isInvalid,
|
||||
hasMissingParts: this.missingParts ? this.missingParts.length : 0,
|
||||
hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
const Logger = require('../Logger')
|
||||
const AudioBookmark = require('./AudioBookmark')
|
||||
|
||||
class AudiobookProgress {
|
||||
constructor(progress) {
|
||||
this.audiobookId = null
|
||||
@@ -10,12 +13,18 @@ class AudiobookProgress {
|
||||
this.lastUpdate = null
|
||||
this.startedAt = null
|
||||
this.finishedAt = null
|
||||
this.bookmarks = []
|
||||
|
||||
if (progress) {
|
||||
this.construct(progress)
|
||||
}
|
||||
}
|
||||
|
||||
bookmarksToJSON() {
|
||||
if (!this.bookmarks) return []
|
||||
return this.bookmarks.map(b => b.toJSON())
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
audiobookId: this.audiobookId,
|
||||
@@ -25,7 +34,8 @@ class AudiobookProgress {
|
||||
isRead: this.isRead,
|
||||
lastUpdate: this.lastUpdate,
|
||||
startedAt: this.startedAt,
|
||||
finishedAt: this.finishedAt
|
||||
finishedAt: this.finishedAt,
|
||||
bookmarks: this.bookmarksToJSON()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +48,11 @@ class AudiobookProgress {
|
||||
this.lastUpdate = progress.lastUpdate
|
||||
this.startedAt = progress.startedAt
|
||||
this.finishedAt = progress.finishedAt || null
|
||||
if (progress.bookmarks) {
|
||||
this.bookmarks = progress.bookmarks.map(b => new AudioBookmark(b))
|
||||
} else {
|
||||
this.bookmarks = []
|
||||
}
|
||||
}
|
||||
|
||||
updateFromStream(stream) {
|
||||
@@ -54,11 +69,9 @@ class AudiobookProgress {
|
||||
// If has < 10 seconds remaining mark as read
|
||||
var timeRemaining = this.totalDuration - this.currentTime
|
||||
if (timeRemaining < 10) {
|
||||
if (!this.isRead) {
|
||||
this.isRead = true
|
||||
this.progress = 1
|
||||
this.finishedAt = Date.now()
|
||||
}
|
||||
this.isRead = true
|
||||
this.progress = 1
|
||||
this.finishedAt = Date.now()
|
||||
} else {
|
||||
this.isRead = false
|
||||
this.finishedAt = null
|
||||
@@ -67,6 +80,7 @@ class AudiobookProgress {
|
||||
|
||||
update(payload) {
|
||||
var hasUpdates = false
|
||||
Logger.debug(`[AudiobookProgress] Update called ${JSON.stringify(payload)}`)
|
||||
for (const key in payload) {
|
||||
if (payload[key] !== this[key]) {
|
||||
if (key === 'isRead') {
|
||||
@@ -92,5 +106,27 @@ class AudiobookProgress {
|
||||
}
|
||||
return hasUpdates
|
||||
}
|
||||
|
||||
checkBookmarkExists(time) {
|
||||
return this.bookmarks.find(bm => bm.time === time)
|
||||
}
|
||||
|
||||
createBookmark(time, title) {
|
||||
var newBookmark = new AudioBookmark()
|
||||
newBookmark.setData(time, title)
|
||||
this.bookmarks.push(newBookmark)
|
||||
return newBookmark
|
||||
}
|
||||
|
||||
updateBookmark(time, title) {
|
||||
var bookmark = this.bookmarks.find(bm => bm.time === time)
|
||||
if (!bookmark) return false
|
||||
bookmark.title = title
|
||||
return bookmark
|
||||
}
|
||||
|
||||
deleteBookmark(time) {
|
||||
this.bookmarks = this.bookmarks.filter(bm => bm.time !== time)
|
||||
}
|
||||
}
|
||||
module.exports = AudiobookProgress
|
||||
+36
-15
@@ -16,6 +16,7 @@ class Stream extends EventEmitter {
|
||||
this.audiobook = audiobook
|
||||
|
||||
this.segmentLength = 6
|
||||
this.maxSeekBackTime = 30
|
||||
this.streamPath = Path.join(streamPath, this.id)
|
||||
this.concatFilesPath = Path.join(this.streamPath, 'files.txt')
|
||||
this.playlistPath = Path.join(this.streamPath, 'output.m3u8')
|
||||
@@ -67,7 +68,7 @@ class Stream extends EventEmitter {
|
||||
|
||||
get segmentStartNumber() {
|
||||
if (!this.startTime) return 0
|
||||
return Math.floor(this.startTime / this.segmentLength)
|
||||
return Math.floor(Math.max(this.startTime - this.maxSeekBackTime, 0) / this.segmentLength)
|
||||
}
|
||||
|
||||
get numSegments() {
|
||||
@@ -82,13 +83,26 @@ class Stream extends EventEmitter {
|
||||
return this.audiobook.tracks
|
||||
}
|
||||
|
||||
get clientUser() {
|
||||
return this.client.user || {}
|
||||
}
|
||||
|
||||
get clientUserAudiobooks() {
|
||||
return this.clientUser.audiobooks || {}
|
||||
}
|
||||
|
||||
get clientUserAudiobookData() {
|
||||
return this.clientUserAudiobooks[this.audiobookId]
|
||||
}
|
||||
|
||||
get clientPlaylistUri() {
|
||||
return `/hls/${this.id}/output.m3u8`
|
||||
}
|
||||
|
||||
get clientProgress() {
|
||||
if (!this.clientCurrentTime) return 0
|
||||
return Number((this.clientCurrentTime / this.totalDuration).toFixed(3))
|
||||
var prog = Math.min(1, this.clientCurrentTime / this.totalDuration)
|
||||
return Number(prog.toFixed(3))
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
@@ -103,18 +117,17 @@ class Stream extends EventEmitter {
|
||||
clientCurrentTime: this.clientCurrentTime,
|
||||
startTime: this.startTime,
|
||||
segmentStartNumber: this.segmentStartNumber,
|
||||
isTranscodeComplete: this.isTranscodeComplete
|
||||
isTranscodeComplete: this.isTranscodeComplete,
|
||||
lastUpdate: this.clientUserAudiobookData ? this.clientUserAudiobookData.lastUpdate : 0
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
var clientUserAudiobooks = this.client.user ? this.client.user.audiobooks || {} : {}
|
||||
var userAudiobook = clientUserAudiobooks[this.audiobookId] || null
|
||||
if (userAudiobook) {
|
||||
var timeRemaining = this.totalDuration - userAudiobook.currentTime
|
||||
Logger.info('[STREAM] User has progress for audiobook', userAudiobook.progress, `Time Remaining: ${timeRemaining}s`)
|
||||
if (this.clientUserAudiobookData) {
|
||||
var timeRemaining = this.totalDuration - this.clientUserAudiobookData.currentTime
|
||||
Logger.info('[STREAM] User has progress for audiobook', this.clientUserAudiobookData.progress, `Time Remaining: ${timeRemaining}s`)
|
||||
if (timeRemaining > 15) {
|
||||
this.startTime = userAudiobook.currentTime
|
||||
this.startTime = this.clientUserAudiobookData.currentTime
|
||||
this.clientCurrentTime = this.startTime
|
||||
}
|
||||
}
|
||||
@@ -238,20 +251,28 @@ class Stream extends EventEmitter {
|
||||
|
||||
this.ffmpeg = Ffmpeg()
|
||||
|
||||
var trackStartTime = await writeConcatFile(this.tracks, this.concatFilesPath, this.startTime)
|
||||
var adjustedStartTime = Math.max(this.startTime - this.maxSeekBackTime, 0)
|
||||
var trackStartTime = await writeConcatFile(this.tracks, this.concatFilesPath, adjustedStartTime)
|
||||
|
||||
this.ffmpeg.addInput(this.concatFilesPath)
|
||||
// seek_timestamp : https://ffmpeg.org/ffmpeg.html
|
||||
// the argument to the -ss option is considered an actual timestamp, and is not offset by the start time of the file
|
||||
// fixes https://github.com/advplyr/audiobookshelf/issues/116
|
||||
this.ffmpeg.inputOption('-seek_timestamp 1')
|
||||
this.ffmpeg.inputFormat('concat')
|
||||
this.ffmpeg.inputOption('-safe 0')
|
||||
|
||||
if (this.startTime > 0) {
|
||||
const shiftedStartTime = this.startTime - trackStartTime
|
||||
Logger.info(`[STREAM] Starting Stream at startTime ${secondsToTimestamp(this.startTime)} and Segment #${this.segmentStartNumber}`)
|
||||
this.ffmpeg.inputOption(`-ss ${shiftedStartTime}`)
|
||||
if (adjustedStartTime > 0) {
|
||||
const shiftedStartTime = adjustedStartTime - trackStartTime
|
||||
// Issues using exact fractional seconds i.e. 29.49814 - changing to 29.5s
|
||||
var startTimeS = Math.round(shiftedStartTime * 10) / 10 + 's'
|
||||
Logger.info(`[STREAM] Starting Stream at startTime ${secondsToTimestamp(adjustedStartTime)} (User startTime ${secondsToTimestamp(this.startTime)}) and Segment #${this.segmentStartNumber}`)
|
||||
this.ffmpeg.inputOption(`-ss ${startTimeS}`)
|
||||
|
||||
this.ffmpeg.inputOption('-noaccurate_seek')
|
||||
}
|
||||
|
||||
const logLevel = process.env.NODE_ENV === 'production' ? 'error' : 'warning'
|
||||
const logLevel = process.env.NODE_ENV === 'production' ? 'error' : 'error'
|
||||
const audioCodec = (this.hlsSegmentType === 'fmp4' || this.tracksAudioFileType === 'opus') ? 'aac' : 'copy'
|
||||
this.ffmpeg.addOption([
|
||||
`-loglevel ${logLevel}`,
|
||||
|
||||
+62
-1
@@ -1,3 +1,4 @@
|
||||
const Logger = require('../Logger')
|
||||
const AudiobookProgress = require('./AudiobookProgress')
|
||||
|
||||
class User {
|
||||
@@ -203,6 +204,7 @@ class User {
|
||||
this.audiobooks[stream.audiobookId] = new AudiobookProgress()
|
||||
}
|
||||
this.audiobooks[stream.audiobookId].updateFromStream(stream)
|
||||
return this.audiobooks[stream.audiobookId]
|
||||
}
|
||||
|
||||
updateAudiobookProgress(audiobook, updatePayload) {
|
||||
@@ -211,7 +213,12 @@ class User {
|
||||
this.audiobooks[audiobook.id] = new AudiobookProgress()
|
||||
this.audiobooks[audiobook.id].audiobookId = audiobook.id
|
||||
}
|
||||
return this.audiobooks[audiobook.id].update(updatePayload)
|
||||
var wasUpdated = this.audiobooks[audiobook.id].update(updatePayload)
|
||||
if (wasUpdated) {
|
||||
Logger.debug(`[User] Audiobook progress was updated ${JSON.stringify(this.audiobooks[audiobook.id])}`)
|
||||
return this.audiobooks[audiobook.id]
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Returns Boolean If update was made
|
||||
@@ -267,5 +274,59 @@ class User {
|
||||
if (!this.librariesAccessible) return false
|
||||
return this.librariesAccessible.includes(libraryId)
|
||||
}
|
||||
|
||||
getAudiobookJSON(audiobookId) {
|
||||
return this.audiobooks[audiobookId] ? this.audiobooks[audiobookId].toJSON() : null
|
||||
}
|
||||
|
||||
createBookmark({ audiobookId, time, title }) {
|
||||
if (!this.audiobooks || !this.audiobooks[audiobookId]) {
|
||||
return {
|
||||
error: 'Invalid Audiobook'
|
||||
}
|
||||
}
|
||||
if (this.audiobooks[audiobookId].checkBookmarkExists(time)) {
|
||||
return {
|
||||
error: 'Bookmark already exists'
|
||||
}
|
||||
}
|
||||
|
||||
var success = this.audiobooks[audiobookId].createBookmark(time, title)
|
||||
if (success) return this.audiobooks[audiobookId]
|
||||
return null
|
||||
}
|
||||
|
||||
updateBookmark({ audiobookId, time, title }) {
|
||||
if (!this.audiobooks || !this.audiobooks[audiobookId]) {
|
||||
return {
|
||||
error: 'Invalid Audiobook'
|
||||
}
|
||||
}
|
||||
if (!this.audiobooks[audiobookId].checkBookmarkExists(time)) {
|
||||
return {
|
||||
error: 'Bookmark does not exist'
|
||||
}
|
||||
}
|
||||
|
||||
var success = this.audiobooks[audiobookId].updateBookmark(time, title)
|
||||
if (success) return this.audiobooks[audiobookId]
|
||||
return null
|
||||
}
|
||||
|
||||
deleteBookmark({ audiobookId, time }) {
|
||||
if (!this.audiobooks || !this.audiobooks[audiobookId]) {
|
||||
return {
|
||||
error: 'Invalid Audiobook'
|
||||
}
|
||||
}
|
||||
if (!this.audiobooks[audiobookId].checkBookmarkExists(time)) {
|
||||
return {
|
||||
error: 'Bookmark does not exist'
|
||||
}
|
||||
}
|
||||
|
||||
this.audiobooks[audiobookId].deleteBookmark(time)
|
||||
return this.audiobooks[audiobookId]
|
||||
}
|
||||
}
|
||||
module.exports = User
|
||||
@@ -69,7 +69,7 @@ function secondsToTimestamp(seconds) {
|
||||
_seconds -= _minutes * 60
|
||||
var _hours = Math.floor(_minutes / 60)
|
||||
_minutes -= _hours * 60
|
||||
_seconds = Math.round(_seconds)
|
||||
_seconds = Math.floor(_seconds)
|
||||
if (!_hours) {
|
||||
return `${_minutes}:${_seconds.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user