Add: Filter and side rail button to show books with issues #132, Add: realtime updates for audiobook progress

This commit is contained in:
advplyr 2021-10-24 15:53:51 -05:00
parent 874c910e24
commit 335bbac81d
14 changed files with 96 additions and 30 deletions

View File

@ -232,6 +232,7 @@ export default {
},
restart() {
this.seek(0)
this.$nextTick(this.sendStreamUpdate)
},
backward10() {
var newTime = this.audioEl.currentTime - 10
@ -372,7 +373,13 @@ export default {
}
this.updateTimestamp()
this.sendStreamUpdate()
// 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

View File

@ -89,6 +89,8 @@ export default {
'$route.query.filter'() {
if (this.$route.query.filter && this.$route.query.filter !== this.filterBy) {
this.$store.dispatch('user/updateUserSettings', { filterBy: this.$route.query.filter })
} else if (!this.$route.query.filter && this.filterBy) {
this.$store.dispatch('user/updateUserSettings', { filterBy: 'all' })
}
}
},

View File

@ -11,14 +11,14 @@
<div v-show="homePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === '' && !homePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="showLibrary ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
<p class="font-book pt-1.5" style="font-size: 1rem">Library</p>
<div v-show="paramId === '' && !homePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
<div v-show="showLibrary" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf/series`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'series' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
@ -31,6 +31,16 @@
<div v-show="paramId === 'series'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : ' bg-error bg-opacity-20'">
<span class="material-icons text-2xl">warning</span>
<p class="font-book pt-1.5" style="font-size: 1rem">Issues</p>
<div v-show="showingIssues" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
<div class="absolute top-1 right-1 w-4 h-4 rounded-full bg-white bg-opacity-30 flex items-center justify-center">
<p class="text-xs font-mono pb-0.5">{{ numIssues }}</p>
</div>
</nuxt-link>
<!-- <nuxt-link to="/library/collections" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
@ -80,6 +90,19 @@ export default {
},
homePage() {
return this.$route.name === 'library-library'
},
libraryBookshelfPage() {
return this.$route.name === 'library-library-bookshelf-id'
},
showLibrary() {
return this.libraryBookshelfPage && this.paramId === '' && !this.showingIssues
},
showingIssues() {
if (!this.$route.query) return false
return this.libraryBookshelfPage && this.$route.query.filter === 'issues'
},
numIssues() {
return this.$store.getters['audiobooks/getAudiobooksWithIssues'].length
}
},
methods: {},

View File

@ -148,10 +148,7 @@ export default {
currentTime,
streamId: this.streamId
}
console.log('Stream update', updatePayload.currentTime)
this.$root.socket.emit('stream_update', updatePayload)
} else {
console.log('Do not update time', diff)
}
},
streamReset({ startTime, streamId }) {

View File

@ -101,6 +101,11 @@ export default {
text: 'Progress',
value: 'progress',
sublist: true
},
{
text: 'Issues',
value: 'issues',
sublist: false
}
]
}

View File

@ -177,6 +177,9 @@ export default {
userStreamUpdate(user) {
this.$store.commit('users/updateUser', user)
},
currentUserAudiobookUpdate(payload) {
this.$store.commit('user/updateUserAudiobook', payload)
},
downloadToastClick(download) {
if (!download || !download.audiobookId) {
return console.error('Invalid download object', download)
@ -285,6 +288,7 @@ export default {
this.socket.on('user_online', this.userOnline)
this.socket.on('user_offline', this.userOffline)
this.socket.on('user_stream_update', this.userStreamUpdate)
this.socket.on('current_user_audiobook_update', this.currentUserAudiobookUpdate)
// Scan Listeners
this.socket.on('scan_start', this.scanStart)

View File

@ -397,16 +397,6 @@ export default {
this.$store.commit('setBookshelfBookIds', [])
this.$store.commit('showEditModal', this.audiobook)
},
lookupMetadata(index) {
this.$axios
.$get(`/api/metadata/${this.audiobookId}/${index}`)
.then((metadata) => {
console.log('Metadata for ' + index, metadata)
})
.catch((error) => {
console.error(error)
})
},
audiobookUpdated() {
console.log('Audiobook Updated - Fetch full audiobook')
this.$axios

View File

@ -22,6 +22,11 @@ export const getters = {
getAudiobook: (state) => id => {
return state.audiobooks.find(ab => ab.id === id)
},
getAudiobooksWithIssues: (state) => {
return state.audiobooks.filter(ab => {
return ab.hasMissingParts || ab.hasInvalidParts || ab.isMissing || ab.isIncomplete
})
},
getEntitiesShowing: (state, getters, rootState, rootGetters) => () => {
if (!state.libraryPage) {
return getters.getFiltered()
@ -66,7 +71,12 @@ export const getters = {
return false
})
}
} else if (filterBy === 'issues') {
filtered = filtered.filter(ab => {
return ab.hasMissingParts || ab.hasInvalidParts || ab.isMissing || ab.isIncomplete
})
}
if (state.keywordFilter) {
const keywordFilterKeys = ['title', 'subtitle', 'author', 'series', 'narrator']
const keyworkFilter = state.keywordFilter.toLowerCase()

View File

@ -1,4 +1,6 @@
import Vue from 'vue'
export const state = () => ({
user: null,
settings: {
@ -80,6 +82,13 @@ export const mutations = {
localStorage.removeItem('token')
}
},
updateUserAudiobook(state, { id, data }) {
if (!state.user) return
if (!state.user.audiobooks) {
Vue.set(state.user, 'audiobooks', {})
}
Vue.set(state.user.audiobooks, id, data)
},
setSettings(state, settings) {
if (!settings) return

View File

@ -86,7 +86,6 @@ class Server {
clientEmitter(userId, ev, data) {
var clients = this.getClientsForUser(userId)
if (!clients.length) {
console.log('clients', clients)
return Logger.error(`[Server] clientEmitter - no clients found for user ${userId}`)
}
clients.forEach((client) => {
@ -247,7 +246,7 @@ class Server {
socket.on('close_stream', () => this.streamManager.closeStreamRequest(socket))
socket.on('stream_update', (payload) => this.streamManager.streamUpdate(socket, payload))
socket.on('progress_update', (payload) => this.audiobookProgressUpdate(socket.sheepClient, payload))
socket.on('progress_update', (payload) => this.audiobookProgressUpdate(socket, payload))
// Downloading
socket.on('download', (payload) => this.downloadManager.downloadSocketRequest(socket, payload))
@ -451,12 +450,20 @@ class Server {
res.sendStatus(200)
}
audiobookProgressUpdate(client, progressPayload) {
audiobookProgressUpdate(socket, progressPayload) {
var client = socket.sheepClient
if (!client || !client.user) {
Logger.error('[Server] audiobookProgressUpdate invalid socket client')
return
}
client.user.updateAudiobookProgress(progressPayload.audiobookId, progressPayload)
var hasUpdates = client.user.updateAudiobookProgress(progressPayload.audiobookId, progressPayload)
if (hasUpdates) {
var userAudiobook = client.user.getAudiobookJSON(progressPayload.audiobookId)
socket.emit('current_user_audiobook_update', {
id: progressPayload.audiobookId,
data: userAudiobook || null
})
}
}
async authenticateSocket(socket, token) {

View File

@ -153,8 +153,15 @@ class StreamManager {
Logger.error('Invalid User for client', client)
return
}
client.user.updateAudiobookProgressFromStream(client.stream)
var userAudiobook = client.user.updateAudiobookProgressFromStream(client.stream)
this.db.updateEntity('user', client.user)
if (userAudiobook) {
socket.emit('current_user_audiobook_update', {
id: userAudiobook.audiobookId,
data: userAudiobook.toJSON()
})
}
}
}
module.exports = StreamManager

View File

@ -193,16 +193,14 @@ class Audiobook {
lastUpdate: this.lastUpdate,
duration: this.totalDuration,
size: this.totalSize,
hasBookMatch: !!this.book,
hasMissingParts: this.missingParts ? this.missingParts.length : 0,
hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0,
// numEbooks: this.ebooks.length,
ebooks: this.ebooks.map(ebook => ebook.toJSON()),
numEbooks: this.ebooks.length,
numTracks: this.tracks.length,
chapters: this.chapters || [],
isMissing: !!this.isMissing,
isInvalid: !!this.isInvalid
isInvalid: !!this.isInvalid,
hasMissingParts: this.missingParts ? this.missingParts.length : 0,
hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0
}
}
@ -232,7 +230,9 @@ class Audiobook {
tracks: this.tracksToJSON(),
chapters: this.chapters || [],
isMissing: !!this.isMissing,
isInvalid: !!this.isInvalid
isInvalid: !!this.isInvalid,
hasMissingParts: this.missingParts ? this.missingParts.length : 0,
hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0
}
}

View File

@ -88,7 +88,7 @@ class Stream extends EventEmitter {
get clientProgress() {
if (!this.clientCurrentTime) return 0
var prog = Math.max(1, this.clientCurrentTime / this.totalDuration)
var prog = Math.min(1, this.clientCurrentTime / this.totalDuration)
return Number(prog.toFixed(3))
}
@ -248,6 +248,7 @@ class Stream extends EventEmitter {
this.ffmpeg.inputOption('-seek_timestamp 1')
this.ffmpeg.inputFormat('concat')
this.ffmpeg.inputOption('-safe 0')
// this.ffmpeg.inputOption('-segment_time_metadata 1')
if (this.startTime > 0) {
const shiftedStartTime = this.startTime - trackStartTime
@ -259,7 +260,7 @@ class Stream extends EventEmitter {
this.ffmpeg.inputOption('-noaccurate_seek')
}
const logLevel = process.env.NODE_ENV === 'production' ? 'error' : 'warning'
const logLevel = process.env.NODE_ENV === 'production' ? 'error' : 'error'
const audioCodec = (this.hlsSegmentType === 'fmp4' || this.tracksAudioFileType === 'opus') ? 'aac' : 'copy'
this.ffmpeg.addOption([
`-loglevel ${logLevel}`,
@ -270,7 +271,6 @@ class Stream extends EventEmitter {
'-f hls',
"-copyts",
"-avoid_negative_ts make_non_negative",
// '-start_at_zero',
"-max_delay 5000000",
"-max_muxing_queue_size 2048",
`-hls_time 6`,

View File

@ -203,6 +203,7 @@ class User {
this.audiobooks[stream.audiobookId] = new AudiobookProgress()
}
this.audiobooks[stream.audiobookId].updateFromStream(stream)
return this.audiobooks[stream.audiobookId]
}
updateAudiobookProgress(audiobook, updatePayload) {
@ -267,5 +268,9 @@ class User {
if (!this.librariesAccessible) return false
return this.librariesAccessible.includes(libraryId)
}
getAudiobookJSON(audiobookId) {
return this.audiobooks[audiobookId] ? this.audiobooks[audiobookId].toJSON() : null
}
}
module.exports = User