mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-28 13:06:16 +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-feature-settings: 'liga';
|
||||||
-webkit-font-smoothing: antialiased;
|
-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;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div class="w-full -mt-4">
|
||||||
<div class="w-full relative mb-4">
|
<div class="w-full relative mb-2">
|
||||||
<div class="absolute left-2 top-0 bottom-0 h-full flex items-center">
|
<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">00:00:00</p>
|
<p ref="currentTimestamp" class="font-mono text-sm text-gray-100 pointer-events-auto">00:00:00</p>
|
||||||
</div>
|
<p class="font-mono text-sm text-gray-100 pointer-events-auto"> / {{ progressPercent }}%</p>
|
||||||
<div class="absolute right-2 top-0 bottom-0 h-full flex items-center">
|
<div class="flex-grow" />
|
||||||
<p class="font-mono text-sm">{{ totalDurationPretty }}</p>
|
<p class="font-mono text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute right-20 top-0 bottom-0">
|
<div v-if="chapters.length" class="absolute right-20 top-0 bottom-0 h-full flex items-end">
|
||||||
<div v-if="chapters.length" class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
|
<div class="cursor-pointer text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
|
||||||
<span class="material-icons text-3xl">format_list_bulleted</span>
|
<span class="material-icons text-3xl">format_list_bulleted</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="showExperimentalFeatures" class="absolute top-0 bottom-0 h-full flex items-end" :class="chapters.length ? ' right-32' : 'right-20'">
|
||||||
<div class="absolute right-32 top-0 bottom-0">
|
<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" />
|
<controls-volume-control ref="volumeControl" v-model="volume" @input="updateVolume" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex my-2">
|
|
||||||
<div class="flex-grow" />
|
|
||||||
|
|
||||||
|
<div class="flex pb-2">
|
||||||
|
<div class="flex-grow" />
|
||||||
<template v-if="!loading">
|
<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">
|
<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>
|
<span class="material-icons text-3xl">first_page</span>
|
||||||
@@ -85,6 +89,10 @@ export default {
|
|||||||
chapters: {
|
chapters: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
|
},
|
||||||
|
bookmarks: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -116,6 +124,17 @@ export default {
|
|||||||
totalDurationPretty() {
|
totalDurationPretty() {
|
||||||
return this.$secondsToTimestamp(this.totalDuration)
|
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() {
|
chapterTicks() {
|
||||||
return this.chapters.map((chap) => {
|
return this.chapters.map((chap) => {
|
||||||
var perc = chap.start / this.totalDuration
|
var perc = chap.start / this.totalDuration
|
||||||
@@ -127,6 +146,9 @@ export default {
|
|||||||
},
|
},
|
||||||
currentChapter() {
|
currentChapter() {
|
||||||
return this.chapters.find((chapter) => chapter.start <= this.currentTime && this.currentTime < chapter.end)
|
return this.chapters.find((chapter) => chapter.start <= this.currentTime && this.currentTime < chapter.end)
|
||||||
|
},
|
||||||
|
showExperimentalFeatures() {
|
||||||
|
return this.$store.state.showExperimentalFeatures
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -134,6 +156,11 @@ export default {
|
|||||||
this.seek(chapter.start)
|
this.seek(chapter.start)
|
||||||
this.showChaptersModal = false
|
this.showChaptersModal = false
|
||||||
},
|
},
|
||||||
|
selectBookmark(bookmark) {
|
||||||
|
if (bookmark) {
|
||||||
|
this.seek(bookmark.time)
|
||||||
|
}
|
||||||
|
},
|
||||||
seek(time) {
|
seek(time) {
|
||||||
if (this.loading) {
|
if (this.loading) {
|
||||||
return
|
return
|
||||||
@@ -232,6 +259,7 @@ export default {
|
|||||||
},
|
},
|
||||||
restart() {
|
restart() {
|
||||||
this.seek(0)
|
this.seek(0)
|
||||||
|
this.$nextTick(this.sendStreamUpdate)
|
||||||
},
|
},
|
||||||
backward10() {
|
backward10() {
|
||||||
var newTime = this.audioEl.currentTime - 10
|
var newTime = this.audioEl.currentTime - 10
|
||||||
@@ -346,7 +374,7 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var lastbuff = this.getLastBufferedTime()
|
var lastbuff = this.getLastBufferedTime()
|
||||||
this.sendStreamUpdate()
|
|
||||||
var bufferlen = (lastbuff / this.audioEl.duration) * this.trackWidth
|
var bufferlen = (lastbuff / this.audioEl.duration) * this.trackWidth
|
||||||
bufferlen = Math.round(bufferlen)
|
bufferlen = Math.round(bufferlen)
|
||||||
if (this.bufferTrackWidth === bufferlen || !this.$refs.bufferTrack) return
|
if (this.bufferTrackWidth === bufferlen || !this.$refs.bufferTrack) return
|
||||||
@@ -373,6 +401,13 @@ export default {
|
|||||||
|
|
||||||
this.updateTimestamp()
|
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
|
this.currentTime = this.audioEl.currentTime
|
||||||
|
|
||||||
var perc = this.audioEl.currentTime / this.audioEl.duration
|
var perc = this.audioEl.currentTime / this.audioEl.duration
|
||||||
@@ -399,6 +434,7 @@ export default {
|
|||||||
},
|
},
|
||||||
audioLoadedData() {
|
audioLoadedData() {
|
||||||
this.totalDuration = this.audioEl.duration
|
this.totalDuration = this.audioEl.duration
|
||||||
|
this.$emit('loaded', this.totalDuration)
|
||||||
},
|
},
|
||||||
set(url, currentTime, playOnLoad = false) {
|
set(url, currentTime, playOnLoad = false) {
|
||||||
if (this.hlsInstance) {
|
if (this.hlsInstance) {
|
||||||
@@ -454,6 +490,9 @@ export default {
|
|||||||
if (!this.chapters.length) return
|
if (!this.chapters.length) return
|
||||||
this.showChaptersModal = !this.showChaptersModal
|
this.showChaptersModal = !this.showChaptersModal
|
||||||
},
|
},
|
||||||
|
showBookmarks() {
|
||||||
|
this.$emit('showBookmarks', this.currentTime)
|
||||||
|
},
|
||||||
play() {
|
play() {
|
||||||
if (!this.$refs.audio) {
|
if (!this.$refs.audio) {
|
||||||
console.error('No Audio ref')
|
console.error('No Audio ref')
|
||||||
@@ -535,16 +574,16 @@ export default {
|
|||||||
this.$emit('close')
|
this.$emit('close')
|
||||||
},
|
},
|
||||||
hotkey(action) {
|
hotkey(action) {
|
||||||
if (action === 'Space') this.playPauseClick()
|
if (action === this.$hotkeys.AudioPlayer.PLAY_PAUSE) this.playPauseClick()
|
||||||
else if (action === 'ArrowRight') this.forward10()
|
else if (action === this.$hotkeys.AudioPlayer.JUMP_FORWARD) this.forward10()
|
||||||
else if (action === 'ArrowLeft') this.backward10()
|
else if (action === this.$hotkeys.AudioPlayer.JUMP_BACKWARD) this.backward10()
|
||||||
else if (action === 'ArrowUp') this.volumeUp()
|
else if (action === this.$hotkeys.AudioPlayer.VOLUME_UP) this.volumeUp()
|
||||||
else if (action === 'ArrowDown') this.volumeDown()
|
else if (action === this.$hotkeys.AudioPlayer.VOLUME_DOWN) this.volumeDown()
|
||||||
else if (action === 'KeyM') this.toggleMute()
|
else if (action === this.$hotkeys.AudioPlayer.MUTE_UNMUTE) this.toggleMute()
|
||||||
else if (action === 'KeyL') this.showChapters()
|
else if (action === this.$hotkeys.AudioPlayer.SHOW_CHAPTERS) this.showChapters()
|
||||||
else if (action === 'Shift-ArrowUp') this.increasePlaybackRate()
|
else if (action === this.$hotkeys.AudioPlayer.INCREASE_PLAYBACK_RATE) this.increasePlaybackRate()
|
||||||
else if (action === 'Shift-ArrowDown') this.decreasePlaybackRate()
|
else if (action === this.$hotkeys.AudioPlayer.DECREASE_PLAYBACK_RATE) this.decreasePlaybackRate()
|
||||||
else if (action === 'Escape') this.closePlayer()
|
else if (action === this.$hotkeys.AudioPlayer.CLOSE) this.closePlayer()
|
||||||
},
|
},
|
||||||
windowResize() {
|
windowResize() {
|
||||||
this.setTrackWidth()
|
this.setTrackWidth()
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ export default {
|
|||||||
'$route.query.filter'() {
|
'$route.query.filter'() {
|
||||||
if (this.$route.query.filter && this.$route.query.filter !== this.filterBy) {
|
if (this.$route.query.filter && this.$route.query.filter !== this.filterBy) {
|
||||||
this.$store.dispatch('user/updateUserSettings', { filterBy: this.$route.query.filter })
|
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" />
|
<div v-show="homePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</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">
|
<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" />
|
<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>
|
</svg>
|
||||||
|
|
||||||
<p class="font-book pt-1.5" style="font-size: 1rem">Library</p>
|
<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>
|
||||||
|
|
||||||
<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'">
|
<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" />
|
<div v-show="paramId === 'series'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</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'">
|
<!-- <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">
|
<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" />
|
<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() {
|
homePage() {
|
||||||
return this.$route.name === 'library-library'
|
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: {},
|
methods: {},
|
||||||
|
|||||||
@@ -1,20 +1,30 @@
|
|||||||
<template>
|
<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">
|
<nuxt-link :to="`/audiobook/${streamAudiobook.id}`" class="absolute -top-16 left-4 cursor-pointer">
|
||||||
<cards-book-cover :audiobook="streamAudiobook" :width="88" />
|
<cards-book-cover :audiobook="streamAudiobook" :width="88" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<div class="flex items-center pl-24">
|
<div class="flex items-center pl-24">
|
||||||
<div>
|
<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>
|
{{ title }} <span v-if="stream && $isDev" class="text-xs text-gray-400">({{ stream.id }})</span>
|
||||||
</nuxt-link>
|
</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>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<span v-if="stream" class="material-icons px-4 cursor-pointer" @click="cancelStream">close</span>
|
<span v-if="stream" class="material-icons px-4 cursor-pointer" @click="cancelStream">close</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -24,7 +34,11 @@ export default {
|
|||||||
return {
|
return {
|
||||||
audioPlayerReady: false,
|
audioPlayerReady: false,
|
||||||
lastServerUpdateSentSeconds: 0,
|
lastServerUpdateSentSeconds: 0,
|
||||||
stream: null
|
stream: null,
|
||||||
|
totalDuration: 0,
|
||||||
|
showBookmarksModal: false,
|
||||||
|
bookmarkCurrentTime: 0,
|
||||||
|
bookmarkAudiobookId: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -35,6 +49,14 @@ export default {
|
|||||||
user() {
|
user() {
|
||||||
return this.$store.state.user.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() {
|
isLoading() {
|
||||||
if (!this.streamAudiobook) return false
|
if (!this.streamAudiobook) return false
|
||||||
if (this.stream) {
|
if (this.stream) {
|
||||||
@@ -46,6 +68,9 @@ export default {
|
|||||||
streamAudiobook() {
|
streamAudiobook() {
|
||||||
return this.$store.state.streamAudiobook
|
return this.$store.state.streamAudiobook
|
||||||
},
|
},
|
||||||
|
audiobookId() {
|
||||||
|
return this.streamAudiobook ? this.streamAudiobook.id : null
|
||||||
|
},
|
||||||
book() {
|
book() {
|
||||||
return this.streamAudiobook ? this.streamAudiobook.book || {} : {}
|
return this.streamAudiobook ? this.streamAudiobook.book || {} : {}
|
||||||
},
|
},
|
||||||
@@ -66,9 +91,49 @@ export default {
|
|||||||
},
|
},
|
||||||
libraryId() {
|
libraryId() {
|
||||||
return this.streamAudiobook ? this.streamAudiobook.libraryId : null
|
return this.streamAudiobook ? this.streamAudiobook.libraryId : null
|
||||||
|
},
|
||||||
|
totalDurationPretty() {
|
||||||
|
return this.$secondsToTimestamp(this.totalDuration)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
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() {
|
filterByAuthor() {
|
||||||
if (this.$route.name !== 'index') {
|
if (this.$route.name !== 'index') {
|
||||||
this.$router.push(`/library/${this.libraryId || this.$store.state.libraries.currentLibraryId}/bookshelf`)
|
this.$router.push(`/library/${this.libraryId || this.$store.state.libraries.currentLibraryId}/bookshelf`)
|
||||||
|
|||||||
@@ -101,6 +101,11 @@ export default {
|
|||||||
text: 'Progress',
|
text: 'Progress',
|
||||||
value: 'progress',
|
value: 'progress',
|
||||||
sublist: true
|
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) {
|
hotkey(action) {
|
||||||
if (action === 'ArrowRight') {
|
if (action === this.$hotkeys.Modal.NEXT_PAGE) {
|
||||||
this.goNextBook()
|
this.goNextBook()
|
||||||
} else if (action === 'ArrowLeft') {
|
} else if (action === this.$hotkeys.Modal.PREV_PAGE) {
|
||||||
this.goPrevBook()
|
this.goPrevBook()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
hotkey(action) {
|
hotkey(action) {
|
||||||
if (action === 'Escape') {
|
if (action === this.$hotkeys.Modal.CLOSE) {
|
||||||
this.show = false
|
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>
|
||||||
|
|
||||||
<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="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">
|
<div class="flex items-center px-4">
|
||||||
<ui-btn v-if="userCanDelete" color="error" type="button" small @click.stop.prevent="deleteAudiobook">Remove</ui-btn>
|
<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-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>
|
||||||
|
|
||||||
<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-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>
|
</ui-tooltip>
|
||||||
|
|
||||||
<div class="flex-grow" />
|
|
||||||
<ui-btn type="submit">Submit</ui-btn>
|
<ui-btn type="submit">Submit</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -121,6 +122,9 @@ export default {
|
|||||||
isRootUser() {
|
isRootUser() {
|
||||||
return this.$store.getters['user/getIsRoot']
|
return this.$store.getters['user/getIsRoot']
|
||||||
},
|
},
|
||||||
|
isMissing() {
|
||||||
|
return !!this.audiobook && !!this.audiobook.isMissing
|
||||||
|
},
|
||||||
audiobookId() {
|
audiobookId() {
|
||||||
return this.audiobook ? this.audiobook.id : null
|
return this.audiobook ? this.audiobook.id : null
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -91,11 +91,11 @@ export default {
|
|||||||
console.log('Reader hotkey', action)
|
console.log('Reader hotkey', action)
|
||||||
if (!this.$refs.readerComponent) return
|
if (!this.$refs.readerComponent) return
|
||||||
|
|
||||||
if (action === 'ArrowRight') {
|
if (action === this.$hotkeys.EReader.NEXT_PAGE) {
|
||||||
if (this.$refs.readerComponent.next) this.$refs.readerComponent.next()
|
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()
|
if (this.$refs.readerComponent.prev) this.$refs.readerComponent.prev()
|
||||||
} else if (action === 'Escape') {
|
} else if (action === this.$hotkeys.EReader.CLOSE) {
|
||||||
this.close()
|
this.close()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -114,21 +114,17 @@ export default {
|
|||||||
this.ebookType = 'pdf'
|
this.ebookType = 'pdf'
|
||||||
} else if (this.selectedAudiobookFile.ext === '.mobi' || this.selectedAudiobookFile.ext === '.azw3') {
|
} else if (this.selectedAudiobookFile.ext === '.mobi' || this.selectedAudiobookFile.ext === '.azw3') {
|
||||||
this.ebookType = 'mobi'
|
this.ebookType = 'mobi'
|
||||||
// this.initMobi()
|
|
||||||
} else if (this.selectedAudiobookFile.ext === '.epub') {
|
} else if (this.selectedAudiobookFile.ext === '.epub') {
|
||||||
this.ebookType = 'epub'
|
this.ebookType = 'epub'
|
||||||
// this.initEpub()
|
|
||||||
} else if (this.selectedAudiobookFile.ext === '.cbr' || this.selectedAudiobookFile.ext === '.cbz') {
|
} else if (this.selectedAudiobookFile.ext === '.cbr' || this.selectedAudiobookFile.ext === '.cbz') {
|
||||||
this.ebookType = 'comic'
|
this.ebookType = 'comic'
|
||||||
}
|
}
|
||||||
} else if (this.epubEbook) {
|
} else if (this.epubEbook) {
|
||||||
this.ebookType = 'epub'
|
this.ebookType = 'epub'
|
||||||
this.ebookUrl = this.getEbookUrl(this.epubEbook.path)
|
this.ebookUrl = this.getEbookUrl(this.epubEbook.path)
|
||||||
// this.initEpub()
|
|
||||||
} else if (this.mobiEbook) {
|
} else if (this.mobiEbook) {
|
||||||
this.ebookType = 'mobi'
|
this.ebookType = 'mobi'
|
||||||
this.ebookUrl = this.getEbookUrl(this.mobiEbook.path)
|
this.ebookUrl = this.getEbookUrl(this.mobiEbook.path)
|
||||||
// this.initMobi()
|
|
||||||
} else if (this.pdfEbook) {
|
} else if (this.pdfEbook) {
|
||||||
this.ebookType = 'pdf'
|
this.ebookType = 'pdf'
|
||||||
this.ebookUrl = this.getEbookUrl(this.pdfEbook.path)
|
this.ebookUrl = this.getEbookUrl(this.pdfEbook.path)
|
||||||
|
|||||||
+33
-12
@@ -177,6 +177,10 @@ export default {
|
|||||||
userStreamUpdate(user) {
|
userStreamUpdate(user) {
|
||||||
this.$store.commit('users/updateUser', user)
|
this.$store.commit('users/updateUser', user)
|
||||||
},
|
},
|
||||||
|
currentUserAudiobookUpdate(payload) {
|
||||||
|
// console.log('Received user audiobook update', payload)
|
||||||
|
this.$store.commit('user/updateUserAudiobook', payload)
|
||||||
|
},
|
||||||
downloadToastClick(download) {
|
downloadToastClick(download) {
|
||||||
if (!download || !download.audiobookId) {
|
if (!download || !download.audiobookId) {
|
||||||
return console.error('Invalid download object', download)
|
return console.error('Invalid download object', download)
|
||||||
@@ -235,6 +239,12 @@ export default {
|
|||||||
download.status = this.$constants.DownloadStatus.EXPIRED
|
download.status = this.$constants.DownloadStatus.EXPIRED
|
||||||
this.$store.commit('downloads/addUpdateDownload', download)
|
this.$store.commit('downloads/addUpdateDownload', download)
|
||||||
},
|
},
|
||||||
|
showErrorToast(message) {
|
||||||
|
this.$toast.error(message)
|
||||||
|
},
|
||||||
|
showSuccessToast(message) {
|
||||||
|
this.$toast.success(message)
|
||||||
|
},
|
||||||
logEvtReceived(payload) {
|
logEvtReceived(payload) {
|
||||||
this.$store.commit('logs/logEvt', payload)
|
this.$store.commit('logs/logEvt', payload)
|
||||||
},
|
},
|
||||||
@@ -285,6 +295,7 @@ export default {
|
|||||||
this.socket.on('user_online', this.userOnline)
|
this.socket.on('user_online', this.userOnline)
|
||||||
this.socket.on('user_offline', this.userOffline)
|
this.socket.on('user_offline', this.userOffline)
|
||||||
this.socket.on('user_stream_update', this.userStreamUpdate)
|
this.socket.on('user_stream_update', this.userStreamUpdate)
|
||||||
|
this.socket.on('current_user_audiobook_update', this.currentUserAudiobookUpdate)
|
||||||
|
|
||||||
// Scan Listeners
|
// Scan Listeners
|
||||||
this.socket.on('scan_start', this.scanStart)
|
this.socket.on('scan_start', this.scanStart)
|
||||||
@@ -298,6 +309,10 @@ export default {
|
|||||||
this.socket.on('download_killed', this.downloadKilled)
|
this.socket.on('download_killed', this.downloadKilled)
|
||||||
this.socket.on('download_expired', this.downloadExpired)
|
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('log', this.logEvtReceived)
|
||||||
|
|
||||||
this.socket.on('backup_applied', this.backupApplied)
|
this.socket.on('backup_applied', this.backupApplied)
|
||||||
@@ -331,20 +346,24 @@ export default {
|
|||||||
var inputs = ['input', 'select', 'button', 'textarea']
|
var inputs = ['input', 'select', 'button', 'textarea']
|
||||||
return activeElement && inputs.indexOf(activeElement.tagName.toLowerCase()) !== -1
|
return activeElement && inputs.indexOf(activeElement.tagName.toLowerCase()) !== -1
|
||||||
},
|
},
|
||||||
keyUp(e) {
|
getHotkeyName(e) {
|
||||||
var keyCode = e.keyCode || e.which
|
var keyCode = e.keyCode || e.which
|
||||||
if (!this.$keynames[keyCode]) {
|
if (!this.$keynames[keyCode]) {
|
||||||
// Unused hotkey
|
// Unused hotkey
|
||||||
return
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
var keyName = this.$keynames[keyCode]
|
var keyName = this.$keynames[keyCode]
|
||||||
var name = keyName
|
var name = keyName
|
||||||
if (e.shiftKey) name = 'Shift-' + keyName
|
if (e.shiftKey) name = 'Shift-' + keyName
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
console.log('Hotkey command', name)
|
console.log('Hotkey command', name)
|
||||||
}
|
}
|
||||||
|
return name
|
||||||
|
},
|
||||||
|
keyDown(e) {
|
||||||
|
var name = this.getHotkeyName(e)
|
||||||
|
if (!name) return
|
||||||
|
|
||||||
// Input is focused then ignore key press
|
// Input is focused then ignore key press
|
||||||
if (this.checkActiveElementIsInput()) {
|
if (this.checkActiveElementIsInput()) {
|
||||||
@@ -352,34 +371,36 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Modal is open
|
// 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)
|
this.$eventBus.$emit('modal-hotkey', name)
|
||||||
|
e.preventDefault()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// EReader is open
|
// 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)
|
this.$eventBus.$emit('reader-hotkey', name)
|
||||||
|
e.preventDefault()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch selecting
|
// Batch selecting
|
||||||
if (this.$store.getters['getNumAudiobooksSelected']) {
|
if (this.$store.getters['getNumAudiobooksSelected'] && name === 'Escape') {
|
||||||
// ESCAPE key cancels batch selection
|
// ESCAPE key cancels batch selection
|
||||||
if (name === 'Escape') {
|
this.$store.commit('setSelectedAudiobooks', [])
|
||||||
this.$store.commit('setSelectedAudiobooks', [])
|
e.preventDefault()
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Playing audiobook
|
// 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)
|
this.$eventBus.$emit('player-hotkey', name)
|
||||||
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
document.addEventListener('keyup', this.keyUp)
|
window.addEventListener('keydown', this.keyDown)
|
||||||
|
|
||||||
this.initializeSocket()
|
this.initializeSocket()
|
||||||
this.$store.dispatch('libraries/load')
|
this.$store.dispatch('libraries/load')
|
||||||
@@ -403,7 +424,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
document.removeEventListener('keyup', this.keyUp)
|
window.removeEventListener('keydown', this.keyDown)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "1.4.16",
|
"version": "1.5.5",
|
||||||
"description": "Audiobook manager and player",
|
"description": "Audiobook manager and player",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -397,16 +397,6 @@ export default {
|
|||||||
this.$store.commit('setBookshelfBookIds', [])
|
this.$store.commit('setBookshelfBookIds', [])
|
||||||
this.$store.commit('showEditModal', this.audiobook)
|
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() {
|
audiobookUpdated() {
|
||||||
console.log('Audiobook Updated - Fetch full audiobook')
|
console.log('Audiobook Updated - Fetch full audiobook')
|
||||||
this.$axios
|
this.$axios
|
||||||
|
|||||||
@@ -31,12 +31,24 @@ export default {
|
|||||||
return {
|
return {
|
||||||
updatingServerSettings: false,
|
updatingServerSettings: false,
|
||||||
dailyBackups: true,
|
dailyBackups: true,
|
||||||
backupsToKeep: 2
|
backupsToKeep: 2,
|
||||||
|
newServerSettings: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
serverSettings(newVal, oldVal) {
|
||||||
|
if (newVal && !oldVal) {
|
||||||
|
this.newServerSettings = { ...this.serverSettings }
|
||||||
|
this.initServerSettings()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
dailyBackupsTooltip() {
|
dailyBackupsTooltip() {
|
||||||
return 'Runs at 1am every day (your server time). Saved in /metadata/backups.'
|
return 'Runs at 1am every day (your server time). Saved in /metadata/backups.'
|
||||||
|
},
|
||||||
|
serverSettings() {
|
||||||
|
return this.$store.state.serverSettings
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -63,8 +75,16 @@ export default {
|
|||||||
console.error('Failed to update server settings', error)
|
console.error('Failed to update server settings', error)
|
||||||
this.updatingServerSettings = false
|
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>
|
</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.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() {
|
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?')) {
|
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) {
|
if (this.$route.query.query !== this.searchQuery) {
|
||||||
this.newQuery()
|
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',
|
76: 'KeyL',
|
||||||
77: 'KeyM'
|
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) => {
|
export default ({ app }, inject) => {
|
||||||
inject('constants', Constants)
|
inject('constants', Constants)
|
||||||
inject('keynames', KeyNames)
|
inject('keynames', KeyNames)
|
||||||
|
inject('hotkeys', Hotkeys)
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ Vue.prototype.$secondsToTimestamp = (seconds) => {
|
|||||||
_seconds -= _minutes * 60
|
_seconds -= _minutes * 60
|
||||||
var _hours = Math.floor(_minutes / 60)
|
var _hours = Math.floor(_minutes / 60)
|
||||||
_minutes -= _hours * 60
|
_minutes -= _hours * 60
|
||||||
_seconds = Math.round(_seconds)
|
_seconds = Math.floor(_seconds)
|
||||||
if (!_hours) {
|
if (!_hours) {
|
||||||
return `${_minutes}:${_seconds.toString().padStart(2, '0')}`
|
return `${_minutes}:${_seconds.toString().padStart(2, '0')}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ export const getters = {
|
|||||||
getAudiobook: (state) => id => {
|
getAudiobook: (state) => id => {
|
||||||
return state.audiobooks.find(ab => ab.id === 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) => () => {
|
getEntitiesShowing: (state, getters, rootState, rootGetters) => () => {
|
||||||
if (!state.libraryPage) {
|
if (!state.libraryPage) {
|
||||||
return getters.getFiltered()
|
return getters.getFiltered()
|
||||||
@@ -66,7 +71,12 @@ export const getters = {
|
|||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
} else if (filterBy === 'issues') {
|
||||||
|
filtered = filtered.filter(ab => {
|
||||||
|
return ab.hasMissingParts || ab.hasInvalidParts || ab.isMissing || ab.isIncomplete
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.keywordFilter) {
|
if (state.keywordFilter) {
|
||||||
const keywordFilterKeys = ['title', 'subtitle', 'author', 'series', 'narrator']
|
const keywordFilterKeys = ['title', 'subtitle', 'author', 'series', 'narrator']
|
||||||
const keyworkFilter = state.keywordFilter.toLowerCase()
|
const keyworkFilter = state.keywordFilter.toLowerCase()
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
user: null,
|
user: null,
|
||||||
settings: {
|
settings: {
|
||||||
@@ -80,6 +82,13 @@ export const mutations = {
|
|||||||
localStorage.removeItem('token')
|
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) {
|
setSettings(state, settings) {
|
||||||
if (!settings) return
|
if (!settings) return
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "1.4.16",
|
"version": "1.5.5",
|
||||||
"description": "Self-hosted audiobook server for managing and playing audiobooks",
|
"description": "Self-hosted audiobook server for managing and playing audiobooks",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -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.
|
See [documentation](https://audiobookshelf.org/docs) for supported directory structure, folder naming conventions, and audio file metadata usage.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
** Default username is "root" with no password
|
** 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/`
|
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
|
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.
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
```bash
|
```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
|
## Contributing
|
||||||
|
|
||||||
|
|||||||
+92
-5
@@ -11,6 +11,7 @@ const { version } = require('../package.json')
|
|||||||
// Utils
|
// Utils
|
||||||
const { ScanResult } = require('./utils/constants')
|
const { ScanResult } = require('./utils/constants')
|
||||||
const filePerms = require('./utils/filePerms')
|
const filePerms = require('./utils/filePerms')
|
||||||
|
const { secondsToTimestamp } = require('./utils/fileUtils')
|
||||||
const Logger = require('./Logger')
|
const Logger = require('./Logger')
|
||||||
|
|
||||||
// Classes
|
// Classes
|
||||||
@@ -46,7 +47,7 @@ class Server {
|
|||||||
this.watcher = new Watcher(this.AudiobookPath)
|
this.watcher = new Watcher(this.AudiobookPath)
|
||||||
this.coverController = new CoverController(this.db, this.MetadataPath, 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.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.rssFeeds = new RssFeeds(this.Port, this.db)
|
||||||
this.downloadManager = new DownloadManager(this.db, this.MetadataPath, this.AudiobookPath, this.emitter.bind(this))
|
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))
|
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) {
|
clientEmitter(userId, ev, data) {
|
||||||
var clients = this.getClientsForUser(userId)
|
var clients = this.getClientsForUser(userId)
|
||||||
if (!clients.length) {
|
if (!clients.length) {
|
||||||
console.log('clients', clients)
|
|
||||||
return Logger.error(`[Server] clientEmitter - no clients found for user ${userId}`)
|
return Logger.error(`[Server] clientEmitter - no clients found for user ${userId}`)
|
||||||
}
|
}
|
||||||
clients.forEach((client) => {
|
clients.forEach((client) => {
|
||||||
@@ -247,7 +247,7 @@ class Server {
|
|||||||
socket.on('close_stream', () => this.streamManager.closeStreamRequest(socket))
|
socket.on('close_stream', () => this.streamManager.closeStreamRequest(socket))
|
||||||
socket.on('stream_update', (payload) => this.streamManager.streamUpdate(socket, payload))
|
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
|
// Downloading
|
||||||
socket.on('download', (payload) => this.downloadManager.downloadSocketRequest(socket, payload))
|
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('create_backup', () => this.backupManager.requestCreateBackup(socket))
|
||||||
socket.on('apply_backup', (id) => this.backupManager.requestApplyBackup(socket, id))
|
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.on('test', () => {
|
||||||
socket.emit('test_received', socket.id)
|
socket.emit('test_received', socket.id)
|
||||||
})
|
})
|
||||||
@@ -451,12 +456,94 @@ class Server {
|
|||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
audiobookProgressUpdate(client, progressPayload) {
|
async audiobookProgressUpdate(socket, progressPayload) {
|
||||||
|
var client = socket.sheepClient
|
||||||
if (!client || !client.user) {
|
if (!client || !client.user) {
|
||||||
Logger.error('[Server] audiobookProgressUpdate invalid socket client')
|
Logger.error('[Server] audiobookProgressUpdate invalid socket client')
|
||||||
return
|
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) {
|
async authenticateSocket(socket, token) {
|
||||||
|
|||||||
+10
-2
@@ -5,10 +5,11 @@ const fs = require('fs-extra')
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
|
|
||||||
class StreamManager {
|
class StreamManager {
|
||||||
constructor(db, MetadataPath, emitter) {
|
constructor(db, MetadataPath, emitter, clientEmitter) {
|
||||||
this.db = db
|
this.db = db
|
||||||
|
|
||||||
this.emitter = emitter
|
this.emitter = emitter
|
||||||
|
this.clientEmitter = clientEmitter
|
||||||
|
|
||||||
this.MetadataPath = MetadataPath
|
this.MetadataPath = MetadataPath
|
||||||
this.streams = []
|
this.streams = []
|
||||||
@@ -153,8 +154,15 @@ class StreamManager {
|
|||||||
Logger.error('Invalid User for client', client)
|
Logger.error('Invalid User for client', client)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
client.user.updateAudiobookProgressFromStream(client.stream)
|
var userAudiobook = client.user.updateAudiobookProgressFromStream(client.stream)
|
||||||
this.db.updateEntity('user', client.user)
|
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
|
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,
|
lastUpdate: this.lastUpdate,
|
||||||
duration: this.totalDuration,
|
duration: this.totalDuration,
|
||||||
size: this.totalSize,
|
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()),
|
ebooks: this.ebooks.map(ebook => ebook.toJSON()),
|
||||||
numEbooks: this.ebooks.length,
|
numEbooks: this.ebooks.length,
|
||||||
numTracks: this.tracks.length,
|
numTracks: this.tracks.length,
|
||||||
chapters: this.chapters || [],
|
chapters: this.chapters || [],
|
||||||
isMissing: !!this.isMissing,
|
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(),
|
tracks: this.tracksToJSON(),
|
||||||
chapters: this.chapters || [],
|
chapters: this.chapters || [],
|
||||||
isMissing: !!this.isMissing,
|
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 {
|
class AudiobookProgress {
|
||||||
constructor(progress) {
|
constructor(progress) {
|
||||||
this.audiobookId = null
|
this.audiobookId = null
|
||||||
@@ -10,12 +13,18 @@ class AudiobookProgress {
|
|||||||
this.lastUpdate = null
|
this.lastUpdate = null
|
||||||
this.startedAt = null
|
this.startedAt = null
|
||||||
this.finishedAt = null
|
this.finishedAt = null
|
||||||
|
this.bookmarks = []
|
||||||
|
|
||||||
if (progress) {
|
if (progress) {
|
||||||
this.construct(progress)
|
this.construct(progress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bookmarksToJSON() {
|
||||||
|
if (!this.bookmarks) return []
|
||||||
|
return this.bookmarks.map(b => b.toJSON())
|
||||||
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
audiobookId: this.audiobookId,
|
audiobookId: this.audiobookId,
|
||||||
@@ -25,7 +34,8 @@ class AudiobookProgress {
|
|||||||
isRead: this.isRead,
|
isRead: this.isRead,
|
||||||
lastUpdate: this.lastUpdate,
|
lastUpdate: this.lastUpdate,
|
||||||
startedAt: this.startedAt,
|
startedAt: this.startedAt,
|
||||||
finishedAt: this.finishedAt
|
finishedAt: this.finishedAt,
|
||||||
|
bookmarks: this.bookmarksToJSON()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +48,11 @@ class AudiobookProgress {
|
|||||||
this.lastUpdate = progress.lastUpdate
|
this.lastUpdate = progress.lastUpdate
|
||||||
this.startedAt = progress.startedAt
|
this.startedAt = progress.startedAt
|
||||||
this.finishedAt = progress.finishedAt || null
|
this.finishedAt = progress.finishedAt || null
|
||||||
|
if (progress.bookmarks) {
|
||||||
|
this.bookmarks = progress.bookmarks.map(b => new AudioBookmark(b))
|
||||||
|
} else {
|
||||||
|
this.bookmarks = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFromStream(stream) {
|
updateFromStream(stream) {
|
||||||
@@ -54,11 +69,9 @@ class AudiobookProgress {
|
|||||||
// If has < 10 seconds remaining mark as read
|
// If has < 10 seconds remaining mark as read
|
||||||
var timeRemaining = this.totalDuration - this.currentTime
|
var timeRemaining = this.totalDuration - this.currentTime
|
||||||
if (timeRemaining < 10) {
|
if (timeRemaining < 10) {
|
||||||
if (!this.isRead) {
|
this.isRead = true
|
||||||
this.isRead = true
|
this.progress = 1
|
||||||
this.progress = 1
|
this.finishedAt = Date.now()
|
||||||
this.finishedAt = Date.now()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this.isRead = false
|
this.isRead = false
|
||||||
this.finishedAt = null
|
this.finishedAt = null
|
||||||
@@ -67,6 +80,7 @@ class AudiobookProgress {
|
|||||||
|
|
||||||
update(payload) {
|
update(payload) {
|
||||||
var hasUpdates = false
|
var hasUpdates = false
|
||||||
|
Logger.debug(`[AudiobookProgress] Update called ${JSON.stringify(payload)}`)
|
||||||
for (const key in payload) {
|
for (const key in payload) {
|
||||||
if (payload[key] !== this[key]) {
|
if (payload[key] !== this[key]) {
|
||||||
if (key === 'isRead') {
|
if (key === 'isRead') {
|
||||||
@@ -92,5 +106,27 @@ class AudiobookProgress {
|
|||||||
}
|
}
|
||||||
return hasUpdates
|
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
|
module.exports = AudiobookProgress
|
||||||
+36
-15
@@ -16,6 +16,7 @@ class Stream extends EventEmitter {
|
|||||||
this.audiobook = audiobook
|
this.audiobook = audiobook
|
||||||
|
|
||||||
this.segmentLength = 6
|
this.segmentLength = 6
|
||||||
|
this.maxSeekBackTime = 30
|
||||||
this.streamPath = Path.join(streamPath, this.id)
|
this.streamPath = Path.join(streamPath, this.id)
|
||||||
this.concatFilesPath = Path.join(this.streamPath, 'files.txt')
|
this.concatFilesPath = Path.join(this.streamPath, 'files.txt')
|
||||||
this.playlistPath = Path.join(this.streamPath, 'output.m3u8')
|
this.playlistPath = Path.join(this.streamPath, 'output.m3u8')
|
||||||
@@ -67,7 +68,7 @@ class Stream extends EventEmitter {
|
|||||||
|
|
||||||
get segmentStartNumber() {
|
get segmentStartNumber() {
|
||||||
if (!this.startTime) return 0
|
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() {
|
get numSegments() {
|
||||||
@@ -82,13 +83,26 @@ class Stream extends EventEmitter {
|
|||||||
return this.audiobook.tracks
|
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() {
|
get clientPlaylistUri() {
|
||||||
return `/hls/${this.id}/output.m3u8`
|
return `/hls/${this.id}/output.m3u8`
|
||||||
}
|
}
|
||||||
|
|
||||||
get clientProgress() {
|
get clientProgress() {
|
||||||
if (!this.clientCurrentTime) return 0
|
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() {
|
toJSON() {
|
||||||
@@ -103,18 +117,17 @@ class Stream extends EventEmitter {
|
|||||||
clientCurrentTime: this.clientCurrentTime,
|
clientCurrentTime: this.clientCurrentTime,
|
||||||
startTime: this.startTime,
|
startTime: this.startTime,
|
||||||
segmentStartNumber: this.segmentStartNumber,
|
segmentStartNumber: this.segmentStartNumber,
|
||||||
isTranscodeComplete: this.isTranscodeComplete
|
isTranscodeComplete: this.isTranscodeComplete,
|
||||||
|
lastUpdate: this.clientUserAudiobookData ? this.clientUserAudiobookData.lastUpdate : 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
var clientUserAudiobooks = this.client.user ? this.client.user.audiobooks || {} : {}
|
if (this.clientUserAudiobookData) {
|
||||||
var userAudiobook = clientUserAudiobooks[this.audiobookId] || null
|
var timeRemaining = this.totalDuration - this.clientUserAudiobookData.currentTime
|
||||||
if (userAudiobook) {
|
Logger.info('[STREAM] User has progress for audiobook', this.clientUserAudiobookData.progress, `Time Remaining: ${timeRemaining}s`)
|
||||||
var timeRemaining = this.totalDuration - userAudiobook.currentTime
|
|
||||||
Logger.info('[STREAM] User has progress for audiobook', userAudiobook.progress, `Time Remaining: ${timeRemaining}s`)
|
|
||||||
if (timeRemaining > 15) {
|
if (timeRemaining > 15) {
|
||||||
this.startTime = userAudiobook.currentTime
|
this.startTime = this.clientUserAudiobookData.currentTime
|
||||||
this.clientCurrentTime = this.startTime
|
this.clientCurrentTime = this.startTime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,20 +251,28 @@ class Stream extends EventEmitter {
|
|||||||
|
|
||||||
this.ffmpeg = Ffmpeg()
|
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)
|
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.inputFormat('concat')
|
||||||
this.ffmpeg.inputOption('-safe 0')
|
this.ffmpeg.inputOption('-safe 0')
|
||||||
|
|
||||||
if (this.startTime > 0) {
|
if (adjustedStartTime > 0) {
|
||||||
const shiftedStartTime = this.startTime - trackStartTime
|
const shiftedStartTime = adjustedStartTime - trackStartTime
|
||||||
Logger.info(`[STREAM] Starting Stream at startTime ${secondsToTimestamp(this.startTime)} and Segment #${this.segmentStartNumber}`)
|
// Issues using exact fractional seconds i.e. 29.49814 - changing to 29.5s
|
||||||
this.ffmpeg.inputOption(`-ss ${shiftedStartTime}`)
|
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')
|
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'
|
const audioCodec = (this.hlsSegmentType === 'fmp4' || this.tracksAudioFileType === 'opus') ? 'aac' : 'copy'
|
||||||
this.ffmpeg.addOption([
|
this.ffmpeg.addOption([
|
||||||
`-loglevel ${logLevel}`,
|
`-loglevel ${logLevel}`,
|
||||||
|
|||||||
+62
-1
@@ -1,3 +1,4 @@
|
|||||||
|
const Logger = require('../Logger')
|
||||||
const AudiobookProgress = require('./AudiobookProgress')
|
const AudiobookProgress = require('./AudiobookProgress')
|
||||||
|
|
||||||
class User {
|
class User {
|
||||||
@@ -203,6 +204,7 @@ class User {
|
|||||||
this.audiobooks[stream.audiobookId] = new AudiobookProgress()
|
this.audiobooks[stream.audiobookId] = new AudiobookProgress()
|
||||||
}
|
}
|
||||||
this.audiobooks[stream.audiobookId].updateFromStream(stream)
|
this.audiobooks[stream.audiobookId].updateFromStream(stream)
|
||||||
|
return this.audiobooks[stream.audiobookId]
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAudiobookProgress(audiobook, updatePayload) {
|
updateAudiobookProgress(audiobook, updatePayload) {
|
||||||
@@ -211,7 +213,12 @@ class User {
|
|||||||
this.audiobooks[audiobook.id] = new AudiobookProgress()
|
this.audiobooks[audiobook.id] = new AudiobookProgress()
|
||||||
this.audiobooks[audiobook.id].audiobookId = audiobook.id
|
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
|
// Returns Boolean If update was made
|
||||||
@@ -267,5 +274,59 @@ class User {
|
|||||||
if (!this.librariesAccessible) return false
|
if (!this.librariesAccessible) return false
|
||||||
return this.librariesAccessible.includes(libraryId)
|
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
|
module.exports = User
|
||||||
@@ -69,7 +69,7 @@ function secondsToTimestamp(seconds) {
|
|||||||
_seconds -= _minutes * 60
|
_seconds -= _minutes * 60
|
||||||
var _hours = Math.floor(_minutes / 60)
|
var _hours = Math.floor(_minutes / 60)
|
||||||
_minutes -= _hours * 60
|
_minutes -= _hours * 60
|
||||||
_seconds = Math.round(_seconds)
|
_seconds = Math.floor(_seconds)
|
||||||
if (!_hours) {
|
if (!_hours) {
|
||||||
return `${_minutes}:${_seconds.toString().padStart(2, '0')}`
|
return `${_minutes}:${_seconds.toString().padStart(2, '0')}`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user