mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-13 15:34:50 +02:00
Local media sync events
This commit is contained in:
parent
297eca6a86
commit
8f6dd72df2
15 changed files with 234 additions and 80 deletions
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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}%")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -823,6 +823,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
}
|
||||
|
||||
currentPlaybackSession = null
|
||||
mediaProgressSyncer.reset()
|
||||
clientEventEmitter?.onPlaybackClosed()
|
||||
PlayerListener.lastPauseTime = 0
|
||||
isClosed = true
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue