mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-08-05 02:25:45 +02:00
MediaItemHistory and history page
This commit is contained in:
parent
b1805875b9
commit
297eca6a86
22 changed files with 651 additions and 98 deletions
|
@ -242,4 +242,12 @@ class DbManager {
|
|||
fun getLocalPlaybackSession(playbackSessionId:String):PlaybackSession? {
|
||||
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
|
||||
val localLibraryItemId get() = localLibraryItem?.id ?: ""
|
||||
@get:JsonIgnore
|
||||
val localMediaProgressId get() = if (episodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
|
||||
val localMediaProgressId get() = if (localEpisodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
|
||||
@get:JsonIgnore
|
||||
val progress get() = currentTime / getTotalDuration()
|
||||
@get:JsonIgnore
|
||||
val isLocalLibraryItemOnly get() = localLibraryItemId != "" && libraryItemId == null
|
||||
@get:JsonIgnore
|
||||
val mediaItemId get() = if (isLocalLibraryItemOnly) localMediaProgressId else if (episodeId.isNullOrEmpty()) libraryItemId ?: "" else "$libraryItemId-$episodeId"
|
||||
|
||||
@JsonIgnore
|
||||
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())
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ import com.audiobookshelf.app.data.LocalMediaProgress
|
|||
import com.audiobookshelf.app.data.MediaProgress
|
||||
import com.audiobookshelf.app.data.PlaybackSession
|
||||
import com.audiobookshelf.app.device.DeviceManager
|
||||
import com.audiobookshelf.app.media.MediaEventManager
|
||||
import com.audiobookshelf.app.server.ApiHandler
|
||||
import java.util.*
|
||||
import kotlin.concurrent.schedule
|
||||
|
@ -17,6 +18,12 @@ data class MediaProgressSyncData(
|
|||
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) {
|
||||
private val tag = "MediaProgressSync"
|
||||
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 currentLocalMediaProgress: LocalMediaProgress? = null
|
||||
|
||||
val currentDisplayTitle get() = currentPlaybackSession?.displayTitle ?: "Unset"
|
||||
val currentIsLocal get() = currentPlaybackSession?.isLocal == true
|
||||
private val currentDisplayTitle get() = currentPlaybackSession?.displayTitle ?: "Unset"
|
||||
private val currentIsLocal get() = currentPlaybackSession?.isLocal == true
|
||||
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) {
|
||||
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")
|
||||
currentLocalMediaProgress = null
|
||||
listeningTimerTask?.cancel()
|
||||
|
@ -48,25 +55,29 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||
} else {
|
||||
return
|
||||
}
|
||||
} else if (playerNotificationService.getCurrentPlaybackSessionId() != currentSessionId) {
|
||||
} else if (playbackSession.id != currentSessionId) {
|
||||
currentLocalMediaProgress = null
|
||||
}
|
||||
|
||||
listeningTimerRunning = true
|
||||
lastSyncTime = System.currentTimeMillis()
|
||||
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() {
|
||||
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 currentTime = playerNotificationService.getCurrentTimeSeconds()
|
||||
if (currentTime > 0) {
|
||||
sync(shouldSyncServer, currentTime) {
|
||||
sync(shouldSyncServer, currentTime) { syncResult ->
|
||||
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) {
|
||||
if (!listeningTimerRunning) return
|
||||
|
||||
listeningTimerTask?.cancel()
|
||||
listeningTimerTask = null
|
||||
listeningTimerRunning = false
|
||||
|
@ -83,11 +102,19 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||
|
||||
val currentTime = if (shouldSync == true) playerNotificationService.getCurrentTimeSeconds() else 0.0
|
||||
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()
|
||||
cb()
|
||||
}
|
||||
} else {
|
||||
currentPlaybackSession?.let { playbackSession ->
|
||||
MediaEventManager.stopEvent(playbackSession, null)
|
||||
}
|
||||
|
||||
reset()
|
||||
cb()
|
||||
}
|
||||
|
@ -95,6 +122,7 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||
|
||||
fun pause(cb: () -> Unit) {
|
||||
if (!listeningTimerRunning) return
|
||||
|
||||
listeningTimerTask?.cancel()
|
||||
listeningTimerTask = null
|
||||
listeningTimerRunning = false
|
||||
|
@ -103,33 +131,61 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||
|
||||
val currentTime = playerNotificationService.getCurrentTimeSeconds()
|
||||
if (currentTime > 0) { // Current time should always be > 0 on pause
|
||||
sync(true, currentTime) {
|
||||
sync(true, currentTime) { syncResult ->
|
||||
lastSyncTime = 0L
|
||||
Log.d(tag, "pause: Set last sync time 0 $lastSyncTime")
|
||||
failedSyncs = 0
|
||||
|
||||
currentPlaybackSession?.let { playbackSession ->
|
||||
MediaEventManager.pauseEvent(playbackSession, syncResult)
|
||||
}
|
||||
|
||||
cb()
|
||||
}
|
||||
} else {
|
||||
lastSyncTime = 0L
|
||||
Log.d(tag, "pause: Set last sync time 0 $lastSyncTime (current time < 0)")
|
||||
failedSyncs = 0
|
||||
|
||||
currentPlaybackSession?.let { playbackSession ->
|
||||
MediaEventManager.pauseEvent(playbackSession, null)
|
||||
}
|
||||
|
||||
cb()
|
||||
}
|
||||
}
|
||||
|
||||
fun finished(cb: () -> Unit) {
|
||||
if (!listeningTimerRunning) return
|
||||
|
||||
listeningTimerTask?.cancel()
|
||||
listeningTimerTask = null
|
||||
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()
|
||||
|
||||
currentPlaybackSession?.let { playbackSession ->
|
||||
MediaEventManager.finishedEvent(playbackSession, syncResult)
|
||||
}
|
||||
|
||||
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) {
|
||||
currentPlaybackSession?.let {
|
||||
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) {
|
||||
Log.e(tag, "Last sync time is not set $lastSyncTime")
|
||||
return
|
||||
return cb(null)
|
||||
}
|
||||
|
||||
val diffSinceLastSync = System.currentTimeMillis() - lastSyncTime
|
||||
if (diffSinceLastSync < 1000L) {
|
||||
return cb()
|
||||
return cb(null)
|
||||
}
|
||||
val listeningTimeToAdd = diffSinceLastSync / 1000L
|
||||
|
||||
|
@ -157,7 +213,7 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||
|
||||
if (currentPlaybackSession?.progress?.isNaN() == true) {
|
||||
Log.e(tag, "Current Playback Session invalid progress ${currentPlaybackSession?.progress} | Current Time: ${currentPlaybackSession?.currentTime} | Duration: ${currentPlaybackSession?.getTotalDuration()}")
|
||||
return cb()
|
||||
return cb(null)
|
||||
}
|
||||
|
||||
if (currentIsLocal) {
|
||||
|
@ -170,11 +226,12 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||
// 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
|
||||
if (shouldSyncServer && !it.libraryItemId.isNullOrEmpty() && it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
|
||||
apiHandler.sendLocalProgressSync(it) { syncSuccess ->
|
||||
apiHandler.sendLocalProgressSync(it) { syncSuccess, errorMsg ->
|
||||
Log.d(
|
||||
tag,
|
||||
"Local progress sync data sent to server $currentDisplayTitle for time $currentTime"
|
||||
)
|
||||
|
||||
if (syncSuccess) {
|
||||
failedSyncs = 0
|
||||
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")
|
||||
}
|
||||
|
||||
cb()
|
||||
cb(SyncResult(true, syncSuccess, errorMsg))
|
||||
}
|
||||
} else {
|
||||
cb()
|
||||
cb(SyncResult(false, null, null))
|
||||
}
|
||||
}
|
||||
} else if (shouldSyncServer) {
|
||||
apiHandler.sendProgressSync(currentSessionId, syncData) {
|
||||
if (it) {
|
||||
apiHandler.sendProgressSync(currentSessionId, syncData) { syncSuccess, errorMsg ->
|
||||
if (syncSuccess) {
|
||||
Log.d(tag, "Progress sync data sent to server $currentDisplayTitle for time $currentTime")
|
||||
failedSyncs = 0
|
||||
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")
|
||||
}
|
||||
cb()
|
||||
cb(SyncResult(true, syncSuccess, errorMsg))
|
||||
}
|
||||
} else {
|
||||
cb()
|
||||
cb(SyncResult(false, null, null))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
package com.audiobookshelf.app.player
|
||||
|
||||
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
|
||||
|
||||
|
@ -16,6 +18,7 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
|||
}
|
||||
|
||||
private var onSeekBack: Boolean = false
|
||||
private var isSeeking: Boolean = false
|
||||
|
||||
override fun onPlayerError(error: PlaybackException) {
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
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)) {
|
||||
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 {
|
||||
|
|
|
@ -74,6 +74,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
fun onProgressSyncFailing()
|
||||
fun onProgressSyncSuccess()
|
||||
fun onNetworkMeteredChanged(isUnmetered:Boolean)
|
||||
fun onMediaItemHistoryUpdated(mediaItemHistory:MediaItemHistory)
|
||||
}
|
||||
|
||||
private val tag = "PlayerService"
|
||||
|
@ -101,7 +102,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
private var channelId = "audiobookshelf_channel"
|
||||
private var channelName = "Audiobookshelf Channel"
|
||||
|
||||
private var currentPlaybackSession:PlaybackSession? = null
|
||||
var currentPlaybackSession:PlaybackSession? = null
|
||||
private var initialPlaybackRate:Float? = null
|
||||
|
||||
private var isAndroidAuto = false
|
||||
|
@ -682,7 +683,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
seekPlayer(playbackSession.currentTimeMs)
|
||||
// Should already be playing
|
||||
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
|
||||
mediaProgressSyncer.start()
|
||||
currentPlaybackSession?.let { mediaProgressSyncer.play(it) }
|
||||
clientEventEmitter?.onPlayingUpdate(true)
|
||||
}
|
||||
} else {
|
||||
|
@ -690,9 +691,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
if (seekBackTime > 0L) {
|
||||
seekBackward(seekBackTime)
|
||||
}
|
||||
|
||||
// Should already be playing
|
||||
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
|
||||
mediaProgressSyncer.start()
|
||||
currentPlaybackSession?.let { mediaProgressSyncer.play(it) }
|
||||
clientEventEmitter?.onPlayingUpdate(true)
|
||||
}
|
||||
}
|
||||
|
@ -722,7 +724,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
}
|
||||
|
||||
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
|
||||
mediaProgressSyncer.start()
|
||||
mediaProgressSyncer.play(it)
|
||||
clientEventEmitter?.onPlayingUpdate(true)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,9 @@ import android.util.Log
|
|||
import com.audiobookshelf.app.MainActivity
|
||||
import com.audiobookshelf.app.data.*
|
||||
import com.audiobookshelf.app.device.DeviceManager
|
||||
import com.audiobookshelf.app.media.MediaEventManager
|
||||
import com.audiobookshelf.app.player.CastManager
|
||||
import com.audiobookshelf.app.player.MediaProgressSyncer
|
||||
import com.audiobookshelf.app.player.PlayerNotificationService
|
||||
import com.audiobookshelf.app.server.ApiHandler
|
||||
import com.fasterxml.jackson.core.json.JsonReadFeature
|
||||
|
@ -88,7 +90,13 @@ class AbsAudioPlayer : Plugin() {
|
|||
override fun onNetworkMeteredChanged(isUnmetered:Boolean) {
|
||||
emit("onNetworkMeteredChanged", isUnmetered)
|
||||
}
|
||||
|
||||
override fun onMediaItemHistoryUpdated(mediaItemHistory:MediaItemHistory) {
|
||||
notifyListeners("onMediaItemHistoryUpdated", JSObject(jacksonMapper.writeValueAsString(mediaItemHistory)))
|
||||
}
|
||||
})
|
||||
|
||||
MediaEventManager.clientEventEmitter = playerNotificationService.clientEventEmitter
|
||||
}
|
||||
mainActivity.pluginCallback = foregroundServiceReady
|
||||
}
|
||||
|
@ -267,6 +275,7 @@ class AbsAudioPlayer : Plugin() {
|
|||
@PluginMethod
|
||||
fun seek(call: PluginCall) {
|
||||
val time:Int = call.getInt("value", 0) ?: 0 // Value in seconds
|
||||
Log.d(tag, "seek action to $time")
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
playerNotificationService.seekPlayer(time * 1000L) // convert to ms
|
||||
call.resolve()
|
||||
|
|
|
@ -406,4 +406,19 @@ class AbsDatabase : Plugin() {
|
|||
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))
|
||||
|
||||
postRequest("/api/session/$sessionId/sync", payload, null) {
|
||||
if (!it.getString("error").isNullOrEmpty()) {
|
||||
cb(false)
|
||||
cb(false, it.getString("error"))
|
||||
} 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))
|
||||
|
||||
postRequest("/api/session/local", payload, null) {
|
||||
if (!it.getString("error").isNullOrEmpty()) {
|
||||
cb(false)
|
||||
cb(false, it.getString("error"))
|
||||
} else {
|
||||
cb(true)
|
||||
cb(true, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
font-family: 'Material Icons';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(/fonts/MaterialIcons.woff2) format('woff2');
|
||||
src: url(/fonts/MaterialIcons-Regular.ttf) format('truetype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Material Icons Outlined';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(/fonts/MaterialIconsOutlined.woff2) format('woff2');
|
||||
src: url(/fonts/MaterialIconsOutlined-Regular.otf) format('opentype');
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
|
|
|
@ -367,6 +367,14 @@ export default {
|
|||
const items = []
|
||||
|
||||
if (!this.isPodcast) {
|
||||
// TODO: Implement on iOS
|
||||
if (!this.isIos) {
|
||||
items.push({
|
||||
text: 'History',
|
||||
value: 'history'
|
||||
})
|
||||
}
|
||||
|
||||
items.push({
|
||||
text: this.userIsFinished ? 'Mark as Not Finished' : 'Mark as Finished',
|
||||
value: 'markFinished'
|
||||
|
@ -401,6 +409,10 @@ export default {
|
|||
|
||||
if (width * this.bookCoverAspectRatio > 325) width = 325 / this.bookCoverAspectRatio
|
||||
return width
|
||||
},
|
||||
mediaId() {
|
||||
if (this.isPodcast) return null
|
||||
return this.serverLibraryItemId || this.localLibraryItemId
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -430,6 +442,8 @@ export default {
|
|||
} else if (action === 'markFinished') {
|
||||
if (this.isProcessingReadUpdate) return
|
||||
this.toggleFinished()
|
||||
} else if (action === 'history') {
|
||||
this.$router.push(`/media/${this.mediaId}/history?title=${this.title}`)
|
||||
}
|
||||
},
|
||||
moreButtonPress() {
|
153
pages/media/_id/history.vue
Normal file
153
pages/media/_id/history.vue
Normal file
|
@ -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))
|
||||
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', {
|
||||
|
|
|
@ -86,6 +86,10 @@ class DbService {
|
|||
updateDeviceSettings(payload) {
|
||||
return AbsDatabase.updateDeviceSettings(payload)
|
||||
}
|
||||
|
||||
getMediaItemHistory(mediaId) {
|
||||
return AbsDatabase.getMediaItemHistory({ mediaId })
|
||||
}
|
||||
}
|
||||
|
||||
export default ({ app, store }, inject) => {
|
||||
|
|
|
@ -91,10 +91,10 @@ Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true) => {
|
|||
}
|
||||
|
||||
Vue.prototype.$secondsToTimestamp = (seconds) => {
|
||||
var _seconds = seconds
|
||||
var _minutes = Math.floor(seconds / 60)
|
||||
let _seconds = seconds
|
||||
let _minutes = Math.floor(seconds / 60)
|
||||
_seconds -= _minutes * 60
|
||||
var _hours = Math.floor(_minutes / 60)
|
||||
let _hours = Math.floor(_minutes / 60)
|
||||
_minutes -= _hours * 60
|
||||
_seconds = Math.floor(_seconds)
|
||||
if (!_hours) {
|
||||
|
@ -103,6 +103,16 @@ Vue.prototype.$secondsToTimestamp = (seconds) => {
|
|||
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 = ' - ') => {
|
||||
if (typeof input !== 'string') {
|
||||
return false
|
||||
|
|
BIN
static/fonts/MaterialIcons-Regular.ttf
Normal file
BIN
static/fonts/MaterialIcons-Regular.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
static/fonts/MaterialIconsOutlined-Regular.otf
Normal file
BIN
static/fonts/MaterialIconsOutlined-Regular.otf
Normal file
Binary file not shown.
Binary file not shown.
|
@ -5,7 +5,8 @@ module.exports = {
|
|||
options: {
|
||||
safelist: [
|
||||
'bg-success',
|
||||
'bg-info'
|
||||
'bg-info',
|
||||
'text-info'
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue