diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt b/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt index 7c2b00f6..d8420940 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt @@ -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( diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaProgress.kt b/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaProgress.kt index e6658fcc..ceae4f43 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaProgress.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaProgress.kt @@ -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 } diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/MediaProgress.kt b/android/app/src/main/java/com/audiobookshelf/app/data/MediaProgress.kt new file mode 100644 index 00000000..415ae2b0 --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/data/MediaProgress.kt @@ -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" +} diff --git a/android/app/src/main/java/com/audiobookshelf/app/media/MediaEventManager.kt b/android/app/src/main/java/com/audiobookshelf/app/media/MediaEventManager.kt index 72055cc5..046fd90d 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/media/MediaEventManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/media/MediaEventManager.kt @@ -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 { diff --git a/android/app/src/main/java/com/audiobookshelf/app/media/MediaProgressSyncer.kt b/android/app/src/main/java/com/audiobookshelf/app/media/MediaProgressSyncer.kt index 3f7de691..634ef419 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/media/MediaProgressSyncer.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/media/MediaProgressSyncer.kt @@ -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}%") } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerListener.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerListener.kt index a44a7ffb..7039fcf3 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerListener.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerListener.kt @@ -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,51 +31,48 @@ 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() - if (seekBackTime > 0) { - // Current chapter is used so that seek back does not go back to the previous chapter - val currentChapter = playerNotificationService.getCurrentBookChapter() - val minSeekBackTime = currentChapter?.startMs ?: 0 + Log.d(tag, "SeekBackTime: playing started now set seek back time $lastPauseTime") + 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() + val minSeekBackTime = currentChapter?.startMs ?: 0 - val currentTime = playerNotificationService.getCurrentTime() - val newTime = currentTime - seekBackTime - if (newTime < minSeekBackTime) { - seekBackTime = currentTime - minSeekBackTime - } - Log.d(tag, "SeekBackTime $seekBackTime") - onSeekBack = true + val currentTime = playerNotificationService.getCurrentTime() + val newTime = currentTime - seekBackTime + if (newTime < minSeekBackTime) { + seekBackTime = currentTime - minSeekBackTime } + Log.d(tag, "SeekBackTime $seekBackTime") } // 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) } diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt index e4e3ba13..1975fd66 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt @@ -823,6 +823,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } currentPlaybackSession = null + mediaProgressSyncer.reset() clientEventEmitter?.onPlaybackClosed() PlayerListener.lastPauseTime = 0 isClosed = true diff --git a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt index 824362b7..da9d5dd9 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt @@ -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))) diff --git a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt index 49025a94..900584f8 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt @@ -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) } } diff --git a/components/app/AudioPlayerContainer.vue b/components/app/AudioPlayerContainer.vue index 618e8865..6fccdb9e 100644 --- a/components/app/AudioPlayerContainer.vue +++ b/components/app/AudioPlayerContainer.vue @@ -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' diff --git a/layouts/default.vue b/layouts/default.vue index e40f6f42..4ef44e8b 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -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 diff --git a/pages/media/_id/history.vue b/pages/media/_id/history.vue index 67b0d70e..b013fbd4 100644 --- a/pages/media/_id/history.vue +++ b/pages/media/_id/history.vue @@ -10,16 +10,16 @@

{{ name }}

{{ $formatDate(evt.timestamp, 'HH:mm') }}

- {{ getEventIcon(evt.name) }} -

{{ evt.name }}

+ {{ getEventIcon(evt.name) }} +

{{ evt.name }}

- cloud_done - error_outline + cloud_done + error_outline -

+{{ evt.num }}

+

+{{ evt.num }}

-

{{ $secondsToTimestampFull(evt.currentTime) }}

+

{{ $secondsToTimestampFull(evt.currentTime) }}

@@ -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' } diff --git a/plugins/capacitor/AbsAudioPlayer.js b/plugins/capacitor/AbsAudioPlayer.js index 04ce4012..a60fac0a 100644 --- a/plugins/capacitor/AbsAudioPlayer.js +++ b/plugins/capacitor/AbsAudioPlayer.js @@ -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) } } diff --git a/plugins/db.js b/plugins/db.js index 3197d29e..38c40f95 100644 --- a/plugins/db.js +++ b/plugins/db.js @@ -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 }) } diff --git a/plugins/init.client.js b/plugins/init.client.js index d2e39118..3ccb8582 100644 --- a/plugins/init.client.js +++ b/plugins/init.client.js @@ -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)