+
Your Progress: {{ Math.round(progressPercent * 100) }}%
+
{{ $elapsedPretty(userTimeRemaining) }} remaining
+
+ close
+
+
@@ -88,7 +104,17 @@ export default {
},
data() {
return {
- resettingProgress: false
+ isRead: false,
+ resettingProgress: false,
+ isProcessingReadUpdate: false
+ }
+ },
+ watch: {
+ userIsRead: {
+ immediate: true,
+ handler(newVal) {
+ this.isRead = newVal
+ }
}
},
computed: {
@@ -149,7 +175,7 @@ export default {
},
authorTooltipText() {
var txt = ['FL: ' + this.authorFL || 'Not Set', 'LF: ' + this.authorLF || 'Not Set']
- return txt.join('\n')
+ return txt.join('
')
},
series() {
return this.book.series || null
@@ -189,7 +215,7 @@ export default {
return this.audiobook.audioFiles || []
},
description() {
- return this.book.description || 'No Description'
+ return this.book.description || ''
},
userAudiobooks() {
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
@@ -200,6 +226,9 @@ export default {
userCurrentTime() {
return this.userAudiobook ? this.userAudiobook.currentTime : 0
},
+ userIsRead() {
+ return this.userAudiobook ? !!this.userAudiobook.isRead : false
+ },
userTimeRemaining() {
return this.duration - this.userCurrentTime
},
@@ -214,6 +243,23 @@ export default {
}
},
methods: {
+ toggleRead() {
+ var updatePayload = {
+ isRead: !this.isRead
+ }
+ this.isProcessingReadUpdate = true
+ this.$axios
+ .$patch(`/api/user/audiobook/${this.audiobookId}`, updatePayload)
+ .then(() => {
+ this.isProcessingReadUpdate = false
+ this.$toast.success(`"${this.title}" Marked as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
+ })
+ .catch((error) => {
+ console.error('Failed', error)
+ this.isProcessingReadUpdate = false
+ this.$toast.error(`Failed to mark as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
+ })
+ },
openRssFeed() {
this.$axios
.$post('/api/feed', { audiobookId: this.audiobook.id })
@@ -269,6 +315,9 @@ export default {
this.resettingProgress = false
})
}
+ },
+ downloadClick() {
+ this.$store.commit('showEditModalOnTab', { audiobook: this.audiobook, tab: 'download' })
}
},
mounted() {
diff --git a/client/store/index.js b/client/store/index.js
index 73966e2d..688ad59f 100644
--- a/client/store/index.js
+++ b/client/store/index.js
@@ -3,6 +3,7 @@ import Vue from 'vue'
export const state = () => ({
serverSettings: null,
streamAudiobook: null,
+ editModalTab: 'details',
showEditModal: false,
selectedAudiobook: null,
playOnLoad: false,
@@ -63,9 +64,18 @@ export const mutations = {
state.playOnLoad = val
},
showEditModal(state, audiobook) {
+ state.editModalTab = 'details'
state.selectedAudiobook = audiobook
state.showEditModal = true
},
+ showEditModalOnTab(state, { audiobook, tab }) {
+ state.editModalTab = tab
+ state.selectedAudiobook = audiobook
+ state.showEditModal = true
+ },
+ setEditModalTab(state, tab) {
+ state.editModalTab = tab
+ },
setShowEditModal(state, val) {
state.showEditModal = val
},
diff --git a/docker-template.xml b/docker-template.xml
index 436bc218..0997b6fc 100644
--- a/docker-template.xml
+++ b/docker-template.xml
@@ -10,7 +10,7 @@
https://forums.unraid.net/topic/112698-support-audiobookshelf/
https://github.com/advplyr/audiobookshelf
**(Android app in beta, try it out)** Audiobook manager and player. Saves your progress, supports multiple accounts, stream all audio formats on the fly. No more switching between dozens of audio files for a single audiobook, Audiobookshelf shows you one audio track with skipping, seeking and adjustable playback speed. Free & open source mobile apps under construction, consider contributing by posting feedback, suggestions, feature requests on github or the forums.
- MediaApp:Books MediaServer:Books Status:Beta
+ MediaApp:Books MediaServer:Books
http://[IP]:[PORT:80]
https://raw.githubusercontent.com/advplyr/docker-templates/master/audiobookshelf.xml
https://github.com/advplyr/audiobookshelf/raw/master/client/static/Logo.png
diff --git a/package.json b/package.json
index 32e97559..9f8a87e3 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
- "version": "1.0.5",
+ "version": "1.0.6",
"description": "Self-hosted audiobook server for managing and playing audiobooks.",
"main": "index.js",
"scripts": {
diff --git a/server/ApiController.js b/server/ApiController.js
index 30baa7f1..d29e9c41 100644
--- a/server/ApiController.js
+++ b/server/ApiController.js
@@ -36,6 +36,7 @@ class ApiController {
this.router.patch('/match/:id', this.match.bind(this))
this.router.delete('/user/audiobook/:id', this.resetUserAudiobookProgress.bind(this))
+ this.router.patch('/user/audiobook/:id', this.updateUserAudiobookProgress.bind(this))
this.router.patch('/user/password', this.userChangePassword.bind(this))
this.router.patch('/user/settings', this.userUpdateSettings.bind(this))
this.router.get('/users', this.getUsers.bind(this))
@@ -233,7 +234,16 @@ class ApiController {
async resetUserAudiobookProgress(req, res) {
req.user.resetAudiobookProgress(req.params.id)
await this.db.updateEntity('user', req.user)
- this.emitter('user_updated', req.user.toJSONForBrowser())
+ this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
+ res.sendStatus(200)
+ }
+
+ async updateUserAudiobookProgress(req, res) {
+ var wasUpdated = req.user.updateAudiobookProgress(req.params.id, req.body)
+ if (wasUpdated) {
+ await this.db.updateEntity('user', req.user)
+ this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
+ }
res.sendStatus(200)
}
diff --git a/server/StreamManager.js b/server/StreamManager.js
index 9d1f7c41..5b8a32d9 100644
--- a/server/StreamManager.js
+++ b/server/StreamManager.js
@@ -134,11 +134,11 @@ class StreamManager {
Logger.error('No User for client', client)
return
}
- if (!client.user.updateAudiobookProgress) {
+ if (!client.user.updateAudiobookProgressFromStream) {
Logger.error('Invalid User for client', client)
return
}
- client.user.updateAudiobookProgress(client.stream)
+ client.user.updateAudiobookProgressFromStream(client.stream)
this.db.updateEntity('user', client.user)
}
}
diff --git a/server/objects/AudiobookProgress.js b/server/objects/AudiobookProgress.js
new file mode 100644
index 00000000..283552c4
--- /dev/null
+++ b/server/objects/AudiobookProgress.js
@@ -0,0 +1,91 @@
+class AudiobookProgress {
+ constructor(progress) {
+ this.audiobookId = null
+ this.totalDuration = null // seconds
+ this.progress = null // 0 to 1
+ this.currentTime = null // seconds
+ this.isRead = false
+ this.lastUpdate = null
+ this.startedAt = null
+ this.finishedAt = null
+
+ if (progress) {
+ this.construct(progress)
+ }
+ }
+
+ toJSON() {
+ return {
+ audiobookId: this.audiobookId,
+ totalDuration: this.totalDuration,
+ progress: this.progress,
+ currentTime: this.currentTime,
+ isRead: this.isRead,
+ lastUpdate: this.lastUpdate,
+ startedAt: this.startedAt,
+ finishedAt: this.finishedAt
+ }
+ }
+
+ construct(progress) {
+ this.audiobookId = progress.audiobookId
+ this.totalDuration = progress.totalDuration
+ this.progress = progress.progress
+ this.currentTime = progress.currentTime
+ this.isRead = !!progress.isRead
+ this.lastUpdate = progress.lastUpdate
+ this.startedAt = progress.startedAt
+ this.finishedAt = progress.finishedAt || null
+ }
+
+ updateFromStream(stream) {
+ this.audiobookId = stream.audiobookId
+ this.totalDuration = stream.totalDuration
+ this.progress = stream.clientProgress
+ this.currentTime = stream.clientCurrentTime
+ this.lastUpdate = Date.now()
+
+ if (!this.startedAt) {
+ this.startedAt = Date.now()
+ }
+
+ // 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()
+ }
+ } else {
+ this.isRead = false
+ this.finishedAt = null
+ }
+ }
+
+ update(payload) {
+ var hasUpdates = false
+ for (const key in payload) {
+ if (payload[key] !== this[key]) {
+ if (key === 'isRead') {
+ if (!payload[key]) { // Updating to Not Read - Reset progress and current time
+ this.finishedAt = null
+ this.progress = 0
+ this.currentTime = 0
+ } else { // Updating to Read
+ if (!this.finishedAt) this.finishedAt = Date.now()
+ this.progress = 1
+ }
+ }
+
+ this[key] = payload[key]
+ hasUpdates = true
+ }
+ }
+ if (!this.startedAt) {
+ this.startedAt = Date.now()
+ }
+ return hasUpdates
+ }
+}
+module.exports = AudiobookProgress
\ No newline at end of file
diff --git a/server/objects/User.js b/server/objects/User.js
index c9d15eb6..8e2c6253 100644
--- a/server/objects/User.js
+++ b/server/objects/User.js
@@ -1,3 +1,5 @@
+const AudiobookProgress = require('./AudiobookProgress')
+
class User {
constructor(user) {
this.id = null
@@ -26,6 +28,17 @@ class User {
}
}
+ audiobooksToJSON() {
+ if (!this.audiobooks) return null
+ var _map = {}
+ for (const key in this.audiobooks) {
+ if (this.audiobooks[key]) {
+ _map[key] = this.audiobooks[key].toJSON()
+ }
+ }
+ return _map
+ }
+
toJSON() {
return {
id: this.id,
@@ -34,7 +47,7 @@ class User {
type: this.type,
stream: this.stream,
token: this.token,
- audiobooks: this.audiobooks,
+ audiobooks: this.audiobooksToJSON(),
isActive: this.isActive,
createdAt: this.createdAt,
settings: this.settings
@@ -48,7 +61,7 @@ class User {
type: this.type,
stream: this.stream,
token: this.token,
- audiobooks: this.audiobooks,
+ audiobooks: this.audiobooksToJSON(),
isActive: this.isActive,
createdAt: this.createdAt,
settings: this.settings
@@ -62,7 +75,14 @@ class User {
this.type = user.type
this.stream = user.stream || null
this.token = user.token
- this.audiobooks = user.audiobooks || null
+ if (user.audiobooks) {
+ this.audiobooks = {}
+ for (const key in user.audiobooks) {
+ if (user.audiobooks[key]) {
+ this.audiobooks[key] = new AudiobookProgress(user.audiobooks[key])
+ }
+ }
+ }
this.isActive = (user.isActive === undefined || user.id === 'root') ? true : !!user.isActive
this.createdAt = user.createdAt || Date.now()
this.settings = user.settings || this.getDefaultUserSettings()
@@ -84,18 +104,21 @@ class User {
return hasUpdates
}
- updateAudiobookProgress(stream) {
+ updateAudiobookProgressFromStream(stream) {
if (!this.audiobooks) this.audiobooks = {}
if (!this.audiobooks[stream.audiobookId]) {
- this.audiobooks[stream.audiobookId] = {
- audiobookId: stream.audiobookId,
- totalDuration: stream.totalDuration,
- startedAt: Date.now()
- }
+ this.audiobooks[stream.audiobookId] = new AudiobookProgress()
}
- this.audiobooks[stream.audiobookId].lastUpdate = Date.now()
- this.audiobooks[stream.audiobookId].progress = stream.clientProgress
- this.audiobooks[stream.audiobookId].currentTime = stream.clientCurrentTime
+ this.audiobooks[stream.audiobookId].updateFromStream(stream)
+ }
+
+ updateAudiobookProgress(audiobookId, updatePayload) {
+ if (!this.audiobooks) this.audiobooks = {}
+ if (!this.audiobooks[audiobookId]) {
+ this.audiobooks[audiobookId] = new AudiobookProgress()
+ this.audiobooks[audiobookId].audiobookId = audiobookId
+ }
+ return this.audiobooks[audiobookId].update(updatePayload)
}
// Returns Boolean If update was made