Add sync local media progress

This commit is contained in:
advplyr 2022-04-09 18:36:32 -05:00
parent d9e4469089
commit 12a153d423
7 changed files with 186 additions and 18 deletions

View file

@ -155,7 +155,6 @@ class PlaybackSession(
@JsonIgnore
fun getNewLocalMediaProgress():LocalMediaProgress {
var dateNow = System.currentTimeMillis()
return LocalMediaProgress(localMediaProgressId,localLibraryItemId,episodeId,getTotalDuration(),progress,currentTime,false,dateNow,dateNow,null,serverConnectionConfigId,serverAddress,userId,libraryItemId)
return LocalMediaProgress(localMediaProgressId,localLibraryItemId,episodeId,getTotalDuration(),progress,currentTime,false,updatedAt,startedAt,null,serverConnectionConfigId,serverAddress,userId,libraryItemId)
}
}

View file

@ -40,6 +40,7 @@ class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, a
Log.d(tag, "start: Timer already running for $currentDisplayTitle")
if (playerNotificationService.getCurrentPlaybackSessionId() != currentSessionId) {
Log.d(tag, "Playback session changed, reset timer")
currentLocalMediaProgress = null
listeningTimerTask?.cancel()
lastSyncTime = 0L
} else {
@ -85,9 +86,16 @@ class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, a
currentPlaybackSession?.let {
DeviceManager.dbManager.saveLocalPlaybackSession(it)
saveLocalProgress(it)
// Send sync to server also if connected to this server and local item belongs to this server
if (it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
apiHandler.sendLocalProgressSync(it) {
Log.d(tag, "Local progress sync data sent to server $currentDisplayTitle for time $currentTime")
}
}
}
} else {
apiHandler.sendProgressSync(currentSessionId,syncData) {
apiHandler.sendProgressSync(currentSessionId, syncData) {
Log.d(tag, "Progress sync data sent to server $currentDisplayTitle for time $currentTime")
}
}
@ -103,7 +111,7 @@ class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, a
}
} else {
currentLocalMediaProgress?.currentTime = playbackSession.currentTime
currentLocalMediaProgress?.lastUpdate = System.currentTimeMillis()
currentLocalMediaProgress?.lastUpdate = playbackSession.updatedAt
currentLocalMediaProgress?.progress = playbackSession.progress
}
currentLocalMediaProgress?.let {
@ -118,6 +126,7 @@ class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, a
listeningTimerTask = null
listeningTimerRunning = false
currentPlaybackSession = null
currentLocalMediaProgress = null
lastSyncTime = 0L
}
}

View file

@ -1,7 +1,9 @@
package com.audiobookshelf.app.data
import android.util.Log
import com.audiobookshelf.app.MainActivity
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.server.ApiHandler
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.getcapacitor.JSObject
import com.getcapacitor.Plugin
@ -17,10 +19,18 @@ import org.json.JSONObject
class AbsDatabase : Plugin() {
val tag = "AbsDatabase"
lateinit var mainActivity: MainActivity
lateinit var apiHandler: ApiHandler
data class LocalMediaProgressPayload(val value:List<LocalMediaProgress>)
data class LocalLibraryItemsPayload(val value:List<LocalLibraryItem>)
data class LocalFoldersPayload(val value:List<LocalFolder>)
override fun load() {
mainActivity = (activity as MainActivity)
apiHandler = ApiHandler(mainActivity)
}
@PluginMethod
fun getDeviceData(call:PluginCall) {
GlobalScope.launch(Dispatchers.IO) {
@ -166,7 +176,6 @@ class AbsDatabase : Plugin() {
}
}
@PluginMethod
fun getAllLocalMediaProgress(call:PluginCall) {
GlobalScope.launch(Dispatchers.IO) {
@ -182,6 +191,17 @@ class AbsDatabase : Plugin() {
call.resolve()
}
@PluginMethod
fun syncLocalMediaProgressWithServer(call:PluginCall) {
if (DeviceManager.serverConnectionConfig == null) {
Log.e(tag, "syncLocalMediaProgressWithServer not connected to server")
return call.resolve()
}
apiHandler.syncMediaProgress {
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(it)))
}
}
//
// Generic Webview calls to db
//

View file

@ -2,12 +2,17 @@ package com.audiobookshelf.app.server
import android.content.Context
import android.content.SharedPreferences
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.util.Log
import androidx.core.content.ContextCompat.getSystemService
import com.audiobookshelf.app.data.Library
import com.audiobookshelf.app.data.LibraryItem
import com.audiobookshelf.app.data.LocalMediaProgress
import com.audiobookshelf.app.data.PlaybackSession
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.player.MediaProgressSyncData
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.getcapacitor.JSArray
@ -17,12 +22,18 @@ import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException
class ApiHandler {
val tag = "ApiHandler"
private var client = OkHttpClient()
var ctx: Context
var storageSharedPreferences: SharedPreferences? = null
data class LocalMediaProgressSyncPayload(val localMediaProgress:List<LocalMediaProgress>)
@JsonIgnoreProperties(ignoreUnknown = true)
data class MediaProgressSyncResponsePayload(val numServerProgressUpdates:Int, val localProgressUpdates:List<LocalMediaProgress>)
data class LocalMediaProgressSyncResultsPayload(var numLocalMediaProgressForServer:Int, var numServerProgressUpdates:Int, var numLocalProgressUpdates:Int)
constructor(_ctx: Context) {
ctx = _ctx
}
@ -43,6 +54,26 @@ class ApiHandler {
makeRequest(request, cb)
}
fun isOnline(): Boolean {
val connectivityManager = ctx.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (connectivityManager != null) {
val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
if (capabilities != null) {
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
Log.i("Internet", "NetworkCapabilities.TRANSPORT_CELLULAR")
return true
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
Log.i("Internet", "NetworkCapabilities.TRANSPORT_WIFI")
return true
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
Log.i("Internet", "NetworkCapabilities.TRANSPORT_ETHERNET")
return true
}
}
}
return false
}
fun makeRequest(request:Request, cb: (JSObject) -> Unit) {
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
@ -53,16 +84,21 @@ class ApiHandler {
override fun onResponse(call: Call, response: Response) {
response.use {
if (!response.isSuccessful) throw IOException("Unexpected code $response")
var bodyString = response.body!!.string()
var jsonObj = JSObject()
if (bodyString.startsWith("[")) {
var array = JSArray(bodyString)
jsonObj.put("value", array)
if (!it.isSuccessful) throw IOException("Unexpected code $response")
var bodyString = it.body!!.string()
if (bodyString == "OK") {
cb(JSObject())
} else {
jsonObj = JSObject(bodyString)
var jsonObj = JSObject()
if (bodyString.startsWith("[")) {
var array = JSArray(bodyString)
jsonObj.put("value", array)
} else {
jsonObj = JSObject(bodyString)
}
cb(jsonObj)
}
cb(jsonObj)
}
}
})
@ -105,7 +141,6 @@ class ApiHandler {
}
fun playLibraryItem(libraryItemId:String, episodeId:String, forceTranscode:Boolean, cb: (PlaybackSession) -> Unit) {
val mapper = jacksonObjectMapper()
var payload = JSObject()
payload.put("mediaPlayer", "exo-player")
@ -118,7 +153,7 @@ class ApiHandler {
postRequest(endpoint, payload) {
it.put("serverConnectionConfigId", DeviceManager.serverConnectionConfig?.id)
it.put("serverAddress", DeviceManager.serverAddress)
val playbackSession = mapper.readValue<PlaybackSession>(it.toString())
val playbackSession = jacksonObjectMapper().readValue<PlaybackSession>(it.toString())
cb(playbackSession)
}
}
@ -130,4 +165,56 @@ class ApiHandler {
cb()
}
}
fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: () -> Unit) {
var payload = JSObject(jacksonObjectMapper().writeValueAsString(playbackSession))
postRequest("/api/session/local", payload) {
cb()
}
}
fun syncMediaProgress(cb: (LocalMediaProgressSyncResultsPayload) -> Unit) {
if (!isOnline()) {
Log.d(tag, "Error not online")
cb(LocalMediaProgressSyncResultsPayload(0,0,0))
return
}
// Get all local media progress connected to items on the current connected server
var localMediaProgress = DeviceManager.dbManager.getAllLocalMediaProgress().filter {
it.serverConnectionConfigId == DeviceManager.serverConnectionConfig?.id
}
var localSyncResultsPayload = LocalMediaProgressSyncResultsPayload(localMediaProgress.size,0, 0)
if (localMediaProgress.isNotEmpty()) {
Log.d(tag, "Sending sync local progress request with ${localMediaProgress.size} progress items")
var payload = JSObject(jacksonObjectMapper().writeValueAsString(LocalMediaProgressSyncPayload(localMediaProgress)))
postRequest("/api/me/sync-local-progress", payload) {
Log.d(tag, "Media Progress Sync payload $payload - response ${it.toString()}")
if (it.toString() == "{}") {
Log.e(tag, "Progress sync received empty object")
} else {
val progressSyncResponsePayload = jacksonObjectMapper().readValue<MediaProgressSyncResponsePayload>(it.toString())
localSyncResultsPayload.numLocalProgressUpdates = progressSyncResponsePayload.localProgressUpdates.size
localSyncResultsPayload.numServerProgressUpdates = progressSyncResponsePayload.numServerProgressUpdates
Log.d(tag, "Media Progress Sync | Local Updates: $localSyncResultsPayload")
if (progressSyncResponsePayload.localProgressUpdates.isNotEmpty()) {
// Update all local media progress
progressSyncResponsePayload.localProgressUpdates.forEach { localMediaProgress ->
DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
}
}
}
cb(localSyncResultsPayload)
}
} else {
Log.d(tag, "No local media progress to sync")
cb(localSyncResultsPayload)
}
}
}

View file

@ -19,19 +19,37 @@ export default {
data() {
return {
attemptingConnection: false,
inittingLibraries: false
inittingLibraries: false,
hasMounted: false,
disconnectTime: 0
}
},
watch: {
networkConnected: {
handler(newVal, oldVal) {
if (!this.hasMounted) {
// watcher runs before mount, handling libraries/connection should be handled in mount
return
}
if (newVal) {
console.log(`[default] network connected changed ${oldVal} -> ${newVal}`)
if (!this.user) {
this.attemptConnection()
} else if (!this.currentLibraryId) {
this.initLibraries()
} else {
var timeSinceDisconnect = Date.now() - this.disconnectTime
if (timeSinceDisconnect > 5000) {
console.log('Time since disconnect was', timeSinceDisconnect, 'sync with server')
setTimeout(() => {
// TODO: Some issue here
this.syncLocalMediaProgress()
}, 4000)
}
}
} else {
console.log(`[default] lost network connection`)
this.disconnectTime = Date.now()
}
}
}
@ -156,8 +174,9 @@ export default {
}
},
async attemptConnection() {
console.warn('[default] attemptConnection')
if (!this.networkConnected) {
console.warn('No network connection')
console.warn('[default] No network connection')
return
}
if (this.attemptingConnection) {
@ -200,7 +219,7 @@ export default {
this.$socket.connect(serverConnectionConfig.address, serverConnectionConfig.token)
console.log('Successful connection on last saved connection config', JSON.stringify(serverConnectionConfig))
console.log('[default] Successful connection on last saved connection config', JSON.stringify(serverConnectionConfig))
await this.initLibraries()
this.attemptingConnection = false
},
@ -232,6 +251,29 @@ export default {
this.$eventBus.$emit('library-changed')
this.$store.dispatch('libraries/fetch', this.currentLibraryId)
this.inittingLibraries = false
},
async syncLocalMediaProgress() {
if (!this.user) {
console.log('[default] No need to sync local media progress - not connected to server')
return
}
console.log('[default] Calling syncLocalMediaProgress')
var response = await this.$db.syncLocalMediaProgressWithServer()
if (!response) {
this.$toast.error('Failed to sync local media with server')
return
}
const { numLocalMediaProgressForServer, numServerProgressUpdates, numLocalProgressUpdates } = response
if (numLocalMediaProgressForServer > 0) {
if (numServerProgressUpdates > 0 || numLocalProgressUpdates > 0) {
console.log(`[default] ${numServerProgressUpdates} Server progress updates | ${numLocalProgressUpdates} Local progress updates`)
} else {
console.log('[default] syncLocalMediaProgress No updates were necessary')
}
} else {
console.log('[default] syncLocalMediaProgress No local media progress to sync')
}
}
},
async mounted() {
@ -257,9 +299,12 @@ export default {
await this.attemptConnection()
}
console.log(`[default] finished connection attempt or already connected ${!!this.user}`)
await this.syncLocalMediaProgress()
this.$store.dispatch('globals/loadLocalMediaProgress')
this.checkForUpdate()
this.loadSavedSettings()
this.hasMounted = true
}
},
beforeDestroy() {

View file

@ -166,6 +166,10 @@ class AbsDatabaseWeb extends WebPlugin {
async removeLocalMediaProgress({ localMediaProgressId }) {
return null
}
async syncLocalMediaProgressWithServer() {
return null
}
}
const AbsDatabase = registerPlugin('AbsDatabase', {

View file

@ -88,6 +88,10 @@ class DbService {
removeLocalMediaProgress(localMediaProgressId) {
return AbsDatabase.removeLocalMediaProgress({ localMediaProgressId })
}
syncLocalMediaProgressWithServer() {
return AbsDatabase.syncLocalMediaProgressWithServer()
}
}
export default ({ app, store }, inject) => {