diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt b/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt index c9b07091..749f8b3e 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt @@ -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) } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/MediaProgressSyncer.kt b/android/app/src/main/java/com/audiobookshelf/app/player/MediaProgressSyncer.kt index 0f7b41c8..60cfe8e1 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/MediaProgressSyncer.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/MediaProgressSyncer.kt @@ -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 } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt index 63b7130f..cf1d8222 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt @@ -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) data class LocalLibraryItemsPayload(val value:List) data class LocalFoldersPayload(val value:List) + 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 // diff --git a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt index 5fe9daf7..3fc50786 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt @@ -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) + @JsonIgnoreProperties(ignoreUnknown = true) + data class MediaProgressSyncResponsePayload(val numServerProgressUpdates:Int, val localProgressUpdates:List) + 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(it.toString()) + val playbackSession = jacksonObjectMapper().readValue(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(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) + } + } } diff --git a/layouts/default.vue b/layouts/default.vue index bc4d196d..760da6ff 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -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() { diff --git a/plugins/capacitor/AbsDatabase.js b/plugins/capacitor/AbsDatabase.js index e7448451..09541c23 100644 --- a/plugins/capacitor/AbsDatabase.js +++ b/plugins/capacitor/AbsDatabase.js @@ -166,6 +166,10 @@ class AbsDatabaseWeb extends WebPlugin { async removeLocalMediaProgress({ localMediaProgressId }) { return null } + + async syncLocalMediaProgressWithServer() { + return null + } } const AbsDatabase = registerPlugin('AbsDatabase', { diff --git a/plugins/db.js b/plugins/db.js index 37a43db9..3bc2a860 100644 --- a/plugins/db.js +++ b/plugins/db.js @@ -88,6 +88,10 @@ class DbService { removeLocalMediaProgress(localMediaProgressId) { return AbsDatabase.removeLocalMediaProgress({ localMediaProgressId }) } + + syncLocalMediaProgressWithServer() { + return AbsDatabase.syncLocalMediaProgressWithServer() + } } export default ({ app, store }, inject) => {