Android add media progress sync for server sessions

This commit is contained in:
advplyr 2022-04-08 20:27:54 -05:00
parent 9ad351f0d7
commit 526fca98b9
10 changed files with 290 additions and 317 deletions

View file

@ -19,130 +19,6 @@ data class DeviceData(
var localLibraryItemIdPlaying:String? var localLibraryItemIdPlaying:String?
) )
@JsonIgnoreProperties(ignoreUnknown = true)
data class LocalLibraryItem(
var id:String,
var serverAddress:String?,
var libraryItemId:String?,
var folderId:String,
var basePath:String,
var absolutePath:String,
var contentUrl:String,
var isInvalid:Boolean,
var mediaType:String,
var media:MediaType,
var localFiles:MutableList<LocalFile>,
var coverContentUrl:String?,
var coverAbsolutePath:String?,
var isLocal:Boolean
) {
@JsonIgnore
fun getDuration():Double {
var total = 0.0
var audioTracks = media.getAudioTracks()
audioTracks.forEach{ total += it.duration }
return total
}
@JsonIgnore
fun updateFromScan(audioTracks:MutableList<AudioTrack>, _localFiles:MutableList<LocalFile>) {
media.setAudioTracks(audioTracks)
localFiles = _localFiles
if (coverContentUrl != null) {
if (localFiles.find { it.contentUrl == coverContentUrl } == null) {
// Cover was removed
coverContentUrl = null
coverAbsolutePath = null
media.coverPath = null
}
}
}
@JsonIgnore
fun getPlaybackSession():PlaybackSession {
var sessionId = "play-${UUID.randomUUID()}"
var mediaMetadata = media.metadata
var chapters = if (mediaType == "book") (media as Book).chapters else mutableListOf()
var authorName = "Unknown"
if (mediaType == "book") {
var bookMetadata = mediaMetadata as BookMetadata
authorName = bookMetadata?.authorName ?: "Unknown"
}
return PlaybackSession(sessionId,null,null,null, mediaType, mediaMetadata, chapters, mediaMetadata.title, authorName,null,getDuration(),PLAYMETHOD_LOCAL, media.getAudioTracks() as MutableList<AudioTrack>,0.0,null,this,null,null)
}
@JsonIgnore
fun removeLocalFile(localFileId:String) {
localFiles.removeIf { it.id == localFileId }
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
data class LocalMediaItem(
var id:String,
var serverAddress:String?,
var name: String,
var mediaType:String,
var folderId:String,
var contentUrl:String,
var simplePath: String,
var basePath:String,
var absolutePath:String,
var audioTracks:MutableList<AudioTrack>,
var localFiles:MutableList<LocalFile>,
var coverContentUrl:String?,
var coverAbsolutePath:String?
) {
@JsonIgnore
fun getDuration():Double {
var total = 0.0
audioTracks.forEach{ total += it.duration }
return total
}
@JsonIgnore
fun getTotalSize():Long {
var total = 0L
localFiles.forEach { total += it.size }
return total
}
@JsonIgnore
fun getMediaMetadata():MediaTypeMetadata {
return if (mediaType == "book") {
BookMetadata(name,null, mutableListOf(), mutableListOf(), mutableListOf(),null,null,null,null,null,null,null,false,null,null,null,null)
} else {
PodcastMetadata(name,null,null, mutableListOf())
}
}
@JsonIgnore
fun getAudiobookChapters():List<BookChapter> {
if (mediaType != "book" || audioTracks.isEmpty()) return mutableListOf()
if (audioTracks.size == 1) { // Single track audiobook look for chapters from ffprobe
return audioTracks[0].audioProbeResult?.getBookChapters() ?: mutableListOf()
}
// Multi-track make chapters from tracks
return audioTracks.map { it.getBookChapter() }
}
@JsonIgnore
fun getLocalLibraryItem():LocalLibraryItem {
var mediaMetadata = getMediaMetadata()
if (mediaType == "book") {
var chapters = getAudiobookChapters()
var book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,getTotalSize(),getDuration())
return LocalLibraryItem(id,serverAddress, null, folderId, basePath,absolutePath, contentUrl, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true)
} else {
var podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false)
return LocalLibraryItem(id,serverAddress, null, folderId, basePath,absolutePath, contentUrl, false, mediaType, podcast,localFiles,coverContentUrl, coverAbsolutePath, true)
}
}
}
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class LocalFile( data class LocalFile(
var id:String, var id:String,

View file

@ -0,0 +1,65 @@
package com.audiobookshelf.app.data
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import java.util.*
@JsonIgnoreProperties(ignoreUnknown = true)
data class LocalLibraryItem(
var id:String,
var serverAddress:String?,
var libraryItemId:String?,
var folderId:String,
var basePath:String,
var absolutePath:String,
var contentUrl:String,
var isInvalid:Boolean,
var mediaType:String,
var media:MediaType,
var localFiles:MutableList<LocalFile>,
var coverContentUrl:String?,
var coverAbsolutePath:String?,
var isLocal:Boolean
) {
@JsonIgnore
fun getDuration():Double {
var total = 0.0
var audioTracks = media.getAudioTracks()
audioTracks.forEach{ total += it.duration }
return total
}
@JsonIgnore
fun updateFromScan(audioTracks:MutableList<AudioTrack>, _localFiles:MutableList<LocalFile>) {
media.setAudioTracks(audioTracks)
localFiles = _localFiles
if (coverContentUrl != null) {
if (localFiles.find { it.contentUrl == coverContentUrl } == null) {
// Cover was removed
coverContentUrl = null
coverAbsolutePath = null
media.coverPath = null
}
}
}
@JsonIgnore
fun getPlaybackSession():PlaybackSession {
var sessionId = "play-${UUID.randomUUID()}"
var mediaMetadata = media.metadata
var chapters = if (mediaType == "book") (media as Book).chapters else mutableListOf()
var authorName = "Unknown"
if (mediaType == "book") {
var bookMetadata = mediaMetadata as BookMetadata
authorName = bookMetadata?.authorName ?: "Unknown"
}
return PlaybackSession(sessionId,null,null,null, mediaType, mediaMetadata, chapters, mediaMetadata.title, authorName,null,getDuration(),PLAYMETHOD_LOCAL, media.getAudioTracks() as MutableList<AudioTrack>,0.0,null,this,null,null)
}
@JsonIgnore
fun removeLocalFile(localFileId:String) {
localFiles.removeIf { it.id == localFileId }
}
}

View file

@ -0,0 +1,72 @@
package com.audiobookshelf.app.data
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
/*
Used as a helper class to generate LocalLibraryItem from scan results
*/
@JsonIgnoreProperties(ignoreUnknown = true)
data class LocalMediaItem(
var id:String,
var serverAddress:String?,
var name: String,
var mediaType:String,
var folderId:String,
var contentUrl:String,
var simplePath: String,
var basePath:String,
var absolutePath:String,
var audioTracks:MutableList<AudioTrack>,
var localFiles:MutableList<LocalFile>,
var coverContentUrl:String?,
var coverAbsolutePath:String?
) {
@JsonIgnore
fun getDuration():Double {
var total = 0.0
audioTracks.forEach{ total += it.duration }
return total
}
@JsonIgnore
fun getTotalSize():Long {
var total = 0L
localFiles.forEach { total += it.size }
return total
}
@JsonIgnore
fun getMediaMetadata():MediaTypeMetadata {
return if (mediaType == "book") {
BookMetadata(name,null, mutableListOf(), mutableListOf(), mutableListOf(),null,null,null,null,null,null,null,false,null,null,null,null)
} else {
PodcastMetadata(name,null,null, mutableListOf())
}
}
@JsonIgnore
fun getAudiobookChapters():List<BookChapter> {
if (mediaType != "book" || audioTracks.isEmpty()) return mutableListOf()
if (audioTracks.size == 1) { // Single track audiobook look for chapters from ffprobe
return audioTracks[0].audioProbeResult?.getBookChapters() ?: mutableListOf()
}
// Multi-track make chapters from tracks
return audioTracks.map { it.getBookChapter() }
}
@JsonIgnore
fun getLocalLibraryItem():LocalLibraryItem {
var mediaMetadata = getMediaMetadata()
if (mediaType == "book") {
var chapters = getAudiobookChapters()
var book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,getTotalSize(),getDuration())
return LocalLibraryItem(id,serverAddress, null, folderId, basePath,absolutePath, contentUrl, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true)
} else {
var podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false)
return LocalLibraryItem(id,serverAddress, null, folderId, basePath,absolutePath, contentUrl, false, mediaType, podcast,localFiles,coverContentUrl, coverAbsolutePath, true)
}
}
}

View file

@ -0,0 +1,18 @@
package com.audiobookshelf.app.data
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class MediaProgress(
val id:String,
val libraryItemId:String,
val episodeId:String,
val duration:Double,
val progress:Double, // 0 to 1
val currentTime:Int,
val isFinished:Boolean,
val lastUpdate:Long,
val startedAt:Long,
val finishedAt:Long,
val isLocal:Boolean?
)

View file

@ -127,4 +127,9 @@ class PlaybackSession(
} }
return mediaItems return mediaItems
} }
@JsonIgnore
fun clone():PlaybackSession {
return PlaybackSession(id,userId,libraryItemId,episodeId,mediaType,mediaMetadata,chapters,displayTitle,displayAuthor,coverPath,duration,playMethod,audioTracks,currentTime,libraryItem,localLibraryItem,serverUrl,token)
}
} }

View file

@ -1,184 +0,0 @@
package com.audiobookshelf.app.player
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.audiobookshelf.app.device.DeviceManager
import com.getcapacitor.JSObject
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException
import java.util.*
import kotlin.concurrent.schedule
/*
* Normal progress sync is handled in webview, but when using android auto webview may not be open.
* If webview is not open sync progress every 5s. Webview can be closed at any time so interval is always set.
*/
class AudiobookProgressSyncer constructor(playerNotificationService:PlayerNotificationService, client: OkHttpClient) {
private val tag = "AudiobookProgressSync"
private val playerNotificationService:PlayerNotificationService = playerNotificationService
private val client:OkHttpClient = client
private var listeningTimerTask: TimerTask? = null
var listeningTimerRunning:Boolean = false
private var webviewOpenOnStart:Boolean = false
private var webviewClosedMidSession:Boolean = false
private var listeningBookTitle:String? = ""
private var listeningBookIsLocal:Boolean = false
private var listeningBookId:String? = ""
private var listeningStreamId:String? = ""
private var lastPlaybackTime:Long = 0
private var lastUpdateTime:Long = 0
fun start() {
if (listeningTimerRunning) {
Log.d(tag, "start: Timer already running for $listeningBookTitle")
if (playerNotificationService.getCurrentBookTitle() != listeningBookTitle) {
Log.d(tag, "start: Changed audiobook stream - resetting timer")
listeningTimerTask?.cancel()
}
}
listeningTimerRunning = true
webviewOpenOnStart = playerNotificationService.getIsWebviewOpen()
listeningBookTitle = playerNotificationService.getCurrentBookTitle()
listeningBookIsLocal = false
listeningBookId = "empty"
listeningStreamId = "empty"
lastPlaybackTime = playerNotificationService.getCurrentTime()
lastUpdateTime = System.currentTimeMillis() / 1000L
listeningTimerTask = Timer("ListeningTimer", false).schedule(0L, 5000L) {
Handler(Looper.getMainLooper()).post() {
// Webview was closed while android auto is open - switch to native sync
var isWebviewOpen = playerNotificationService.getIsWebviewOpen()
if (!isWebviewOpen && webviewOpenOnStart) {
Log.d(tag, "Listening Timer: webview closed Switching to native sync tracking")
webviewOpenOnStart = false
webviewClosedMidSession = true
lastUpdateTime = System.currentTimeMillis() / 1000L
} else if (isWebviewOpen && webviewClosedMidSession) {
Log.d(tag, "Listening Timer: webview re-opened Switching back to webview sync tracking")
webviewClosedMidSession = false
webviewOpenOnStart = true
lastUpdateTime = System.currentTimeMillis() / 1000L
}
if (!webviewOpenOnStart && playerNotificationService.currentPlayer.isPlaying) {
sync()
}
}
}
}
fun stop() {
if (!listeningTimerRunning) return
Log.d(tag, "stop: Stopping listening for $listeningBookTitle")
if (!webviewOpenOnStart) {
sync()
}
reset()
}
fun reset() {
listeningTimerTask?.cancel()
listeningTimerTask = null
listeningTimerRunning = false
listeningBookTitle = ""
listeningBookId = ""
listeningBookIsLocal = false
listeningStreamId = ""
}
fun sync() {
var currTime = System.currentTimeMillis() / 1000L
var elapsed = currTime - lastUpdateTime
lastUpdateTime = currTime
if (!listeningBookIsLocal) {
Log.d(tag, "ListeningTimer: Sending sync data to server: elapsed $elapsed | $listeningStreamId | $listeningBookId")
// Send sync data only for streaming books
var syncData: JSObject = JSObject()
syncData.put("timeListened", elapsed)
syncData.put("currentTime", playerNotificationService.getCurrentTime() / 1000)
syncData.put("streamId", listeningStreamId)
syncData.put("audiobookId", listeningBookId)
sendStreamSyncData(syncData) {
Log.d(tag, "Stream sync done")
}
} else if (listeningStreamId == "download") {
// TODO: Save downloaded audiobook progress & send to server if connected
Log.d(tag, "ListeningTimer: Is listening download")
// Send sync data only for local books
var syncData: JSObject = JSObject()
// var duration = playerNotificationService.getAudiobookDuration() / 1000
var duration = 1000
var currentTime = playerNotificationService.getCurrentTime() / 1000
syncData.put("totalDuration", duration)
syncData.put("currentTime", currentTime)
syncData.put("progress", if (duration > 0) (currentTime / duration) else 0)
syncData.put("isRead", false)
syncData.put("lastUpdate", System.currentTimeMillis())
syncData.put("audiobookId", listeningBookId)
sendLocalSyncData(syncData) {
Log.d(tag, "Local sync done")
}
}
}
fun sendLocalSyncData(payload:JSObject, cb: (() -> Unit)) {
var serverUrl = DeviceManager.serverAddress
var token = DeviceManager.token
if (serverUrl == "" || token == "") {
return
}
Log.d(tag, "Sync Local $serverUrl | $token")
var url = "$serverUrl/api/syncLocal"
sendServerRequest(url, token, payload, cb)
}
fun sendStreamSyncData(payload:JSObject, cb: (() -> Unit)) {
var serverUrl = DeviceManager.serverAddress
var token = DeviceManager.token
if (serverUrl == "" || token == "") {
return
}
Log.d(tag, "Sync Stream $serverUrl | $token")
var url = "$serverUrl/api/syncStream"
sendServerRequest(url, token, payload, cb)
}
fun sendServerRequest(url:String, token:String, payload:JSObject, cb: () -> Unit) {
val mediaType = "application/json; charset=utf-8".toMediaType()
val requestBody = payload.toString().toRequestBody(mediaType)
val request = Request.Builder().post(requestBody)
.url(url).addHeader("Authorization", "Bearer $token")
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.d(tag, "FAILURE TO CONNECT")
e.printStackTrace()
cb()
}
override fun onResponse(call: Call, response: Response) {
response.use {
if (!response.isSuccessful) throw IOException("Unexpected code $response")
cb()
}
}
})
}
}

View file

@ -0,0 +1,96 @@
package com.audiobookshelf.app.player
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.audiobookshelf.app.data.MediaProgress
import com.audiobookshelf.app.data.PlaybackSession
import com.audiobookshelf.app.server.ApiHandler
import java.util.*
import kotlin.concurrent.schedule
data class MediaProgressSyncData(
var timeListened:Long, // seconds
var duration:Double, // seconds
var currentTime:Double // seconds
)
class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, apiHandler: ApiHandler) {
private val tag = "MediaProgressSync"
private val playerNotificationService:PlayerNotificationService = playerNotificationService
private val apiHandler = apiHandler
private var listeningTimerTask: TimerTask? = null
var listeningTimerRunning:Boolean = false
private var lastSyncTime:Long = 0
var currentPlaybackSession: PlaybackSession? = null // copy of pb session currently syncing
// var currentMediaProgress: MediaProgress? = null
val currentDisplayTitle get() = currentPlaybackSession?.displayTitle ?: "Unset"
val currentIsLocal get() = currentPlaybackSession?.isLocal == true
val currentSessionId get() = currentPlaybackSession?.id ?: ""
val currentPlaybackDuration get() = currentPlaybackSession?.duration ?: 0.0
fun start() {
if (listeningTimerRunning) {
Log.d(tag, "start: Timer already running for $currentDisplayTitle")
if (playerNotificationService.getCurrentPlaybackSessionId() != currentSessionId) {
Log.d(tag, "Playback session changed, reset timer")
listeningTimerTask?.cancel()
lastSyncTime = 0L
} else {
return
}
}
listeningTimerRunning = true
lastSyncTime = System.currentTimeMillis()
currentPlaybackSession = playerNotificationService.getCurrentPlaybackSessionCopy()
listeningTimerTask = Timer("ListeningTimer", false).schedule(0L, 5000L) {
Handler(Looper.getMainLooper()).post() {
if (playerNotificationService.currentPlayer.isPlaying) {
var currentTime = playerNotificationService.getCurrentTimeSeconds()
sync(currentTime)
}
}
}
}
fun stop() {
if (!listeningTimerRunning) return
Log.d(tag, "stop: Stopping listening for $currentDisplayTitle")
var currentTime = playerNotificationService.getCurrentTimeSeconds()
sync(currentTime)
reset()
}
fun sync(currentTime:Double) {
var diffSinceLastSync = System.currentTimeMillis() - lastSyncTime
if (diffSinceLastSync < 1000L) {
return
}
var listeningTimeToAdd = diffSinceLastSync / 1000L
lastSyncTime = System.currentTimeMillis()
var syncData = MediaProgressSyncData(listeningTimeToAdd,currentPlaybackDuration,currentTime)
if (currentIsLocal) {
// TODO: Save local progress sync
} else {
apiHandler.sendProgressSync(currentSessionId,syncData) {
Log.d(tag, "Progress sync data sent to server $currentDisplayTitle for time $currentTime")
}
}
}
fun reset() {
listeningTimerTask?.cancel()
listeningTimerTask = null
listeningTimerRunning = false
currentPlaybackSession = null
lastSyncTime = 0L
}
}

View file

@ -24,6 +24,7 @@ import androidx.media.utils.MediaConstants
import com.audiobookshelf.app.Audiobook import com.audiobookshelf.app.Audiobook
import com.audiobookshelf.app.AudiobookManager import com.audiobookshelf.app.AudiobookManager
import com.audiobookshelf.app.data.PlaybackSession import com.audiobookshelf.app.data.PlaybackSession
import com.audiobookshelf.app.server.ApiHandler
import com.getcapacitor.Bridge import com.getcapacitor.Bridge
import com.getcapacitor.JSObject import com.getcapacitor.JSObject
import com.google.android.exoplayer2.* import com.google.android.exoplayer2.*
@ -69,6 +70,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
private lateinit var mediaSession: MediaSessionCompat private lateinit var mediaSession: MediaSessionCompat
private lateinit var transportControls:MediaControllerCompat.TransportControls private lateinit var transportControls:MediaControllerCompat.TransportControls
private lateinit var audiobookManager: AudiobookManager private lateinit var audiobookManager: AudiobookManager
lateinit var apiHandler: ApiHandler
lateinit var mPlayer: SimpleExoPlayer lateinit var mPlayer: SimpleExoPlayer
lateinit var currentPlayer:Player lateinit var currentPlayer:Player
@ -76,7 +78,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
lateinit var sleepTimerManager:SleepTimerManager lateinit var sleepTimerManager:SleepTimerManager
lateinit var castManager:CastManager lateinit var castManager:CastManager
lateinit var audiobookProgressSyncer:AudiobookProgressSyncer lateinit var mediaProgressSyncer:MediaProgressSyncer
private var notificationId = 10; private var notificationId = 10;
private var channelId = "audiobookshelf_channel" private var channelId = "audiobookshelf_channel"
@ -195,7 +197,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
playerNotificationManager.setPlayer(null) playerNotificationManager.setPlayer(null)
mPlayer.release() mPlayer.release()
mediaSession.release() mediaSession.release()
audiobookProgressSyncer.reset() mediaProgressSyncer.reset()
Log.d(tag, "onDestroy") Log.d(tag, "onDestroy")
isStarted = false isStarted = false
@ -236,14 +238,17 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
var client: OkHttpClient = OkHttpClient() var client: OkHttpClient = OkHttpClient()
// Initialize API
apiHandler = ApiHandler(ctx)
// Initialize sleep timer // Initialize sleep timer
sleepTimerManager = SleepTimerManager(this) sleepTimerManager = SleepTimerManager(this)
// Initialize Cast Manager // Initialize Cast Manager
castManager = CastManager(this) castManager = CastManager(this)
// Initialize Audiobook Progress Syncer (Only used for android auto when webview is not open) // Initialize Media Progress Syncer
audiobookProgressSyncer = AudiobookProgressSyncer(this, client) mediaProgressSyncer = MediaProgressSyncer(this, apiHandler)
// Initialize shake sensor // Initialize shake sensor
Log.d(tag, "onCreate Register sensor listener ${mAccelerometer?.isWakeUpSensor}") Log.d(tag, "onCreate Register sensor listener ${mAccelerometer?.isWakeUpSensor}")
@ -601,13 +606,12 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
} }
} else lastPauseTime = System.currentTimeMillis() } else lastPauseTime = System.currentTimeMillis()
// If app is only running in android auto then webview will not be open // Start/stop progress sync interval
// so progress needs to be synced natively
Log.d(tag, "Playing ${getCurrentBookTitle()} | ${currentPlayer.mediaMetadata.title} | ${currentPlayer.mediaMetadata.displayTitle}") Log.d(tag, "Playing ${getCurrentBookTitle()} | ${currentPlayer.mediaMetadata.title} | ${currentPlayer.mediaMetadata.displayTitle}")
if (player.isPlaying) { if (player.isPlaying) {
audiobookProgressSyncer.start() mediaProgressSyncer.start()
} else { } else {
audiobookProgressSyncer.stop() mediaProgressSyncer.stop()
} }
clientEventEmitter?.onPlayingUpdate(player.isPlaying) clientEventEmitter?.onPlayingUpdate(player.isPlaying)
@ -763,6 +767,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
} }
} }
fun getCurrentTimeSeconds() : Double {
return getCurrentTime() / 1000.0
}
fun getBufferedTime() : Long { fun getBufferedTime() : Long {
if (currentPlayer.mediaItemCount > 1) { if (currentPlayer.mediaItemCount > 1) {
var windowIndex = currentPlayer.currentWindowIndex var windowIndex = currentPlayer.currentWindowIndex
@ -785,6 +793,14 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
return currentPlaybackSession?.displayTitle return currentPlaybackSession?.displayTitle
} }
fun getCurrentPlaybackSessionCopy() :PlaybackSession? {
return currentPlaybackSession?.clone()
}
fun getCurrentPlaybackSessionId() :String? {
return currentPlaybackSession?.id
}
fun calcPauseSeekBackTime() : Long { fun calcPauseSeekBackTime() : Long {
if (lastPauseTime <= 0) return 0 if (lastPauseTime <= 0) return 0
var time: Long = System.currentTimeMillis() - lastPauseTime var time: Long = System.currentTimeMillis() - lastPauseTime

View file

@ -7,6 +7,7 @@ import com.audiobookshelf.app.data.Library
import com.audiobookshelf.app.data.LibraryItem import com.audiobookshelf.app.data.LibraryItem
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.player.MediaProgressSyncData
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import com.getcapacitor.JSArray import com.getcapacitor.JSArray
@ -119,4 +120,12 @@ class ApiHandler {
cb(playbackSession) cb(playbackSession)
} }
} }
fun sendProgressSync(sessionId:String, syncData: MediaProgressSyncData, cb: () -> Unit) {
var payload = JSObject(jacksonObjectMapper().writeValueAsString(syncData))
postRequest("/api/session/$sessionId/sync", payload) {
cb()
}
}
} }

View file

@ -26,7 +26,7 @@ export const getters = {
if (!state.user || !state.user.mediaProgress) return null if (!state.user || !state.user.mediaProgress) return null
return state.user.mediaProgress.find(li => { return state.user.mediaProgress.find(li => {
if (episodeId && li.episodeId !== episodeId) return false if (episodeId && li.episodeId !== episodeId) return false
return li.id == libraryItemId return li.libraryItemId == libraryItemId
}) })
}, },
getUserBookmarksForItem: (state) => (libraryItemId) => { getUserBookmarksForItem: (state) => (libraryItemId) => {