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 @JsonIgnore
fun getNewLocalMediaProgress():LocalMediaProgress { fun getNewLocalMediaProgress():LocalMediaProgress {
var dateNow = System.currentTimeMillis() return LocalMediaProgress(localMediaProgressId,localLibraryItemId,episodeId,getTotalDuration(),progress,currentTime,false,updatedAt,startedAt,null,serverConnectionConfigId,serverAddress,userId,libraryItemId)
return LocalMediaProgress(localMediaProgressId,localLibraryItemId,episodeId,getTotalDuration(),progress,currentTime,false,dateNow,dateNow,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") Log.d(tag, "start: Timer already running for $currentDisplayTitle")
if (playerNotificationService.getCurrentPlaybackSessionId() != currentSessionId) { if (playerNotificationService.getCurrentPlaybackSessionId() != currentSessionId) {
Log.d(tag, "Playback session changed, reset timer") Log.d(tag, "Playback session changed, reset timer")
currentLocalMediaProgress = null
listeningTimerTask?.cancel() listeningTimerTask?.cancel()
lastSyncTime = 0L lastSyncTime = 0L
} else { } else {
@ -85,6 +86,13 @@ class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, a
currentPlaybackSession?.let { currentPlaybackSession?.let {
DeviceManager.dbManager.saveLocalPlaybackSession(it) DeviceManager.dbManager.saveLocalPlaybackSession(it)
saveLocalProgress(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 { } else {
apiHandler.sendProgressSync(currentSessionId, syncData) { apiHandler.sendProgressSync(currentSessionId, syncData) {
@ -103,7 +111,7 @@ class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, a
} }
} else { } else {
currentLocalMediaProgress?.currentTime = playbackSession.currentTime currentLocalMediaProgress?.currentTime = playbackSession.currentTime
currentLocalMediaProgress?.lastUpdate = System.currentTimeMillis() currentLocalMediaProgress?.lastUpdate = playbackSession.updatedAt
currentLocalMediaProgress?.progress = playbackSession.progress currentLocalMediaProgress?.progress = playbackSession.progress
} }
currentLocalMediaProgress?.let { currentLocalMediaProgress?.let {
@ -118,6 +126,7 @@ class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, a
listeningTimerTask = null listeningTimerTask = null
listeningTimerRunning = false listeningTimerRunning = false
currentPlaybackSession = null currentPlaybackSession = null
currentLocalMediaProgress = null
lastSyncTime = 0L lastSyncTime = 0L
} }
} }

View file

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

View file

@ -2,12 +2,17 @@ package com.audiobookshelf.app.server
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.util.Log import android.util.Log
import androidx.core.content.ContextCompat.getSystemService
import com.audiobookshelf.app.data.Library import com.audiobookshelf.app.data.Library
import com.audiobookshelf.app.data.LibraryItem import com.audiobookshelf.app.data.LibraryItem
import com.audiobookshelf.app.data.LocalMediaProgress
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.audiobookshelf.app.player.MediaProgressSyncData
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
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
@ -17,12 +22,18 @@ import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException import java.io.IOException
class ApiHandler { class ApiHandler {
val tag = "ApiHandler" val tag = "ApiHandler"
private var client = OkHttpClient() private var client = OkHttpClient()
var ctx: Context var ctx: Context
var storageSharedPreferences: SharedPreferences? = null 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) { constructor(_ctx: Context) {
ctx = _ctx ctx = _ctx
} }
@ -43,6 +54,26 @@ class ApiHandler {
makeRequest(request, cb) 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) { fun makeRequest(request:Request, cb: (JSObject) -> Unit) {
client.newCall(request).enqueue(object : Callback { client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) { override fun onFailure(call: Call, e: IOException) {
@ -53,8 +84,12 @@ class ApiHandler {
override fun onResponse(call: Call, response: Response) { override fun onResponse(call: Call, response: Response) {
response.use { response.use {
if (!response.isSuccessful) throw IOException("Unexpected code $response") if (!it.isSuccessful) throw IOException("Unexpected code $response")
var bodyString = response.body!!.string()
var bodyString = it.body!!.string()
if (bodyString == "OK") {
cb(JSObject())
} else {
var jsonObj = JSObject() var jsonObj = JSObject()
if (bodyString.startsWith("[")) { if (bodyString.startsWith("[")) {
var array = JSArray(bodyString) var array = JSArray(bodyString)
@ -65,6 +100,7 @@ class ApiHandler {
cb(jsonObj) cb(jsonObj)
} }
} }
}
}) })
} }
@ -105,7 +141,6 @@ class ApiHandler {
} }
fun playLibraryItem(libraryItemId:String, episodeId:String, forceTranscode:Boolean, cb: (PlaybackSession) -> Unit) { fun playLibraryItem(libraryItemId:String, episodeId:String, forceTranscode:Boolean, cb: (PlaybackSession) -> Unit) {
val mapper = jacksonObjectMapper()
var payload = JSObject() var payload = JSObject()
payload.put("mediaPlayer", "exo-player") payload.put("mediaPlayer", "exo-player")
@ -118,7 +153,7 @@ class ApiHandler {
postRequest(endpoint, payload) { postRequest(endpoint, payload) {
it.put("serverConnectionConfigId", DeviceManager.serverConnectionConfig?.id) it.put("serverConnectionConfigId", DeviceManager.serverConnectionConfig?.id)
it.put("serverAddress", DeviceManager.serverAddress) it.put("serverAddress", DeviceManager.serverAddress)
val playbackSession = mapper.readValue<PlaybackSession>(it.toString()) val playbackSession = jacksonObjectMapper().readValue<PlaybackSession>(it.toString())
cb(playbackSession) cb(playbackSession)
} }
} }
@ -130,4 +165,56 @@ class ApiHandler {
cb() 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,20 +19,38 @@ export default {
data() { data() {
return { return {
attemptingConnection: false, attemptingConnection: false,
inittingLibraries: false inittingLibraries: false,
hasMounted: false,
disconnectTime: 0
} }
}, },
watch: { watch: {
networkConnected: { networkConnected: {
handler(newVal, oldVal) { handler(newVal, oldVal) {
if (!this.hasMounted) {
// watcher runs before mount, handling libraries/connection should be handled in mount
return
}
if (newVal) { if (newVal) {
console.log(`[default] network connected changed ${oldVal} -> ${newVal}`) console.log(`[default] network connected changed ${oldVal} -> ${newVal}`)
if (!this.user) { if (!this.user) {
this.attemptConnection() this.attemptConnection()
} else if (!this.currentLibraryId) { } else if (!this.currentLibraryId) {
this.initLibraries() 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() { async attemptConnection() {
console.warn('[default] attemptConnection')
if (!this.networkConnected) { if (!this.networkConnected) {
console.warn('No network connection') console.warn('[default] No network connection')
return return
} }
if (this.attemptingConnection) { if (this.attemptingConnection) {
@ -200,7 +219,7 @@ export default {
this.$socket.connect(serverConnectionConfig.address, serverConnectionConfig.token) 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() await this.initLibraries()
this.attemptingConnection = false this.attemptingConnection = false
}, },
@ -232,6 +251,29 @@ export default {
this.$eventBus.$emit('library-changed') this.$eventBus.$emit('library-changed')
this.$store.dispatch('libraries/fetch', this.currentLibraryId) this.$store.dispatch('libraries/fetch', this.currentLibraryId)
this.inittingLibraries = false 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() { async mounted() {
@ -257,9 +299,12 @@ export default {
await this.attemptConnection() await this.attemptConnection()
} }
console.log(`[default] finished connection attempt or already connected ${!!this.user}`)
await this.syncLocalMediaProgress()
this.$store.dispatch('globals/loadLocalMediaProgress') this.$store.dispatch('globals/loadLocalMediaProgress')
this.checkForUpdate() this.checkForUpdate()
this.loadSavedSettings() this.loadSavedSettings()
this.hasMounted = true
} }
}, },
beforeDestroy() { beforeDestroy() {

View file

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

View file

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