mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-09-01 08:07:20 +02:00
MediaItemHistory and history page
This commit is contained in:
@@ -242,4 +242,12 @@ class DbManager {
|
|||||||
fun getLocalPlaybackSession(playbackSessionId:String):PlaybackSession? {
|
fun getLocalPlaybackSession(playbackSessionId:String):PlaybackSession? {
|
||||||
return Paper.book("localPlaybackSession").read(playbackSessionId)
|
return Paper.book("localPlaybackSession").read(playbackSessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun saveMediaItemHistory(mediaItemHistory:MediaItemHistory) {
|
||||||
|
Paper.book("mediaItemHistory").write(mediaItemHistory.id,mediaItemHistory)
|
||||||
|
}
|
||||||
|
fun getMediaItemHistory(id:String):MediaItemHistory? {
|
||||||
|
return Paper.book("mediaItemHistory").read(id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
class MediaItemEvent(
|
||||||
|
var name:String, // e.g. Play/Pause/Stop/Seek/Save
|
||||||
|
var type:String, // Playback/Info
|
||||||
|
var description:String?,
|
||||||
|
var currentTime:Number?, // Seconds
|
||||||
|
var serverSyncAttempted:Boolean?,
|
||||||
|
var serverSyncSuccess:Boolean?,
|
||||||
|
var serverSyncMessage:String?,
|
||||||
|
var timestamp: Long
|
||||||
|
)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
class MediaItemHistory(
|
||||||
|
var id: String, // media id
|
||||||
|
var mediaDisplayTitle: String,
|
||||||
|
var libraryItemId: String,
|
||||||
|
var episodeId: String?,
|
||||||
|
var isLocal:Boolean,
|
||||||
|
var serverConnectionConfigId:String?,
|
||||||
|
var serverAddress:String?,
|
||||||
|
var serverUserId:String?,
|
||||||
|
var createdAt: Long,
|
||||||
|
var events:MutableList<MediaItemEvent>,
|
||||||
|
)
|
||||||
@@ -65,11 +65,13 @@ class PlaybackSession(
|
|||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val localLibraryItemId get() = localLibraryItem?.id ?: ""
|
val localLibraryItemId get() = localLibraryItem?.id ?: ""
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val localMediaProgressId get() = if (episodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
|
val localMediaProgressId get() = if (localEpisodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val progress get() = currentTime / getTotalDuration()
|
val progress get() = currentTime / getTotalDuration()
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val isLocalLibraryItemOnly get() = localLibraryItemId != "" && libraryItemId == null
|
val isLocalLibraryItemOnly get() = localLibraryItemId != "" && libraryItemId == null
|
||||||
|
@get:JsonIgnore
|
||||||
|
val mediaItemId get() = if (isLocalLibraryItemOnly) localMediaProgressId else if (episodeId.isNullOrEmpty()) libraryItemId ?: "" else "$libraryItemId-$episodeId"
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getCurrentTrackIndex():Int {
|
fun getCurrentTrackIndex():Int {
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
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.device.DeviceManager
|
||||||
|
import com.audiobookshelf.app.player.PlayerNotificationService
|
||||||
|
import com.audiobookshelf.app.player.SyncResult
|
||||||
|
|
||||||
|
object MediaEventManager {
|
||||||
|
const val tag = "MediaEventManager"
|
||||||
|
|
||||||
|
var clientEventEmitter: PlayerNotificationService.ClientEventEmitter? = null
|
||||||
|
|
||||||
|
fun playEvent(playbackSession: PlaybackSession) {
|
||||||
|
Log.i(tag, "Play Event for media \"${playbackSession.displayTitle}\"")
|
||||||
|
addPlaybackEvent("Play", playbackSession, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pauseEvent(playbackSession: PlaybackSession, syncResult: SyncResult?) {
|
||||||
|
Log.i(tag, "Pause Event for media \"${playbackSession.displayTitle}\"")
|
||||||
|
addPlaybackEvent("Pause", playbackSession, syncResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopEvent(playbackSession: PlaybackSession, syncResult: SyncResult?) {
|
||||||
|
Log.i(tag, "Stop Event for media \"${playbackSession.displayTitle}\"")
|
||||||
|
addPlaybackEvent("Stop", playbackSession, syncResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveEvent(playbackSession: PlaybackSession, syncResult: SyncResult?) {
|
||||||
|
Log.i(tag, "Save Event for media \"${playbackSession.displayTitle}\"")
|
||||||
|
addPlaybackEvent("Save", playbackSession, syncResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun finishedEvent(playbackSession: PlaybackSession, syncResult: SyncResult?) {
|
||||||
|
Log.i(tag, "Finished Event for media \"${playbackSession.displayTitle}\"")
|
||||||
|
addPlaybackEvent("Finished", playbackSession, syncResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun seekEvent(playbackSession: PlaybackSession, syncResult: SyncResult?) {
|
||||||
|
Log.i(tag, "Seek Event for media \"${playbackSession.displayTitle}\", currentTime=${playbackSession.currentTime}")
|
||||||
|
addPlaybackEvent("Seek", playbackSession, syncResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addPlaybackEvent(eventName:String, playbackSession:PlaybackSession, syncResult:SyncResult?) {
|
||||||
|
val mediaItemHistory = getMediaItemHistoryForSession(playbackSession) ?: createMediaItemHistoryForSession(playbackSession)
|
||||||
|
|
||||||
|
val mediaItemEvent = MediaItemEvent(
|
||||||
|
name = eventName,
|
||||||
|
type = "Playback",
|
||||||
|
description = "",
|
||||||
|
currentTime = playbackSession.currentTime,
|
||||||
|
serverSyncAttempted = syncResult?.serverSyncAttempted ?: false,
|
||||||
|
serverSyncSuccess = syncResult?.serverSyncSuccess,
|
||||||
|
serverSyncMessage = syncResult?.serverSyncMessage,
|
||||||
|
timestamp = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
mediaItemHistory.events.add(mediaItemEvent)
|
||||||
|
DeviceManager.dbManager.saveMediaItemHistory(mediaItemHistory)
|
||||||
|
|
||||||
|
clientEventEmitter?.onMediaItemHistoryUpdated(mediaItemHistory)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMediaItemHistoryForSession(playbackSession: PlaybackSession) : MediaItemHistory? {
|
||||||
|
return DeviceManager.dbManager.getMediaItemHistory(playbackSession.mediaItemId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createMediaItemHistoryForSession(playbackSession: PlaybackSession):MediaItemHistory {
|
||||||
|
Log.i(tag, "Creating new media item history for media \"${playbackSession.displayTitle}\"")
|
||||||
|
val isLocalOnly = playbackSession.isLocalLibraryItemOnly
|
||||||
|
val libraryItemId = if (isLocalOnly) playbackSession.localLibraryItemId else playbackSession.libraryItemId ?: ""
|
||||||
|
val episodeId:String? = if (isLocalOnly && playbackSession.localEpisodeId != null) playbackSession.localEpisodeId else playbackSession.episodeId
|
||||||
|
return MediaItemHistory(
|
||||||
|
id = playbackSession.mediaItemId,
|
||||||
|
mediaDisplayTitle = playbackSession.displayTitle ?: "Unset",
|
||||||
|
libraryItemId,
|
||||||
|
episodeId,
|
||||||
|
isLocalOnly,
|
||||||
|
playbackSession.
|
||||||
|
serverConnectionConfigId,
|
||||||
|
playbackSession.serverAddress,
|
||||||
|
playbackSession.userId,
|
||||||
|
createdAt = System.currentTimeMillis(),
|
||||||
|
events = mutableListOf())
|
||||||
|
}
|
||||||
|
}
|
||||||
+82
-25
@@ -7,6 +7,7 @@ import com.audiobookshelf.app.data.LocalMediaProgress
|
|||||||
import com.audiobookshelf.app.data.MediaProgress
|
import com.audiobookshelf.app.data.MediaProgress
|
||||||
import com.audiobookshelf.app.data.PlaybackSession
|
import com.audiobookshelf.app.data.PlaybackSession
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
|
import com.audiobookshelf.app.media.MediaEventManager
|
||||||
import com.audiobookshelf.app.server.ApiHandler
|
import com.audiobookshelf.app.server.ApiHandler
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
@@ -17,6 +18,12 @@ data class MediaProgressSyncData(
|
|||||||
var currentTime:Double // seconds
|
var currentTime:Double // seconds
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class SyncResult(
|
||||||
|
var serverSyncAttempted:Boolean,
|
||||||
|
var serverSyncSuccess:Boolean?,
|
||||||
|
var serverSyncMessage:String?
|
||||||
|
)
|
||||||
|
|
||||||
class MediaProgressSyncer(val playerNotificationService:PlayerNotificationService, private val apiHandler: ApiHandler) {
|
class MediaProgressSyncer(val playerNotificationService:PlayerNotificationService, private val apiHandler: ApiHandler) {
|
||||||
private val tag = "MediaProgressSync"
|
private val tag = "MediaProgressSync"
|
||||||
private val METERED_CONNECTION_SYNC_INTERVAL = 60000
|
private val METERED_CONNECTION_SYNC_INTERVAL = 60000
|
||||||
@@ -30,15 +37,15 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
var currentPlaybackSession: PlaybackSession? = null // copy of pb session currently syncing
|
var currentPlaybackSession: PlaybackSession? = null // copy of pb session currently syncing
|
||||||
var currentLocalMediaProgress: LocalMediaProgress? = null
|
var currentLocalMediaProgress: LocalMediaProgress? = null
|
||||||
|
|
||||||
val currentDisplayTitle get() = currentPlaybackSession?.displayTitle ?: "Unset"
|
private val currentDisplayTitle get() = currentPlaybackSession?.displayTitle ?: "Unset"
|
||||||
val currentIsLocal get() = currentPlaybackSession?.isLocal == true
|
private val currentIsLocal get() = currentPlaybackSession?.isLocal == true
|
||||||
val currentSessionId get() = currentPlaybackSession?.id ?: ""
|
val currentSessionId get() = currentPlaybackSession?.id ?: ""
|
||||||
val currentPlaybackDuration get() = currentPlaybackSession?.duration ?: 0.0
|
private val currentPlaybackDuration get() = currentPlaybackSession?.duration ?: 0.0
|
||||||
|
|
||||||
fun start() {
|
fun start(playbackSession:PlaybackSession) {
|
||||||
if (listeningTimerRunning) {
|
if (listeningTimerRunning) {
|
||||||
Log.d(tag, "start: Timer already running for $currentDisplayTitle")
|
Log.d(tag, "start: Timer already running for $currentDisplayTitle")
|
||||||
if (playerNotificationService.getCurrentPlaybackSessionId() != currentSessionId) {
|
if (playbackSession.id != currentSessionId) {
|
||||||
Log.d(tag, "Playback session changed, reset timer")
|
Log.d(tag, "Playback session changed, reset timer")
|
||||||
currentLocalMediaProgress = null
|
currentLocalMediaProgress = null
|
||||||
listeningTimerTask?.cancel()
|
listeningTimerTask?.cancel()
|
||||||
@@ -48,25 +55,29 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
} else {
|
} else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else if (playerNotificationService.getCurrentPlaybackSessionId() != currentSessionId) {
|
} else if (playbackSession.id != currentSessionId) {
|
||||||
currentLocalMediaProgress = null
|
currentLocalMediaProgress = null
|
||||||
}
|
}
|
||||||
|
|
||||||
listeningTimerRunning = true
|
listeningTimerRunning = true
|
||||||
lastSyncTime = System.currentTimeMillis()
|
lastSyncTime = System.currentTimeMillis()
|
||||||
Log.d(tag, "start: init last sync time $lastSyncTime")
|
Log.d(tag, "start: init last sync time $lastSyncTime")
|
||||||
currentPlaybackSession = playerNotificationService.getCurrentPlaybackSessionCopy()
|
currentPlaybackSession = playbackSession.clone()
|
||||||
|
|
||||||
listeningTimerTask = Timer("ListeningTimer", false).schedule(0L, 5000L) {
|
listeningTimerTask = Timer("ListeningTimer", false).schedule(15000L, 15000L) {
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
if (playerNotificationService.currentPlayer.isPlaying) {
|
if (playerNotificationService.currentPlayer.isPlaying) {
|
||||||
// Only sync with server on unmetered connection every 5s OR sync with server if last sync time is >= 60s
|
// Only sync with server on unmetered connection every 15s OR sync with server if last sync time is >= 60s
|
||||||
val shouldSyncServer = PlayerNotificationService.isUnmeteredNetwork || System.currentTimeMillis() - lastSyncTime >= METERED_CONNECTION_SYNC_INTERVAL
|
val shouldSyncServer = PlayerNotificationService.isUnmeteredNetwork || System.currentTimeMillis() - lastSyncTime >= METERED_CONNECTION_SYNC_INTERVAL
|
||||||
|
|
||||||
val currentTime = playerNotificationService.getCurrentTimeSeconds()
|
val currentTime = playerNotificationService.getCurrentTimeSeconds()
|
||||||
if (currentTime > 0) {
|
if (currentTime > 0) {
|
||||||
sync(shouldSyncServer, currentTime) {
|
sync(shouldSyncServer, currentTime) { syncResult ->
|
||||||
Log.d(tag, "Sync complete")
|
Log.d(tag, "Sync complete")
|
||||||
|
|
||||||
|
currentPlaybackSession?.let { playbackSession ->
|
||||||
|
MediaEventManager.saveEvent(playbackSession, syncResult)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,8 +85,16 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun play(playbackSession:PlaybackSession) {
|
||||||
|
Log.d(tag, "play ${playbackSession.displayTitle}")
|
||||||
|
MediaEventManager.playEvent(playbackSession)
|
||||||
|
|
||||||
|
start(playbackSession)
|
||||||
|
}
|
||||||
|
|
||||||
fun stop(shouldSync:Boolean? = true, cb: () -> Unit) {
|
fun stop(shouldSync:Boolean? = true, cb: () -> Unit) {
|
||||||
if (!listeningTimerRunning) return
|
if (!listeningTimerRunning) return
|
||||||
|
|
||||||
listeningTimerTask?.cancel()
|
listeningTimerTask?.cancel()
|
||||||
listeningTimerTask = null
|
listeningTimerTask = null
|
||||||
listeningTimerRunning = false
|
listeningTimerRunning = false
|
||||||
@@ -83,11 +102,19 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
|
|
||||||
val currentTime = if (shouldSync == true) playerNotificationService.getCurrentTimeSeconds() else 0.0
|
val currentTime = if (shouldSync == true) playerNotificationService.getCurrentTimeSeconds() else 0.0
|
||||||
if (currentTime > 0) { // Current time should always be > 0 on stop
|
if (currentTime > 0) { // Current time should always be > 0 on stop
|
||||||
sync(true, currentTime) {
|
sync(true, currentTime) { syncResult ->
|
||||||
|
currentPlaybackSession?.let { playbackSession ->
|
||||||
|
MediaEventManager.stopEvent(playbackSession, syncResult)
|
||||||
|
}
|
||||||
|
|
||||||
reset()
|
reset()
|
||||||
cb()
|
cb()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
currentPlaybackSession?.let { playbackSession ->
|
||||||
|
MediaEventManager.stopEvent(playbackSession, null)
|
||||||
|
}
|
||||||
|
|
||||||
reset()
|
reset()
|
||||||
cb()
|
cb()
|
||||||
}
|
}
|
||||||
@@ -95,6 +122,7 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
|
|
||||||
fun pause(cb: () -> Unit) {
|
fun pause(cb: () -> Unit) {
|
||||||
if (!listeningTimerRunning) return
|
if (!listeningTimerRunning) return
|
||||||
|
|
||||||
listeningTimerTask?.cancel()
|
listeningTimerTask?.cancel()
|
||||||
listeningTimerTask = null
|
listeningTimerTask = null
|
||||||
listeningTimerRunning = false
|
listeningTimerRunning = false
|
||||||
@@ -103,33 +131,61 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
|
|
||||||
val currentTime = playerNotificationService.getCurrentTimeSeconds()
|
val currentTime = playerNotificationService.getCurrentTimeSeconds()
|
||||||
if (currentTime > 0) { // Current time should always be > 0 on pause
|
if (currentTime > 0) { // Current time should always be > 0 on pause
|
||||||
sync(true, currentTime) {
|
sync(true, currentTime) { syncResult ->
|
||||||
lastSyncTime = 0L
|
lastSyncTime = 0L
|
||||||
Log.d(tag, "pause: Set last sync time 0 $lastSyncTime")
|
Log.d(tag, "pause: Set last sync time 0 $lastSyncTime")
|
||||||
failedSyncs = 0
|
failedSyncs = 0
|
||||||
|
|
||||||
|
currentPlaybackSession?.let { playbackSession ->
|
||||||
|
MediaEventManager.pauseEvent(playbackSession, syncResult)
|
||||||
|
}
|
||||||
|
|
||||||
cb()
|
cb()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
lastSyncTime = 0L
|
lastSyncTime = 0L
|
||||||
Log.d(tag, "pause: Set last sync time 0 $lastSyncTime (current time < 0)")
|
Log.d(tag, "pause: Set last sync time 0 $lastSyncTime (current time < 0)")
|
||||||
failedSyncs = 0
|
failedSyncs = 0
|
||||||
|
|
||||||
|
currentPlaybackSession?.let { playbackSession ->
|
||||||
|
MediaEventManager.pauseEvent(playbackSession, null)
|
||||||
|
}
|
||||||
|
|
||||||
cb()
|
cb()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun finished(cb: () -> Unit) {
|
fun finished(cb: () -> Unit) {
|
||||||
if (!listeningTimerRunning) return
|
if (!listeningTimerRunning) return
|
||||||
|
|
||||||
listeningTimerTask?.cancel()
|
listeningTimerTask?.cancel()
|
||||||
listeningTimerTask = null
|
listeningTimerTask = null
|
||||||
listeningTimerRunning = false
|
listeningTimerRunning = false
|
||||||
Log.d(tag, "stop: Stopping listening for $currentDisplayTitle")
|
Log.d(tag, "finished: Stopping listening for $currentDisplayTitle")
|
||||||
|
|
||||||
sync(true, currentPlaybackSession?.duration ?: 0.0) {
|
sync(true, currentPlaybackSession?.duration ?: 0.0) { syncResult ->
|
||||||
reset()
|
reset()
|
||||||
|
|
||||||
|
currentPlaybackSession?.let { playbackSession ->
|
||||||
|
MediaEventManager.finishedEvent(playbackSession, syncResult)
|
||||||
|
}
|
||||||
|
|
||||||
cb()
|
cb()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun seek() {
|
||||||
|
currentPlaybackSession?.currentTime = playerNotificationService.getCurrentTimeSeconds()
|
||||||
|
Log.d(tag, "seek: $currentDisplayTitle, currentTime=${currentPlaybackSession?.currentTime}")
|
||||||
|
|
||||||
|
if (currentPlaybackSession == null) {
|
||||||
|
Log.e(tag, "seek: Playback session not set")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaEventManager.seekEvent(currentPlaybackSession!!, null)
|
||||||
|
}
|
||||||
|
|
||||||
fun syncFromServerProgress(mediaProgress: MediaProgress) {
|
fun syncFromServerProgress(mediaProgress: MediaProgress) {
|
||||||
currentPlaybackSession?.let {
|
currentPlaybackSession?.let {
|
||||||
it.updatedAt = mediaProgress.lastUpdate
|
it.updatedAt = mediaProgress.lastUpdate
|
||||||
@@ -139,15 +195,15 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sync(shouldSyncServer:Boolean, currentTime:Double, cb: () -> Unit) {
|
fun sync(shouldSyncServer:Boolean, currentTime:Double, cb: (SyncResult?) -> Unit) {
|
||||||
if (lastSyncTime <= 0) {
|
if (lastSyncTime <= 0) {
|
||||||
Log.e(tag, "Last sync time is not set $lastSyncTime")
|
Log.e(tag, "Last sync time is not set $lastSyncTime")
|
||||||
return
|
return cb(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
val diffSinceLastSync = System.currentTimeMillis() - lastSyncTime
|
val diffSinceLastSync = System.currentTimeMillis() - lastSyncTime
|
||||||
if (diffSinceLastSync < 1000L) {
|
if (diffSinceLastSync < 1000L) {
|
||||||
return cb()
|
return cb(null)
|
||||||
}
|
}
|
||||||
val listeningTimeToAdd = diffSinceLastSync / 1000L
|
val listeningTimeToAdd = diffSinceLastSync / 1000L
|
||||||
|
|
||||||
@@ -157,7 +213,7 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
|
|
||||||
if (currentPlaybackSession?.progress?.isNaN() == true) {
|
if (currentPlaybackSession?.progress?.isNaN() == true) {
|
||||||
Log.e(tag, "Current Playback Session invalid progress ${currentPlaybackSession?.progress} | Current Time: ${currentPlaybackSession?.currentTime} | Duration: ${currentPlaybackSession?.getTotalDuration()}")
|
Log.e(tag, "Current Playback Session invalid progress ${currentPlaybackSession?.progress} | Current Time: ${currentPlaybackSession?.currentTime} | Duration: ${currentPlaybackSession?.getTotalDuration()}")
|
||||||
return cb()
|
return cb(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentIsLocal) {
|
if (currentIsLocal) {
|
||||||
@@ -170,11 +226,12 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
// Local library item is linked to a server library item
|
// Local library item is linked to a server library item
|
||||||
// Send sync to server also if connected to this server and local item belongs to this server
|
// Send sync to server also if connected to this server and local item belongs to this server
|
||||||
if (shouldSyncServer && !it.libraryItemId.isNullOrEmpty() && it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
|
if (shouldSyncServer && !it.libraryItemId.isNullOrEmpty() && it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
|
||||||
apiHandler.sendLocalProgressSync(it) { syncSuccess ->
|
apiHandler.sendLocalProgressSync(it) { syncSuccess, errorMsg ->
|
||||||
Log.d(
|
Log.d(
|
||||||
tag,
|
tag,
|
||||||
"Local progress sync data sent to server $currentDisplayTitle for time $currentTime"
|
"Local progress sync data sent to server $currentDisplayTitle for time $currentTime"
|
||||||
)
|
)
|
||||||
|
|
||||||
if (syncSuccess) {
|
if (syncSuccess) {
|
||||||
failedSyncs = 0
|
failedSyncs = 0
|
||||||
playerNotificationService.alertSyncSuccess()
|
playerNotificationService.alertSyncSuccess()
|
||||||
@@ -187,15 +244,15 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
Log.e(tag, "Local Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime")
|
Log.e(tag, "Local Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime")
|
||||||
}
|
}
|
||||||
|
|
||||||
cb()
|
cb(SyncResult(true, syncSuccess, errorMsg))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
cb()
|
cb(SyncResult(false, null, null))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (shouldSyncServer) {
|
} else if (shouldSyncServer) {
|
||||||
apiHandler.sendProgressSync(currentSessionId, syncData) {
|
apiHandler.sendProgressSync(currentSessionId, syncData) { syncSuccess, errorMsg ->
|
||||||
if (it) {
|
if (syncSuccess) {
|
||||||
Log.d(tag, "Progress sync data sent to server $currentDisplayTitle for time $currentTime")
|
Log.d(tag, "Progress sync data sent to server $currentDisplayTitle for time $currentTime")
|
||||||
failedSyncs = 0
|
failedSyncs = 0
|
||||||
playerNotificationService.alertSyncSuccess()
|
playerNotificationService.alertSyncSuccess()
|
||||||
@@ -208,10 +265,10 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
}
|
}
|
||||||
Log.e(tag, "Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime")
|
Log.e(tag, "Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime")
|
||||||
}
|
}
|
||||||
cb()
|
cb(SyncResult(true, syncSuccess, errorMsg))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
cb()
|
cb(SyncResult(false, null, null))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
package com.audiobookshelf.app.player
|
package com.audiobookshelf.app.player
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.audiobookshelf.app.data.PlaybackSession
|
||||||
import com.audiobookshelf.app.data.PlayerState
|
import com.audiobookshelf.app.data.PlayerState
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
|
import com.audiobookshelf.app.media.MediaEventManager
|
||||||
import com.google.android.exoplayer2.PlaybackException
|
import com.google.android.exoplayer2.PlaybackException
|
||||||
import com.google.android.exoplayer2.Player
|
import com.google.android.exoplayer2.Player
|
||||||
|
|
||||||
@@ -16,6 +18,7 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var onSeekBack: Boolean = false
|
private var onSeekBack: Boolean = false
|
||||||
|
private var isSeeking: Boolean = false
|
||||||
|
|
||||||
override fun onPlayerError(error: PlaybackException) {
|
override fun onPlayerError(error: PlaybackException) {
|
||||||
val errorMessage = error.message ?: "Unknown Error"
|
val errorMessage = error.message ?: "Unknown Error"
|
||||||
@@ -23,6 +26,92 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
|||||||
playerNotificationService.handlePlayerPlaybackError(errorMessage) // If was direct playing session, fallback to transcode
|
playerNotificationService.handlePlayerPlaybackError(errorMessage) // If was direct playing session, fallback to transcode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPositionDiscontinuity(
|
||||||
|
oldPosition: Player.PositionInfo,
|
||||||
|
newPosition: Player.PositionInfo,
|
||||||
|
reason: Int
|
||||||
|
) {
|
||||||
|
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()
|
||||||
|
} 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
|
||||||
|
}
|
||||||
|
|
||||||
|
val player = playerNotificationService.currentPlayer
|
||||||
|
|
||||||
|
if (player.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
|
||||||
|
|
||||||
|
val currentTime = playerNotificationService.getCurrentTime()
|
||||||
|
val newTime = currentTime - seekBackTime
|
||||||
|
if (newTime < minSeekBackTime) {
|
||||||
|
seekBackTime = currentTime - minSeekBackTime
|
||||||
|
}
|
||||||
|
Log.d(tag, "SeekBackTime $seekBackTime")
|
||||||
|
onSeekBack = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if playback session still exists or sync media progress if updated
|
||||||
|
val pauseLength: Long = System.currentTimeMillis() - lastPauseTime
|
||||||
|
if (pauseLength > PAUSE_LEN_BEFORE_RECHECK) {
|
||||||
|
val shouldCarryOn = playerNotificationService.checkCurrentSessionProgress(seekBackTime)
|
||||||
|
if (!shouldCarryOn) return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seekBackTime > 0L) {
|
||||||
|
playerNotificationService.seekBackward(seekBackTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d(tag, "SeekBackTime: Player not playing set last pause time")
|
||||||
|
lastPauseTime = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start/stop progress sync interval
|
||||||
|
if (player.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) }
|
||||||
|
} else {
|
||||||
|
playerNotificationService.mediaProgressSyncer.pause {
|
||||||
|
Log.d(tag, "Media Progress Syncer paused and synced")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playerNotificationService.clientEventEmitter?.onPlayingUpdate(player.isPlaying)
|
||||||
|
|
||||||
|
DeviceManager.widgetUpdater?.onPlayerChanged(playerNotificationService)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onEvents(player: Player, events: Player.Events) {
|
override fun onEvents(player: Player, events: Player.Events) {
|
||||||
Log.d(tag, "onEvents ${playerNotificationService.getMediaPlayer()} | ${events.size()}")
|
Log.d(tag, "onEvents ${playerNotificationService.getMediaPlayer()} | ${events.size()}")
|
||||||
|
|
||||||
@@ -67,62 +156,6 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
|||||||
if (events.contains(Player.EVENT_PLAYLIST_METADATA_CHANGED)) {
|
if (events.contains(Player.EVENT_PLAYLIST_METADATA_CHANGED)) {
|
||||||
Log.d(tag, "EVENT_PLAYLIST_METADATA_CHANGED ${playerNotificationService.getMediaPlayer()}")
|
Log.d(tag, "EVENT_PLAYLIST_METADATA_CHANGED ${playerNotificationService.getMediaPlayer()}")
|
||||||
}
|
}
|
||||||
if (events.contains(Player.EVENT_IS_PLAYING_CHANGED)) {
|
|
||||||
Log.d(tag, "EVENT IS PLAYING CHANGED ${playerNotificationService.getMediaPlayer()}")
|
|
||||||
|
|
||||||
if (player.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
|
|
||||||
|
|
||||||
val currentTime = playerNotificationService.getCurrentTime()
|
|
||||||
val newTime = currentTime - seekBackTime
|
|
||||||
if (newTime < minSeekBackTime) {
|
|
||||||
seekBackTime = currentTime - minSeekBackTime
|
|
||||||
}
|
|
||||||
Log.d(tag, "SeekBackTime $seekBackTime")
|
|
||||||
onSeekBack = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if playback session still exists or sync media progress if updated
|
|
||||||
val pauseLength: Long = System.currentTimeMillis() - lastPauseTime
|
|
||||||
if (pauseLength > PAUSE_LEN_BEFORE_RECHECK) {
|
|
||||||
val shouldCarryOn = playerNotificationService.checkCurrentSessionProgress(seekBackTime)
|
|
||||||
if (!shouldCarryOn) return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (seekBackTime > 0L) {
|
|
||||||
playerNotificationService.seekBackward(seekBackTime)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.d(tag, "SeekBackTime: Player not playing set last pause time")
|
|
||||||
lastPauseTime = System.currentTimeMillis()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start/stop progress sync interval
|
|
||||||
if (player.isPlaying) {
|
|
||||||
player.volume = 1F // Volume on sleep timer might have decreased this
|
|
||||||
playerNotificationService.mediaProgressSyncer.start()
|
|
||||||
} else {
|
|
||||||
playerNotificationService.mediaProgressSyncer.pause {
|
|
||||||
Log.d(tag, "Media Progress Syncer paused and synced")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
playerNotificationService.clientEventEmitter?.onPlayingUpdate(player.isPlaying)
|
|
||||||
|
|
||||||
DeviceManager.widgetUpdater?.onPlayerChanged(playerNotificationService)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calcPauseSeekBackTime() : Long {
|
private fun calcPauseSeekBackTime() : Long {
|
||||||
|
|||||||
+6
-4
@@ -74,6 +74,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
fun onProgressSyncFailing()
|
fun onProgressSyncFailing()
|
||||||
fun onProgressSyncSuccess()
|
fun onProgressSyncSuccess()
|
||||||
fun onNetworkMeteredChanged(isUnmetered:Boolean)
|
fun onNetworkMeteredChanged(isUnmetered:Boolean)
|
||||||
|
fun onMediaItemHistoryUpdated(mediaItemHistory:MediaItemHistory)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val tag = "PlayerService"
|
private val tag = "PlayerService"
|
||||||
@@ -101,7 +102,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
private var channelId = "audiobookshelf_channel"
|
private var channelId = "audiobookshelf_channel"
|
||||||
private var channelName = "Audiobookshelf Channel"
|
private var channelName = "Audiobookshelf Channel"
|
||||||
|
|
||||||
private var currentPlaybackSession:PlaybackSession? = null
|
var currentPlaybackSession:PlaybackSession? = null
|
||||||
private var initialPlaybackRate:Float? = null
|
private var initialPlaybackRate:Float? = null
|
||||||
|
|
||||||
private var isAndroidAuto = false
|
private var isAndroidAuto = false
|
||||||
@@ -682,7 +683,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
seekPlayer(playbackSession.currentTimeMs)
|
seekPlayer(playbackSession.currentTimeMs)
|
||||||
// Should already be playing
|
// Should already be playing
|
||||||
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
|
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
|
||||||
mediaProgressSyncer.start()
|
currentPlaybackSession?.let { mediaProgressSyncer.play(it) }
|
||||||
clientEventEmitter?.onPlayingUpdate(true)
|
clientEventEmitter?.onPlayingUpdate(true)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -690,9 +691,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
if (seekBackTime > 0L) {
|
if (seekBackTime > 0L) {
|
||||||
seekBackward(seekBackTime)
|
seekBackward(seekBackTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should already be playing
|
// Should already be playing
|
||||||
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
|
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
|
||||||
mediaProgressSyncer.start()
|
currentPlaybackSession?.let { mediaProgressSyncer.play(it) }
|
||||||
clientEventEmitter?.onPlayingUpdate(true)
|
clientEventEmitter?.onPlayingUpdate(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -722,7 +724,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
|
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
|
||||||
mediaProgressSyncer.start()
|
mediaProgressSyncer.play(it)
|
||||||
clientEventEmitter?.onPlayingUpdate(true)
|
clientEventEmitter?.onPlayingUpdate(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import android.util.Log
|
|||||||
import com.audiobookshelf.app.MainActivity
|
import com.audiobookshelf.app.MainActivity
|
||||||
import com.audiobookshelf.app.data.*
|
import com.audiobookshelf.app.data.*
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
|
import com.audiobookshelf.app.media.MediaEventManager
|
||||||
import com.audiobookshelf.app.player.CastManager
|
import com.audiobookshelf.app.player.CastManager
|
||||||
|
import com.audiobookshelf.app.player.MediaProgressSyncer
|
||||||
import com.audiobookshelf.app.player.PlayerNotificationService
|
import com.audiobookshelf.app.player.PlayerNotificationService
|
||||||
import com.audiobookshelf.app.server.ApiHandler
|
import com.audiobookshelf.app.server.ApiHandler
|
||||||
import com.fasterxml.jackson.core.json.JsonReadFeature
|
import com.fasterxml.jackson.core.json.JsonReadFeature
|
||||||
@@ -88,7 +90,13 @@ class AbsAudioPlayer : Plugin() {
|
|||||||
override fun onNetworkMeteredChanged(isUnmetered:Boolean) {
|
override fun onNetworkMeteredChanged(isUnmetered:Boolean) {
|
||||||
emit("onNetworkMeteredChanged", isUnmetered)
|
emit("onNetworkMeteredChanged", isUnmetered)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onMediaItemHistoryUpdated(mediaItemHistory:MediaItemHistory) {
|
||||||
|
notifyListeners("onMediaItemHistoryUpdated", JSObject(jacksonMapper.writeValueAsString(mediaItemHistory)))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
MediaEventManager.clientEventEmitter = playerNotificationService.clientEventEmitter
|
||||||
}
|
}
|
||||||
mainActivity.pluginCallback = foregroundServiceReady
|
mainActivity.pluginCallback = foregroundServiceReady
|
||||||
}
|
}
|
||||||
@@ -267,6 +275,7 @@ class AbsAudioPlayer : Plugin() {
|
|||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun seek(call: PluginCall) {
|
fun seek(call: PluginCall) {
|
||||||
val time:Int = call.getInt("value", 0) ?: 0 // Value in seconds
|
val time:Int = call.getInt("value", 0) ?: 0 // Value in seconds
|
||||||
|
Log.d(tag, "seek action to $time")
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
playerNotificationService.seekPlayer(time * 1000L) // convert to ms
|
playerNotificationService.seekPlayer(time * 1000L) // convert to ms
|
||||||
call.resolve()
|
call.resolve()
|
||||||
|
|||||||
@@ -406,4 +406,19 @@ class AbsDatabase : Plugin() {
|
|||||||
call.resolve(JSObject(jacksonMapper.writeValueAsString(DeviceManager.deviceData)))
|
call.resolve(JSObject(jacksonMapper.writeValueAsString(DeviceManager.deviceData)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun getMediaItemHistory(call:PluginCall) { // Returns device data
|
||||||
|
Log.d(tag, "getMediaItemHistory ${call.data}")
|
||||||
|
val mediaId = call.getString("mediaId") ?: ""
|
||||||
|
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
val mediaItemHistory = DeviceManager.dbManager.getMediaItemHistory(mediaId)
|
||||||
|
if (mediaItemHistory == null) {
|
||||||
|
call.resolve()
|
||||||
|
} else {
|
||||||
|
call.resolve(JSObject(jacksonMapper.writeValueAsString(mediaItemHistory)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -226,26 +226,26 @@ class ApiHandler(var ctx:Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendProgressSync(sessionId:String, syncData: MediaProgressSyncData, cb: (Boolean) -> Unit) {
|
fun sendProgressSync(sessionId:String, syncData: MediaProgressSyncData, cb: (Boolean, String?) -> Unit) {
|
||||||
val payload = JSObject(jacksonMapper.writeValueAsString(syncData))
|
val payload = JSObject(jacksonMapper.writeValueAsString(syncData))
|
||||||
|
|
||||||
postRequest("/api/session/$sessionId/sync", payload, null) {
|
postRequest("/api/session/$sessionId/sync", payload, null) {
|
||||||
if (!it.getString("error").isNullOrEmpty()) {
|
if (!it.getString("error").isNullOrEmpty()) {
|
||||||
cb(false)
|
cb(false, it.getString("error"))
|
||||||
} else {
|
} else {
|
||||||
cb(true)
|
cb(true, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: (Boolean) -> Unit) {
|
fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: (Boolean, String?) -> Unit) {
|
||||||
val payload = JSObject(jacksonMapper.writeValueAsString(playbackSession))
|
val payload = JSObject(jacksonMapper.writeValueAsString(playbackSession))
|
||||||
|
|
||||||
postRequest("/api/session/local", payload, null) {
|
postRequest("/api/session/local", payload, null) {
|
||||||
if (!it.getString("error").isNullOrEmpty()) {
|
if (!it.getString("error").isNullOrEmpty()) {
|
||||||
cb(false)
|
cb(false, it.getString("error"))
|
||||||
} else {
|
} else {
|
||||||
cb(true)
|
cb(true, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -2,14 +2,14 @@
|
|||||||
font-family: 'Material Icons';
|
font-family: 'Material Icons';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: url(/fonts/MaterialIcons.woff2) format('woff2');
|
src: url(/fonts/MaterialIcons-Regular.ttf) format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Material Icons Outlined';
|
font-family: 'Material Icons Outlined';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: url(/fonts/MaterialIconsOutlined.woff2) format('woff2');
|
src: url(/fonts/MaterialIconsOutlined-Regular.otf) format('opentype');
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-icons {
|
.material-icons {
|
||||||
|
|||||||
@@ -367,6 +367,14 @@ export default {
|
|||||||
const items = []
|
const items = []
|
||||||
|
|
||||||
if (!this.isPodcast) {
|
if (!this.isPodcast) {
|
||||||
|
// TODO: Implement on iOS
|
||||||
|
if (!this.isIos) {
|
||||||
|
items.push({
|
||||||
|
text: 'History',
|
||||||
|
value: 'history'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
text: this.userIsFinished ? 'Mark as Not Finished' : 'Mark as Finished',
|
text: this.userIsFinished ? 'Mark as Not Finished' : 'Mark as Finished',
|
||||||
value: 'markFinished'
|
value: 'markFinished'
|
||||||
@@ -401,6 +409,10 @@ export default {
|
|||||||
|
|
||||||
if (width * this.bookCoverAspectRatio > 325) width = 325 / this.bookCoverAspectRatio
|
if (width * this.bookCoverAspectRatio > 325) width = 325 / this.bookCoverAspectRatio
|
||||||
return width
|
return width
|
||||||
|
},
|
||||||
|
mediaId() {
|
||||||
|
if (this.isPodcast) return null
|
||||||
|
return this.serverLibraryItemId || this.localLibraryItemId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -430,6 +442,8 @@ export default {
|
|||||||
} else if (action === 'markFinished') {
|
} else if (action === 'markFinished') {
|
||||||
if (this.isProcessingReadUpdate) return
|
if (this.isProcessingReadUpdate) return
|
||||||
this.toggleFinished()
|
this.toggleFinished()
|
||||||
|
} else if (action === 'history') {
|
||||||
|
this.$router.push(`/media/${this.mediaId}/history?title=${this.title}`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
moreButtonPress() {
|
moreButtonPress() {
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full h-full px-3 py-4 overflow-y-auto relative bg-bg">
|
||||||
|
<p class="mb-4 text-lg font-semibold">History for {{ displayTitle }}</p>
|
||||||
|
|
||||||
|
<div v-if="!mediaEvents.length" class="text-center py-8">
|
||||||
|
<p class="text-gray-200">No History</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="(events, name) in groupedMediaEvents" :key="name" class="py-2">
|
||||||
|
<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 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>
|
||||||
|
|
||||||
|
<p v-if="evt.num" class="text-sm text-gray-400 italic">+{{ evt.num }}</p>
|
||||||
|
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<p class="text-base text-white">{{ $secondsToTimestampFull(evt.currentTime) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { AbsAudioPlayer } from '@/plugins/capacitor'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async asyncData({ params, store, redirect, app, query }) {
|
||||||
|
const mediaItemHistory = await app.$db.getMediaItemHistory(params.id)
|
||||||
|
|
||||||
|
if (!mediaItemHistory) {
|
||||||
|
return redirect('/?error=Media Item Not Found')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: query.title || 'Unknown',
|
||||||
|
mediaItemHistory
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
onMediaItemHistoryUpdatedListener: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
displayTitle() {
|
||||||
|
if (!this.mediaItemHistory) return this.title
|
||||||
|
return this.mediaItemHistory.mediaDisplayTitle
|
||||||
|
},
|
||||||
|
mediaEvents() {
|
||||||
|
if (!this.mediaItemHistory) return []
|
||||||
|
return (this.mediaItemHistory.events || []).sort((a, b) => b.timestamp - a.timestamp)
|
||||||
|
},
|
||||||
|
groupedMediaEvents() {
|
||||||
|
const groups = {}
|
||||||
|
|
||||||
|
const today = this.$formatDate(new Date(), 'MMM dd, yyyy')
|
||||||
|
const yesterday = this.$formatDate(Date.now() - 1000 * 60 * 60 * 24, 'MMM dd, yyyy')
|
||||||
|
|
||||||
|
let lastKey = null
|
||||||
|
let numSaves = 0
|
||||||
|
let index = 0
|
||||||
|
|
||||||
|
this.mediaEvents.forEach((evt) => {
|
||||||
|
const date = this.$formatDate(evt.timestamp, 'MMM dd, yyyy')
|
||||||
|
let include = true
|
||||||
|
|
||||||
|
let key = date
|
||||||
|
if (date === today) key = 'Today'
|
||||||
|
else if (date === yesterday) key = 'Yesterday'
|
||||||
|
|
||||||
|
if (!groups[key]) groups[key] = []
|
||||||
|
|
||||||
|
if (!lastKey) lastKey = key
|
||||||
|
|
||||||
|
// Collapse saves
|
||||||
|
if (evt.name === 'Save') {
|
||||||
|
if (numSaves > 0 && lastKey === key) {
|
||||||
|
include = false
|
||||||
|
groups[key][index - 1].num = numSaves
|
||||||
|
numSaves++
|
||||||
|
} else {
|
||||||
|
lastKey = key
|
||||||
|
numSaves = 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
numSaves = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (include) {
|
||||||
|
groups[key].push(evt)
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return groups
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getEventIcon(name) {
|
||||||
|
switch (name) {
|
||||||
|
case 'Play':
|
||||||
|
return 'play_circle_filled'
|
||||||
|
case 'Pause':
|
||||||
|
return 'pause_circle_filled'
|
||||||
|
case 'Stop':
|
||||||
|
return 'stop_circle'
|
||||||
|
case 'Save':
|
||||||
|
return 'sync'
|
||||||
|
case 'Seek':
|
||||||
|
return 'commit'
|
||||||
|
default:
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getEventColor(name) {
|
||||||
|
switch (name) {
|
||||||
|
case 'Play':
|
||||||
|
return 'success'
|
||||||
|
case 'Pause':
|
||||||
|
return 'gray-300'
|
||||||
|
case 'Stop':
|
||||||
|
return 'error'
|
||||||
|
case 'Save':
|
||||||
|
return 'info'
|
||||||
|
case 'Seek':
|
||||||
|
return 'gray-200'
|
||||||
|
default:
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onMediaItemHistoryUpdated(mediaItemHistory) {
|
||||||
|
if (!mediaItemHistory || !mediaItemHistory.id) {
|
||||||
|
console.error('Invalid media item history', mediaItemHistory)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log('Media Item History updated')
|
||||||
|
|
||||||
|
this.mediaItemHistory = mediaItemHistory
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
this.onMediaItemHistoryUpdatedListener = await AbsAudioPlayer.addListener('onMediaItemHistoryUpdated', this.onMediaItemHistoryUpdated)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.onMediaItemHistoryUpdatedListener) this.onMediaItemHistoryUpdatedListener.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -217,6 +217,132 @@ class AbsDatabaseWeb extends WebPlugin {
|
|||||||
localStorage.setItem('device', JSON.stringify(deviceData))
|
localStorage.setItem('device', JSON.stringify(deviceData))
|
||||||
return deviceData
|
return deviceData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getMediaItemHistory({ mediaId }) {
|
||||||
|
console.log('Get media item history', mediaId)
|
||||||
|
return {
|
||||||
|
id: mediaId,
|
||||||
|
mediaDisplayTitle: 'Test Book',
|
||||||
|
libraryItemId: mediaId,
|
||||||
|
episodeId: null,
|
||||||
|
isLocal: false,
|
||||||
|
serverConnectionConfigId: null,
|
||||||
|
serverAddress: null,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
name: 'Pause',
|
||||||
|
type: 'Playback',
|
||||||
|
description: null,
|
||||||
|
currentTime: 81,
|
||||||
|
serverSyncAttempted: true,
|
||||||
|
serverSyncSuccess: true,
|
||||||
|
serverSyncMessage: null,
|
||||||
|
timestamp: Date.now() - (1000 * 60 * 22) + 13000 // 22 mins ago + 13s
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Play',
|
||||||
|
type: 'Playback',
|
||||||
|
description: null,
|
||||||
|
currentTime: 68,
|
||||||
|
serverSyncAttempted: false,
|
||||||
|
serverSyncSuccess: null,
|
||||||
|
serverSyncMessage: null,
|
||||||
|
timestamp: Date.now() - (1000 * 60 * 22) // 22 mins ago
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Pause',
|
||||||
|
type: 'Playback',
|
||||||
|
description: null,
|
||||||
|
currentTime: 68,
|
||||||
|
serverSyncAttempted: true,
|
||||||
|
serverSyncSuccess: false,
|
||||||
|
serverSyncMessage: null,
|
||||||
|
timestamp: Date.now() - (1000 * 60 * 60) + (58000) // 1 hour ago + 58s
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Save',
|
||||||
|
type: 'Playback',
|
||||||
|
description: null,
|
||||||
|
currentTime: 55,
|
||||||
|
serverSyncAttempted: true,
|
||||||
|
serverSyncSuccess: true,
|
||||||
|
serverSyncMessage: null,
|
||||||
|
timestamp: Date.now() - (1000 * 60 * 60) + (45000) // 1 hour ago + 45s
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Save',
|
||||||
|
type: 'Playback',
|
||||||
|
description: null,
|
||||||
|
currentTime: 40,
|
||||||
|
serverSyncAttempted: true,
|
||||||
|
serverSyncSuccess: true,
|
||||||
|
serverSyncMessage: null,
|
||||||
|
timestamp: Date.now() - (1000 * 60 * 60) + (30000) // 1 hour ago + 30s
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Save',
|
||||||
|
type: 'Playback',
|
||||||
|
description: null,
|
||||||
|
currentTime: 25,
|
||||||
|
serverSyncAttempted: true,
|
||||||
|
serverSyncSuccess: true,
|
||||||
|
serverSyncMessage: null,
|
||||||
|
timestamp: Date.now() - (1000 * 60 * 60) + (15000) // 1 hour ago + 15s
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Play',
|
||||||
|
type: 'Playback',
|
||||||
|
description: null,
|
||||||
|
currentTime: 10,
|
||||||
|
serverSyncAttempted: false,
|
||||||
|
serverSyncSuccess: null,
|
||||||
|
serverSyncMessage: null,
|
||||||
|
timestamp: Date.now() - (1000 * 60 * 60) // 1 hour ago
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Stop',
|
||||||
|
type: 'Playback',
|
||||||
|
description: null,
|
||||||
|
currentTime: 10,
|
||||||
|
serverSyncAttempted: true,
|
||||||
|
serverSyncSuccess: true,
|
||||||
|
serverSyncMessage: null,
|
||||||
|
timestamp: Date.now() - (1000 * 60 * 60 * 25) + 10000 // 25 hours ago + 10s
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Seek',
|
||||||
|
type: 'Playback',
|
||||||
|
description: null,
|
||||||
|
currentTime: 6,
|
||||||
|
serverSyncAttempted: true,
|
||||||
|
serverSyncSuccess: true,
|
||||||
|
serverSyncMessage: null,
|
||||||
|
timestamp: Date.now() - (1000 * 60 * 60 * 25) + 2000 // 25 hours ago + 2s
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Play',
|
||||||
|
type: 'Playback',
|
||||||
|
description: null,
|
||||||
|
currentTime: 0,
|
||||||
|
serverSyncAttempted: false,
|
||||||
|
serverSyncSuccess: null,
|
||||||
|
serverSyncMessage: null,
|
||||||
|
timestamp: Date.now() - (1000 * 60 * 60 * 25) // 25 hours ago
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Play',
|
||||||
|
type: 'Playback',
|
||||||
|
description: null,
|
||||||
|
currentTime: 0,
|
||||||
|
serverSyncAttempted: false,
|
||||||
|
serverSyncSuccess: null,
|
||||||
|
serverSyncMessage: null,
|
||||||
|
timestamp: Date.now() - (1000 * 60 * 60 * 50) // 50 hours ago
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const AbsDatabase = registerPlugin('AbsDatabase', {
|
const AbsDatabase = registerPlugin('AbsDatabase', {
|
||||||
|
|||||||
@@ -86,6 +86,10 @@ class DbService {
|
|||||||
updateDeviceSettings(payload) {
|
updateDeviceSettings(payload) {
|
||||||
return AbsDatabase.updateDeviceSettings(payload)
|
return AbsDatabase.updateDeviceSettings(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMediaItemHistory(mediaId) {
|
||||||
|
return AbsDatabase.getMediaItemHistory({ mediaId })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ({ app, store }, inject) => {
|
export default ({ app, store }, inject) => {
|
||||||
|
|||||||
+13
-3
@@ -91,10 +91,10 @@ Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Vue.prototype.$secondsToTimestamp = (seconds) => {
|
Vue.prototype.$secondsToTimestamp = (seconds) => {
|
||||||
var _seconds = seconds
|
let _seconds = seconds
|
||||||
var _minutes = Math.floor(seconds / 60)
|
let _minutes = Math.floor(seconds / 60)
|
||||||
_seconds -= _minutes * 60
|
_seconds -= _minutes * 60
|
||||||
var _hours = Math.floor(_minutes / 60)
|
let _hours = Math.floor(_minutes / 60)
|
||||||
_minutes -= _hours * 60
|
_minutes -= _hours * 60
|
||||||
_seconds = Math.floor(_seconds)
|
_seconds = Math.floor(_seconds)
|
||||||
if (!_hours) {
|
if (!_hours) {
|
||||||
@@ -103,6 +103,16 @@ Vue.prototype.$secondsToTimestamp = (seconds) => {
|
|||||||
return `${_hours}:${_minutes.toString().padStart(2, '0')}:${_seconds.toString().padStart(2, '0')}`
|
return `${_hours}:${_minutes.toString().padStart(2, '0')}:${_seconds.toString().padStart(2, '0')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Vue.prototype.$secondsToTimestampFull = (seconds) => {
|
||||||
|
let _seconds = seconds
|
||||||
|
let _minutes = Math.floor(seconds / 60)
|
||||||
|
_seconds -= _minutes * 60
|
||||||
|
let _hours = Math.floor(_minutes / 60)
|
||||||
|
_minutes -= _hours * 60
|
||||||
|
_seconds = Math.floor(_seconds)
|
||||||
|
return `${_hours.toString().padStart(2, '0')}:${_minutes.toString().padStart(2, '0')}:${_seconds.toString().padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
Vue.prototype.$sanitizeFilename = (input, colonReplacement = ' - ') => {
|
Vue.prototype.$sanitizeFilename = (input, colonReplacement = ' - ') => {
|
||||||
if (typeof input !== 'string') {
|
if (typeof input !== 'string') {
|
||||||
return false
|
return false
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2
-1
@@ -5,7 +5,8 @@ module.exports = {
|
|||||||
options: {
|
options: {
|
||||||
safelist: [
|
safelist: [
|
||||||
'bg-success',
|
'bg-success',
|
||||||
'bg-info'
|
'bg-info',
|
||||||
|
'text-info'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user