mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-08-04 10:04:39 +02:00
Add sync local media progress
This commit is contained in:
parent
d9e4469089
commit
12a153d423
7 changed files with 186 additions and 18 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
//
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -166,6 +166,10 @@ class AbsDatabaseWeb extends WebPlugin {
|
|||
async removeLocalMediaProgress({ localMediaProgressId }) {
|
||||
return null
|
||||
}
|
||||
|
||||
async syncLocalMediaProgressWithServer() {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const AbsDatabase = registerPlugin('AbsDatabase', {
|
||||
|
|
|
@ -88,6 +88,10 @@ class DbService {
|
|||
removeLocalMediaProgress(localMediaProgressId) {
|
||||
return AbsDatabase.removeLocalMediaProgress({ localMediaProgressId })
|
||||
}
|
||||
|
||||
syncLocalMediaProgressWithServer() {
|
||||
return AbsDatabase.syncLocalMediaProgressWithServer()
|
||||
}
|
||||
}
|
||||
|
||||
export default ({ app, store }, inject) => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue