Local media sync events

This commit is contained in:
advplyr 2023-01-15 14:58:26 -06:00
parent 297eca6a86
commit 8f6dd72df2
15 changed files with 234 additions and 80 deletions

View file

@ -370,26 +370,14 @@ data class BookChapter(
val endMs get() = (end * 1000L).toLong()
}
@JsonIgnoreProperties(ignoreUnknown = true)
class MediaProgress(
var id:String,
var libraryItemId:String,
var episodeId:String?,
var duration:Double, // seconds
progress:Double, // 0 to 1
var currentTime:Double,
isFinished:Boolean,
var lastUpdate:Long,
var startedAt:Long,
var finishedAt:Long?
) : MediaProgressWrapper(isFinished, progress)
@JsonTypeInfo(use= JsonTypeInfo.Id.DEDUCTION, defaultImpl = MediaProgress::class)
@JsonSubTypes(
JsonSubTypes.Type(MediaProgress::class),
JsonSubTypes.Type(LocalMediaProgress::class)
)
open class MediaProgressWrapper(var isFinished:Boolean, var progress:Double)
open class MediaProgressWrapper(var isFinished:Boolean, var currentTime:Double, var progress:Double) {
open val mediaItemId get() = ""
}
// Helper class
data class LibraryItemWithEpisode(

View file

@ -1,5 +1,6 @@
package com.audiobookshelf.app.data
import android.util.Log
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import kotlin.math.roundToInt
@ -11,7 +12,7 @@ class LocalMediaProgress(
var localEpisodeId:String?,
var duration:Double,
progress:Double, // 0 to 1
var currentTime:Double,
currentTime:Double,
isFinished:Boolean,
var lastUpdate:Long,
var startedAt:Long,
@ -22,9 +23,16 @@ class LocalMediaProgress(
var serverUserId:String?,
var libraryItemId:String?,
var episodeId:String?
) : MediaProgressWrapper(isFinished, progress) {
) : MediaProgressWrapper(isFinished, currentTime, progress) {
@get:JsonIgnore
val progressPercent get() = if (progress.isNaN()) 0 else (progress * 100).roundToInt()
@get:JsonIgnore
override val mediaItemId get() = if (libraryItemId != null) {
if (episodeId.isNullOrEmpty()) libraryItemId ?: "" else "$libraryItemId-$episodeId"
} else {
if (localEpisodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
}
@JsonIgnore
fun updateIsFinished(finished:Boolean) {
@ -41,8 +49,7 @@ class LocalMediaProgress(
fun updateFromPlaybackSession(playbackSession:PlaybackSession) {
currentTime = playbackSession.currentTime
progress = playbackSession.progress
lastUpdate = System.currentTimeMillis()
lastUpdate = playbackSession.updatedAt
isFinished = playbackSession.progress >= 0.99
finishedAt = if (isFinished) lastUpdate else null
}

View file

@ -0,0 +1,22 @@
package com.audiobookshelf.app.data
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
class MediaProgress(
var id:String,
var libraryItemId:String,
var episodeId:String?,
var duration:Double, // seconds
progress:Double, // 0 to 1
currentTime:Double,
isFinished:Boolean,
var lastUpdate:Long,
var startedAt:Long,
var finishedAt:Long?
) : MediaProgressWrapper(isFinished, currentTime, progress) {
@get:JsonIgnore
override val mediaItemId get() = if (episodeId.isNullOrEmpty()) libraryItemId else "$libraryItemId-$episodeId"
}

View file

@ -1,9 +1,7 @@
package com.audiobookshelf.app.media
import android.util.Log
import com.audiobookshelf.app.data.MediaItemEvent
import com.audiobookshelf.app.data.MediaItemHistory
import com.audiobookshelf.app.data.PlaybackSession
import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.player.PlayerNotificationService
import com.audiobookshelf.app.player.SyncResult
@ -43,8 +41,36 @@ object MediaEventManager {
addPlaybackEvent("Seek", playbackSession, syncResult)
}
fun syncEvent(mediaProgress: MediaProgressWrapper, description: String) {
Log.i(tag, "Sync Event for media item id \"${mediaProgress.mediaItemId}\", currentTime=${mediaProgress.currentTime}")
addSyncEvent("Sync", mediaProgress, description)
}
private fun addSyncEvent(eventName:String, mediaProgress:MediaProgressWrapper, description: String) {
val mediaItemHistory = getMediaItemHistoryMediaItem(mediaProgress.mediaItemId)
if (mediaItemHistory == null) {
Log.w(tag, "addSyncEvent: Media Item History not created yet for media item id ${mediaProgress.mediaItemId}")
return
}
val mediaItemEvent = MediaItemEvent(
name = eventName,
type = "Sync",
description = description,
currentTime = mediaProgress.currentTime,
serverSyncAttempted = false,
serverSyncSuccess = null,
serverSyncMessage = null,
timestamp = System.currentTimeMillis()
)
mediaItemHistory.events.add(mediaItemEvent)
DeviceManager.dbManager.saveMediaItemHistory(mediaItemHistory)
clientEventEmitter?.onMediaItemHistoryUpdated(mediaItemHistory)
}
private fun addPlaybackEvent(eventName:String, playbackSession:PlaybackSession, syncResult:SyncResult?) {
val mediaItemHistory = getMediaItemHistoryForSession(playbackSession) ?: createMediaItemHistoryForSession(playbackSession)
val mediaItemHistory = getMediaItemHistoryMediaItem(playbackSession.mediaItemId) ?: createMediaItemHistoryForSession(playbackSession)
val mediaItemEvent = MediaItemEvent(
name = eventName,
@ -62,8 +88,8 @@ object MediaEventManager {
clientEventEmitter?.onMediaItemHistoryUpdated(mediaItemHistory)
}
private fun getMediaItemHistoryForSession(playbackSession: PlaybackSession) : MediaItemHistory? {
return DeviceManager.dbManager.getMediaItemHistory(playbackSession.mediaItemId)
private fun getMediaItemHistoryMediaItem(mediaItemId: String) : MediaItemHistory? {
return DeviceManager.dbManager.getMediaItemHistory(mediaItemId)
}
private fun createMediaItemHistoryForSession(playbackSession: PlaybackSession):MediaItemHistory {

View file

@ -6,6 +6,7 @@ import android.util.Log
import com.audiobookshelf.app.data.LocalMediaProgress
import com.audiobookshelf.app.data.MediaProgress
import com.audiobookshelf.app.data.PlaybackSession
import com.audiobookshelf.app.data.Podcast
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.media.MediaEventManager
import com.audiobookshelf.app.server.ApiHandler
@ -190,6 +191,9 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
currentPlaybackSession?.let {
it.updatedAt = mediaProgress.lastUpdate
it.currentTime = mediaProgress.currentTime
MediaEventManager.syncEvent(mediaProgress, "Received from server get media progress request while playback session open")
DeviceManager.dbManager.saveLocalPlaybackSession(it)
saveLocalProgress(it)
}
@ -279,17 +283,18 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
currentLocalMediaProgress = playbackSession.getNewLocalMediaProgress()
} else {
currentLocalMediaProgress = mediaProgress
currentLocalMediaProgress?.updateFromPlaybackSession(playbackSession)
}
} else {
currentLocalMediaProgress?.updateFromPlaybackSession(playbackSession)
}
currentLocalMediaProgress?.let {
if (it.progress.isNaN()) {
Log.e(tag, "Invalid progress on local media progress")
} else {
DeviceManager.dbManager.saveLocalMediaProgress(it)
playerNotificationService.clientEventEmitter?.onLocalMediaProgressUpdate(it)
Log.d(tag, "Saved Local Progress Current Time: ID ${it.id} | ${it.currentTime} | Duration ${it.duration} | Progress ${it.progressPercent}%")
}
}

View file

@ -4,7 +4,6 @@ import android.util.Log
import com.audiobookshelf.app.data.PlaybackSession
import com.audiobookshelf.app.data.PlayerState
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.media.MediaEventManager
import com.google.android.exoplayer2.PlaybackException
import com.google.android.exoplayer2.Player
@ -17,8 +16,7 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
var lastPauseTime: Long = 0 //ms
}
private var onSeekBack: Boolean = false
private var isSeeking: Boolean = false
private var lazyIsPlaying: Boolean = false
override fun onPlayerError(error: PlaybackException) {
val errorMessage = error.message ?: "Unknown Error"
@ -33,38 +31,37 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
) {
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
// If playing set seeking flag
isSeeking = playerNotificationService.currentPlayer.isPlaying
Log.d(tag, "onPositionDiscontinuity: oldPosition=${oldPosition.positionMs}/${oldPosition.mediaItemIndex}, newPosition=${newPosition.positionMs}/${newPosition.mediaItemIndex}, isPlaying=${playerNotificationService.currentPlayer.isPlaying} reason=SEEK")
playerNotificationService.mediaProgressSyncer.seek()
lastPauseTime = 0 // When seeking while paused reset the auto-rewind
} else {
Log.d(tag, "onPositionDiscontinuity: oldPosition=${oldPosition.positionMs}/${oldPosition.mediaItemIndex}, newPosition=${newPosition.positionMs}/${newPosition.mediaItemIndex}, isPlaying=${playerNotificationService.currentPlayer.isPlaying}, reason=$reason")
}
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
Log.d(tag, "onIsPlayingChanged to $isPlaying | ${playerNotificationService.getMediaPlayer()}")
if (isSeeking) {
if (isPlaying) {
isSeeking = false
Log.d(tag, "onIsPlayingChanged isSeeking seek complete")
} else {
Log.d(tag, "onIsPlayingChanged isSeeking skipping pause")
}
return
}
Log.d(tag, "onIsPlayingChanged to $isPlaying | ${playerNotificationService.getMediaPlayer()} | playbackState=${playerNotificationService.currentPlayer.playbackState}")
val player = playerNotificationService.currentPlayer
if (player.isPlaying) {
// Goal of these 2 if statements and the lazyIsPlaying is to ignore this event when it is triggered by a seek
// When a seek occurs the player is paused and buffering, then plays again right afterwards.
if (!isPlaying && player.playbackState == Player.STATE_BUFFERING) {
Log.d(tag, "onIsPlayingChanged: Pause event when buffering is ignored")
return
}
if (lazyIsPlaying == isPlaying) {
Log.d(tag, "onIsPlayingChanged: Lazy is playing $lazyIsPlaying is already set to this so ignoring")
return
}
lazyIsPlaying = isPlaying
if (isPlaying) {
Log.d(tag, "SeekBackTime: Player is playing")
if (lastPauseTime > 0 && DeviceManager.deviceData.deviceSettings?.disableAutoRewind != true) {
var seekBackTime = 0L
if (onSeekBack) onSeekBack = false
else {
Log.d(tag, "SeekBackTime: playing started now set seek back time $lastPauseTime")
seekBackTime = calcPauseSeekBackTime()
var seekBackTime = calcPauseSeekBackTime()
if (seekBackTime > 0) {
// Current chapter is used so that seek back does not go back to the previous chapter
val currentChapter = playerNotificationService.getCurrentBookChapter()
@ -76,8 +73,6 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
seekBackTime = currentTime - minSeekBackTime
}
Log.d(tag, "SeekBackTime $seekBackTime")
onSeekBack = true
}
}
// Check if playback session still exists or sync media progress if updated
@ -92,12 +87,12 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
}
}
} else {
Log.d(tag, "SeekBackTime: Player not playing set last pause time")
Log.d(tag, "SeekBackTime: Player not playing set last pause time | playbackState=${player.playbackState}")
lastPauseTime = System.currentTimeMillis()
}
// Start/stop progress sync interval
if (player.isPlaying) {
if (isPlaying) {
player.volume = 1F // Volume on sleep timer might have decreased this
val playbackSession: PlaybackSession? = playerNotificationService.mediaProgressSyncer.currentPlaybackSession ?: playerNotificationService.currentPlaybackSession
playbackSession?.let { playerNotificationService.mediaProgressSyncer.play(it) }
@ -107,7 +102,7 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
}
}
playerNotificationService.clientEventEmitter?.onPlayingUpdate(player.isPlaying)
playerNotificationService.clientEventEmitter?.onPlayingUpdate(isPlaying)
DeviceManager.widgetUpdater?.onPlayerChanged(playerNotificationService)
}

View file

@ -823,6 +823,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
}
currentPlaybackSession = null
mediaProgressSyncer.reset()
clientEventEmitter?.onPlaybackClosed()
PlayerListener.lastPauseTime = 0
isClosed = true

View file

@ -3,6 +3,7 @@ package com.audiobookshelf.app.data
import android.util.Log
import com.audiobookshelf.app.MainActivity
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.media.MediaEventManager
import com.audiobookshelf.app.server.ApiHandler
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
@ -188,6 +189,24 @@ class AbsDatabase : Plugin() {
}
}
@PluginMethod
fun getLocalMediaProgressForServerItem(call:PluginCall) {
val libraryItemId = call.getString("libraryItemId", "").toString()
var episodeId:String? = call.getString("episodeId", "").toString()
if (episodeId == "") episodeId = null
GlobalScope.launch(Dispatchers.IO) {
val allLocalMediaProgress = DeviceManager.dbManager.getAllLocalMediaProgress()
val localMediaProgress = allLocalMediaProgress.find { libraryItemId == it.libraryItemId && (episodeId == null || it.episodeId == episodeId) }
if (localMediaProgress == null) {
call.resolve()
} else {
call.resolve(JSObject(jacksonMapper.writeValueAsString(localMediaProgress)))
}
}
}
@PluginMethod
fun removeLocalMediaProgress(call:PluginCall) {
val localMediaProgressId = call.getString("localMediaProgressId", "").toString()
@ -256,6 +275,8 @@ class AbsDatabase : Plugin() {
Log.w(tag, "syncServerMediaProgressWithLocalMediaProgress Local media progress not found $localMediaProgressId")
call.resolve()
} else {
MediaEventManager.syncEvent(mediaProgress, "Received from webhook event")
localMediaProgress.updateFromServerMediaProgress(mediaProgress)
DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
call.resolve(JSObject(jacksonMapper.writeValueAsString(localMediaProgress)))

View file

@ -6,6 +6,7 @@ import android.net.NetworkCapabilities
import android.util.Log
import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.media.MediaEventManager
import com.audiobookshelf.app.player.MediaProgressSyncData
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.core.json.JsonReadFeature
@ -283,6 +284,8 @@ class ApiHandler(var ctx:Context) {
if (progressSyncResponsePayload.localProgressUpdates.isNotEmpty()) {
// Update all local media progress
progressSyncResponsePayload.localProgressUpdates.forEach { localMediaProgress ->
MediaEventManager.syncEvent(localMediaProgress, "Received from server sync local API request")
DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
}
}

View file

@ -184,6 +184,7 @@ export default {
async playLibraryItem(payload) {
const libraryItemId = payload.libraryItemId
const episodeId = payload.episodeId
const startTime = payload.startTime
// When playing local library item and can also play this item from the server
// then store the server library item id so it can be used if a cast is made
@ -200,6 +201,16 @@ export default {
}
}
// if already playing this item then jump to start time
if (this.$store.getters['getIsMediaStreaming'](libraryItemId, episodeId)) {
console.log('Already streaming item', startTime)
if (startTime !== undefined && startTime !== null) {
// seek to start time
AbsAudioPlayer.seek({ value: Math.floor(startTime) })
}
return
}
this.serverLibraryItemId = null
this.serverEpisodeId = null
@ -209,7 +220,9 @@ export default {
}
console.log('Called playLibraryItem', libraryItemId)
AbsAudioPlayer.prepareLibraryItem({ libraryItemId, episodeId, playWhenReady: true, playbackRate })
const preparePayload = { libraryItemId, episodeId, playWhenReady: true, playbackRate }
if (startTime !== undefined && startTime !== null) preparePayload.startTime = startTime
AbsAudioPlayer.prepareLibraryItem(preparePayload)
.then((data) => {
if (data.error) {
const errorMsg = data.error || 'Failed to play'

View file

@ -208,11 +208,19 @@ export default {
console.log(`[default] userMediaProgressUpdate checking for local media progress ${prog.id}`)
// Update local media progress if exists
var localProg = this.$store.getters['globals/getLocalMediaProgressByServerItemId'](prog.libraryItemId, prog.episodeId)
const localProg = await this.$db.getLocalMediaProgressForServerItem({ libraryItemId: prog.libraryItemId, episodeId: prog.episodeId })
var newLocalMediaProgress = null
if (localProg && localProg.lastUpdate < prog.lastUpdate) {
if (localProg.currentTime == prog.currentTime && localProg.isFinished == prog.isFinished) {
console.log('[default] syncing progress server lastUpdate > local lastUpdate but currentTime and isFinished is equal')
return
} else {
console.log(`[default] syncing progress server lastUpdate > local lastUpdate. server currentTime=${prog.currentTime} local currentTime=${localProg.currentTime} | server/local isFinished=${prog.isFinished}/${localProg.isFinished}`)
}
// Server progress is more up-to-date
console.log(`[default] syncing progress from server with local item for "${prog.libraryItemId}" ${prog.episodeId ? `episode ${prog.episodeId}` : ''}`)
console.log(`[default] syncing progress from server with local item for "${prog.libraryItemId}" ${prog.episodeId ? `episode ${prog.episodeId}` : ''} | server lastUpdate=${prog.lastUpdate} > local lastUpdate=${localProg.lastUpdate}`)
const payload = {
localMediaProgressId: localProg.id,
mediaProgress: prog

View file

@ -10,16 +10,16 @@
<p class="my-2 text-gray-400 font-semibold">{{ name }}</p>
<div v-for="(evt, index) in events" :key="index" class="py-3 flex items-center">
<p class="text-sm text-gray-400 w-12">{{ $formatDate(evt.timestamp, 'HH:mm') }}</p>
<span class="material-icons px-2" :class="`text-${getEventColor(evt.name)}`">{{ getEventIcon(evt.name) }}</span>
<p class="text-sm text-white">{{ evt.name }}</p>
<span class="material-icons px-1" :class="`text-${getEventColor(evt.name)}`">{{ getEventIcon(evt.name) }}</span>
<p class="text-sm text-white px-1">{{ evt.name }}</p>
<span v-if="evt.serverSyncAttempted && evt.serverSyncSuccess" class="material-icons-outlined px-2 text-base text-success">cloud_done</span>
<span v-if="evt.serverSyncAttempted && !evt.serverSyncSuccess" class="material-icons px-2 text-base text-error">error_outline</span>
<span v-if="evt.serverSyncAttempted && evt.serverSyncSuccess" class="material-icons-outlined px-1 text-base text-success">cloud_done</span>
<span v-if="evt.serverSyncAttempted && !evt.serverSyncSuccess" class="material-icons px-1 text-base text-error">error_outline</span>
<p v-if="evt.num" class="text-sm text-gray-400 italic">+{{ evt.num }}</p>
<p v-if="evt.num" class="text-sm text-gray-400 italic px-1">+{{ evt.num }}</p>
<div class="flex-grow" />
<p class="text-base text-white">{{ $secondsToTimestampFull(evt.currentTime) }}</p>
<p class="text-base text-white" @click="clickPlaybackTime(evt.currentTime)">{{ $secondsToTimestampFull(evt.currentTime) }}</p>
</div>
</div>
</div>
@ -43,7 +43,8 @@ export default {
},
data() {
return {
onMediaItemHistoryUpdatedListener: null
onMediaItemHistoryUpdatedListener: null,
startingPlayback: false
}
},
computed: {
@ -55,6 +56,17 @@ export default {
if (!this.mediaItemHistory) return []
return (this.mediaItemHistory.events || []).sort((a, b) => b.timestamp - a.timestamp)
},
mediaItemIsLocal() {
return this.mediaItemHistory && this.mediaItemHistory.isLocal
},
mediaItemLibraryItemId() {
if (!this.mediaItemHistory) return null
return this.mediaItemHistory.libraryItemId
},
mediaItemEpisodeId() {
if (!this.mediaItemHistory) return null
return this.mediaItemHistory.episodeId
},
groupedMediaEvents() {
const groups = {}
@ -63,11 +75,12 @@ export default {
let lastKey = null
let numSaves = 0
let index = 0
let numSyncs = 0
this.mediaEvents.forEach((evt) => {
const date = this.$formatDate(evt.timestamp, 'MMM dd, yyyy')
let include = true
let keyUpdated = false
let key = date
if (date === today) key = 'Today'
@ -75,25 +88,41 @@ export default {
if (!groups[key]) groups[key] = []
if (!lastKey) lastKey = key
if (!lastKey || lastKey !== key) {
lastKey = key
keyUpdated = true
}
// Collapse saves
if (evt.name === 'Save') {
if (numSaves > 0 && lastKey === key) {
if (numSaves > 0 && !keyUpdated) {
include = false
groups[key][index - 1].num = numSaves
const totalInGroup = groups[key].length
groups[key][totalInGroup - 1].num = numSaves
numSaves++
} else {
lastKey = key
numSaves = 1
}
} else {
numSaves = 0
}
// Collapse syncs
if (evt.name === 'Sync') {
if (numSyncs > 0 && !keyUpdated) {
include = false
const totalInGroup = groups[key].length
groups[key][totalInGroup - 1].num = numSyncs
numSyncs++
} else {
numSyncs = 1
}
} else {
numSyncs = 0
}
if (include) {
groups[key].push(evt)
index++
}
})
@ -101,6 +130,33 @@ export default {
}
},
methods: {
async clickPlaybackTime(time) {
if (this.startingPlayback) return
this.startingPlayback = true
await this.$hapticsImpact()
console.log('Click playback time', time)
this.playAtTime(time)
setTimeout(() => {
this.startingPlayback = false
}, 1000)
},
playAtTime(startTime) {
if (this.mediaItemIsLocal) {
// Local only
this.$eventBus.$emit('play-item', { libraryItemId: this.mediaItemLibraryItemId, episodeId: this.mediaItemEpisodeId, startTime })
} else {
// Server may have local
const localProg = this.$store.getters['globals/getLocalMediaProgressByServerItemId'](this.mediaItemLibraryItemId, this.mediaItemEpisodeId)
if (localProg) {
// Has local copy so prefer
this.$eventBus.$emit('play-item', { libraryItemId: localProg.localLibraryItemId, episodeId: localProg.localEpisodeId, serverLibraryItemId: this.mediaItemLibraryItemId, serverEpisodeId: this.mediaItemEpisodeId, startTime })
} else {
// Only on server
this.$eventBus.$emit('play-item', { libraryItemId: this.mediaItemLibraryItemId, episodeId: this.mediaItemEpisodeId, startTime })
}
}
},
getEventIcon(name) {
switch (name) {
case 'Play':
@ -113,6 +169,8 @@ export default {
return 'sync'
case 'Seek':
return 'commit'
case 'Sync':
return 'cloud_download'
default:
return 'info'
}
@ -129,6 +187,8 @@ export default {
return 'info'
case 'Seek':
return 'gray-200'
case 'Sync':
return 'accent'
default:
return 'info'
}

View file

@ -43,7 +43,7 @@ class AbsAudioPlayerWeb extends WebPlugin {
}
// PluginMethod
async prepareLibraryItem({ libraryItemId, episodeId, playWhenReady }) {
async prepareLibraryItem({ libraryItemId, episodeId, playWhenReady, startTime }) {
console.log('[AbsAudioPlayer] Prepare library item', libraryItemId)
if (libraryItemId.startsWith('local_')) {
@ -52,6 +52,7 @@ class AbsAudioPlayerWeb extends WebPlugin {
var route = !episodeId ? `/api/items/${libraryItemId}/play` : `/api/items/${libraryItemId}/play/${episodeId}`
var playbackSession = await $axios.$post(route, { mediaPlayer: 'html5-mobile', forceDirectPlay: true })
if (playbackSession) {
if (startTime !== undefined && startTime !== null) playbackSession.currentTime = startTime
this.setAudioPlayer(playbackSession, true)
}
}

View file

@ -62,6 +62,10 @@ class DbService {
return AbsDatabase.getAllLocalMediaProgress().then((data) => data.value)
}
getLocalMediaProgressForServerItem(payload) {
return AbsDatabase.getLocalMediaProgressForServerItem(payload)
}
removeLocalMediaProgress(localMediaProgressId) {
return AbsDatabase.removeLocalMediaProgress({ localMediaProgressId })
}

View file

@ -104,7 +104,7 @@ Vue.prototype.$secondsToTimestamp = (seconds) => {
}
Vue.prototype.$secondsToTimestampFull = (seconds) => {
let _seconds = seconds
let _seconds = Math.round(seconds)
let _minutes = Math.floor(seconds / 60)
_seconds -= _minutes * 60
let _hours = Math.floor(_minutes / 60)