From f215efdcd0b16e111a55c0537d0e9af4025653c7 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 5 Feb 2023 16:54:46 -0600 Subject: [PATCH] Update:Syncing local sessions rewrite to support offline sessions #381 --- .../audiobookshelf/app/data/DeviceClasses.kt | 2 +- .../app/data/LocalMediaProgress.kt | 6 +- .../app/data/PlaybackSession.kt | 4 +- .../app/device/DeviceManager.kt | 21 +++ .../audiobookshelf/app/managers/DbManager.kt | 24 ++- .../app/media/MediaEventManager.kt | 6 +- .../audiobookshelf/app/media/MediaManager.kt | 4 +- .../app/media/MediaProgressSyncer.kt | 27 ++- .../com/audiobookshelf/app/models/User.kt | 11 ++ .../app/player/PlayerNotificationService.kt | 19 ++- .../audiobookshelf/app/plugins/AbsDatabase.kt | 20 ++- .../audiobookshelf/app/server/ApiHandler.kt | 160 ++++++++---------- layouts/default.vue | 28 ++- pages/localMedia/folders/index.vue | 86 ---------- plugins/capacitor/AbsDatabase.js | 4 + plugins/db.js | 4 + store/index.js | 6 +- 17 files changed, 207 insertions(+), 225 deletions(-) create mode 100644 android/app/src/main/java/com/audiobookshelf/app/models/User.kt diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt b/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt index b858fb0e..fd9ea633 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt @@ -158,7 +158,7 @@ data class DeviceSettings( data class DeviceData( var serverConnectionConfigs:MutableList, var lastServerConnectionConfigId:String?, - var currentLocalPlaybackSession: PlaybackSession?, // Stored to open up where left off for local media + var currentLocalPlaybackSession: PlaybackSession?, // Stored to open up where left off for local media. TODO: Old var deviceSettings: DeviceSettings? ) { @JsonIgnore diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaProgress.kt b/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaProgress.kt index ceae4f43..7f177bd1 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaProgress.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaProgress.kt @@ -1,6 +1,5 @@ package com.audiobookshelf.app.data -import android.util.Log import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnoreProperties import kotlin.math.roundToInt @@ -33,6 +32,11 @@ class LocalMediaProgress( if (localEpisodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId" } + @JsonIgnore + fun isMatch(mediaProgress:MediaProgress):Boolean { + if (episodeId != null) return libraryItemId == mediaProgress.libraryItemId && episodeId == mediaProgress.episodeId + return libraryItemId == mediaProgress.libraryItemId + } @JsonIgnore fun updateIsFinished(finished:Boolean) { 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 6c08cfb9..0616efc8 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 @@ -7,7 +7,7 @@ import android.os.Build import android.provider.MediaStore import android.support.v4.media.MediaMetadataCompat import com.audiobookshelf.app.device.DeviceManager -import com.audiobookshelf.app.player.MediaProgressSyncData +import com.audiobookshelf.app.media.MediaProgressSyncData import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.google.android.exoplayer2.MediaItem @@ -254,7 +254,7 @@ class PlaybackSession( } @JsonIgnore - fun syncData(syncData:MediaProgressSyncData) { + fun syncData(syncData: MediaProgressSyncData) { timeListening += syncData.timeListened updatedAt = System.currentTimeMillis() currentTime = syncData.currentTime diff --git a/android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt b/android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt index 0948d02b..bd7b5f58 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt @@ -1,5 +1,8 @@ package com.audiobookshelf.app.device +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities import android.util.Log import com.audiobookshelf.app.data.* import com.audiobookshelf.app.managers.DbManager @@ -46,4 +49,22 @@ object DeviceManager { if (id == null) return null return deviceData.serverConnectionConfigs.find { it.id == id } } + + fun checkConnectivity(ctx:Context): Boolean { + val connectivityManager = ctx.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + 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 + } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/managers/DbManager.kt b/android/app/src/main/java/com/audiobookshelf/app/managers/DbManager.kt index 5dc7c234..be201946 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/managers/DbManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/managers/DbManager.kt @@ -237,18 +237,26 @@ class DbManager { } } - fun saveLocalPlaybackSession(playbackSession: PlaybackSession) { - Paper.book("localPlaybackSession").write(playbackSession.id,playbackSession) - } - fun getLocalPlaybackSession(playbackSessionId:String): PlaybackSession? { - return Paper.book("localPlaybackSession").read(playbackSessionId) - } - - fun saveMediaItemHistory(mediaItemHistory: MediaItemHistory) { Paper.book("mediaItemHistory").write(mediaItemHistory.id,mediaItemHistory) } fun getMediaItemHistory(id:String): MediaItemHistory? { return Paper.book("mediaItemHistory").read(id) } + + fun savePlaybackSession(playbackSession: PlaybackSession) { + Paper.book("playbackSession").write(playbackSession.id,playbackSession) + } + fun removePlaybackSession(playbackSessionId:String) { + Paper.book("playbackSession").delete(playbackSessionId) + } + fun getPlaybackSessions():List { + val sessions:MutableList = mutableListOf() + Paper.book("playbackSession").allKeys.forEach { playbackSessionId -> + Paper.book("playbackSession").read(playbackSessionId)?.let { + sessions.add(it) + } + } + return sessions + } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/media/MediaEventManager.kt b/android/app/src/main/java/com/audiobookshelf/app/media/MediaEventManager.kt index 046fd90d..80d3b406 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/media/MediaEventManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/media/MediaEventManager.kt @@ -4,7 +4,6 @@ import android.util.Log import com.audiobookshelf.app.data.* import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.player.PlayerNotificationService -import com.audiobookshelf.app.player.SyncResult object MediaEventManager { const val tag = "MediaEventManager" @@ -69,7 +68,7 @@ object MediaEventManager { clientEventEmitter?.onMediaItemHistoryUpdated(mediaItemHistory) } - private fun addPlaybackEvent(eventName:String, playbackSession:PlaybackSession, syncResult:SyncResult?) { + private fun addPlaybackEvent(eventName:String, playbackSession:PlaybackSession, syncResult: SyncResult?) { val mediaItemHistory = getMediaItemHistoryMediaItem(playbackSession.mediaItemId) ?: createMediaItemHistoryForSession(playbackSession) val mediaItemEvent = MediaItemEvent( @@ -103,8 +102,7 @@ object MediaEventManager { libraryItemId, episodeId, isLocalOnly, - playbackSession. - serverConnectionConfigId, + playbackSession.serverConnectionConfigId, playbackSession.serverAddress, playbackSession.userId, createdAt = System.currentTimeMillis(), diff --git a/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt b/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt index f89566ca..9d463547 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt @@ -66,7 +66,7 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) { // and reset any server data already set val serverConnConfig = if (DeviceManager.isConnectedToServer) DeviceManager.serverConnectionConfig else DeviceManager.deviceData.getLastServerConnectionConfig() - if (!DeviceManager.isConnectedToServer || !apiHandler.isOnline() || serverConnConfig == null || serverConnConfig.id !== serverConfigIdUsed) { + if (!DeviceManager.isConnectedToServer || !DeviceManager.checkConnectivity(ctx) || serverConnConfig == null || serverConnConfig.id !== serverConfigIdUsed) { podcastEpisodeLibraryItemMap = mutableMapOf() serverLibraryCategories = listOf() serverLibraries = listOf() @@ -217,7 +217,7 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) { Log.d(tag, "checkSetValidServerConnectionConfig | $serverConfigIdUsed") coroutineScope { - if (!apiHandler.isOnline()) { + if (!DeviceManager.checkConnectivity(ctx)) { serverUserMediaProgress = mutableListOf() cb(false) } else { diff --git a/android/app/src/main/java/com/audiobookshelf/app/media/MediaProgressSyncer.kt b/android/app/src/main/java/com/audiobookshelf/app/media/MediaProgressSyncer.kt index efe71e06..70c65d25 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/media/MediaProgressSyncer.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/media/MediaProgressSyncer.kt @@ -1,4 +1,4 @@ -package com.audiobookshelf.app.player +package com.audiobookshelf.app.media import android.os.Handler import android.os.Looper @@ -6,9 +6,8 @@ import android.util.Log import com.audiobookshelf.app.data.LocalMediaProgress import com.audiobookshelf.app.data.MediaProgress import com.audiobookshelf.app.data.PlaybackSession -import com.audiobookshelf.app.data.Podcast import com.audiobookshelf.app.device.DeviceManager -import com.audiobookshelf.app.media.MediaEventManager +import com.audiobookshelf.app.player.PlayerNotificationService import com.audiobookshelf.app.server.ApiHandler import java.util.* import kotlin.concurrent.schedule @@ -25,7 +24,7 @@ data class SyncResult( var serverSyncMessage:String? ) -class MediaProgressSyncer(val playerNotificationService:PlayerNotificationService, private val apiHandler: ApiHandler) { +class MediaProgressSyncer(val playerNotificationService: PlayerNotificationService, private val apiHandler: ApiHandler) { private val tag = "MediaProgressSync" private val METERED_CONNECTION_SYNC_INTERVAL = 60000 @@ -199,8 +198,6 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic it.currentTime = mediaProgress.currentTime MediaEventManager.syncEvent(mediaProgress, "Received from server get media progress request while playback session open") - - DeviceManager.dbManager.saveLocalPlaybackSession(it) saveLocalProgress(it) } } @@ -226,16 +223,25 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic return cb(null) } + val hasNetworkConnection = DeviceManager.checkConnectivity(playerNotificationService) + + // Save playback session to db (server linked sessions only) + // Sessions are removed once successfully synced with the server + currentPlaybackSession?.let { + if (!it.isLocalLibraryItemOnly) { + DeviceManager.dbManager.savePlaybackSession(it) + } + } + if (currentIsLocal) { // Save local progress sync currentPlaybackSession?.let { - DeviceManager.dbManager.saveLocalPlaybackSession(it) saveLocalProgress(it) lastSyncTime = System.currentTimeMillis() // Local library item is linked to a server library item // Send sync to server also if connected to this server and local item belongs to this server - if (shouldSyncServer && !it.libraryItemId.isNullOrEmpty() && it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) { + if (hasNetworkConnection && shouldSyncServer && !it.libraryItemId.isNullOrEmpty() && it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) { apiHandler.sendLocalProgressSync(it) { syncSuccess, errorMsg -> Log.d( tag, @@ -245,6 +251,7 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic if (syncSuccess) { failedSyncs = 0 playerNotificationService.alertSyncSuccess() + DeviceManager.dbManager.removePlaybackSession(it.id) // Remove session from db } else { failedSyncs++ if (failedSyncs == 2) { @@ -260,7 +267,7 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic cb(SyncResult(false, null, null)) } } - } else if (shouldSyncServer) { + } else if (hasNetworkConnection && shouldSyncServer) { Log.d(tag, "sync: currentSessionId=$currentSessionId") apiHandler.sendProgressSync(currentSessionId, syncData) { syncSuccess, errorMsg -> if (syncSuccess) { @@ -268,6 +275,7 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic failedSyncs = 0 playerNotificationService.alertSyncSuccess() lastSyncTime = System.currentTimeMillis() + DeviceManager.dbManager.removePlaybackSession(currentSessionId) // Remove session from db } else { failedSyncs++ if (failedSyncs == 2) { @@ -307,6 +315,7 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic } } + fun reset() { currentPlaybackSession = null currentLocalMediaProgress = null diff --git a/android/app/src/main/java/com/audiobookshelf/app/models/User.kt b/android/app/src/main/java/com/audiobookshelf/app/models/User.kt new file mode 100644 index 00000000..6d610bf2 --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/models/User.kt @@ -0,0 +1,11 @@ +package com.audiobookshelf.app.models + +import com.audiobookshelf.app.data.MediaProgress +import com.fasterxml.jackson.annotation.JsonIgnoreProperties + +@JsonIgnoreProperties(ignoreUnknown = true) +data class User( + val id:String, + val username: String, + val mediaProgress:List +) diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt index 718b9247..15ced0bd 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt @@ -34,6 +34,7 @@ import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.managers.DbManager import com.audiobookshelf.app.managers.SleepTimerManager import com.audiobookshelf.app.media.MediaManager +import com.audiobookshelf.app.media.MediaProgressSyncer import com.audiobookshelf.app.server.ApiHandler import com.google.android.exoplayer2.* import com.google.android.exoplayer2.audio.AudioAttributes @@ -59,6 +60,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { var isStarted = false var isClosed = false var isUnmeteredNetwork = false + var hasNetworkConnectivity = false // Not 100% reliable has internet var isSwitchingPlayer = false // Used when switching between cast player and exoplayer } @@ -97,7 +99,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { var castPlayer:CastPlayer? = null lateinit var sleepTimerManager:SleepTimerManager - lateinit var mediaProgressSyncer:MediaProgressSyncer + lateinit var mediaProgressSyncer: MediaProgressSyncer private var notificationId = 10 private var channelId = "audiobookshelf_channel" @@ -193,8 +195,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { // To listen for network change from metered to unmetered val networkRequest = NetworkRequest.Builder() .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) - .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) + .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) .build() val connectivityManager = getSystemService(ConnectivityManager::class.java) as ConnectivityManager connectivityManager.registerNetworkCallback(networkRequest, networkCallback) @@ -668,7 +670,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { if (currentPlaybackSession == null) return true mediaProgressSyncer.currentPlaybackSession?.let { playbackSession -> - if (!apiHandler.isOnline() || playbackSession.isLocalLibraryItemOnly) { + if (!DeviceManager.checkConnectivity(ctx) || playbackSession.isLocalLibraryItemOnly) { return true // carry on } @@ -1098,10 +1100,11 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { networkCapabilities: NetworkCapabilities ) { super.onCapabilitiesChanged(network, networkCapabilities) - val unmetered = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) - Log.i(tag, "Network capabilities changed is unmetered = $unmetered") - isUnmeteredNetwork = unmetered - clientEventEmitter?.onNetworkMeteredChanged(unmetered) + + isUnmeteredNetwork = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) + hasNetworkConnectivity = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + Log.i(tag, "Network capabilities changed. hasNetworkConnectivity=$hasNetworkConnectivity | isUnmeteredNetwork=$isUnmeteredNetwork") + clientEventEmitter?.onNetworkMeteredChanged(isUnmeteredNetwork) } } 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 42747f53..05bd575f 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 @@ -216,13 +216,25 @@ class AbsDatabase : Plugin() { } @PluginMethod - fun syncLocalMediaProgressWithServer(call:PluginCall) { + fun syncLocalSessionsWithServer(call:PluginCall) { if (DeviceManager.serverConnectionConfig == null) { - Log.e(tag, "syncLocalMediaProgressWithServer not connected to server") + Log.e(tag, "syncLocalSessionsWithServer not connected to server") return call.resolve() } - apiHandler.syncMediaProgress { - call.resolve(JSObject(jacksonMapper.writeValueAsString(it))) + + apiHandler.syncLocalMediaProgressForUser { + Log.d(tag, "Finished syncing local media progress for user") + val savedSessions = DeviceManager.dbManager.getPlaybackSessions().filter { it.serverConnectionConfigId == DeviceManager.serverConnectionConfigId } + + if (savedSessions.isNotEmpty()) { + apiHandler.sendSyncLocalSessions(savedSessions) { success, errorMsg -> + if (!success) { + call.resolve(JSObject("{\"error\":\"$errorMsg\"}")) + } else { + call.resolve() + } + } + } } } 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 5f176824..afd27a01 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 @@ -1,13 +1,13 @@ package com.audiobookshelf.app.server import android.content.Context -import android.net.ConnectivityManager -import android.net.NetworkCapabilities import android.util.Log import com.audiobookshelf.app.data.* import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.media.MediaEventManager -import com.audiobookshelf.app.player.MediaProgressSyncData +import com.audiobookshelf.app.media.MediaProgressSyncData +import com.audiobookshelf.app.media.SyncResult +import com.audiobookshelf.app.models.User import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.core.json.JsonReadFeature import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper @@ -28,14 +28,14 @@ class ApiHandler(var ctx:Context) { private var defaultClient = OkHttpClient() private var pingClient = OkHttpClient.Builder().callTimeout(3, TimeUnit.SECONDS).build() - var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()) + private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()) - data class LocalMediaProgressSyncPayload(val localMediaProgress:List) + data class LocalSessionsSyncRequestPayload(val sessions:List) @JsonIgnoreProperties(ignoreUnknown = true) - data class MediaProgressSyncResponsePayload(val numServerProgressUpdates:Int, val localProgressUpdates:List, val serverProgressUpdates:List) - data class LocalMediaProgressSyncResultsPayload(var numLocalMediaProgressForServer:Int, var numServerProgressUpdates:Int, var numLocalProgressUpdates:Int, var serverProgressUpdates:List) + data class LocalSessionSyncResult(val id:String, val success:Boolean, val progressSynced:Boolean?, val error:String?) + data class LocalSessionsSyncResponsePayload(val results:List) - fun getRequest(endpoint:String, httpClient:OkHttpClient?, config:ServerConnectionConfig?, cb: (JSObject) -> Unit) { + private fun getRequest(endpoint:String, httpClient:OkHttpClient?, config:ServerConnectionConfig?, cb: (JSObject) -> Unit) { val address = config?.address ?: DeviceManager.serverAddress val token = config?.token ?: DeviceManager.token @@ -58,7 +58,7 @@ class ApiHandler(var ctx:Context) { makeRequest(request, null, cb) } - fun patchRequest(endpoint:String, payload: JSObject, cb: (JSObject) -> Unit) { + private fun patchRequest(endpoint:String, payload: JSObject, cb: (JSObject) -> Unit) { val mediaType = "application/json; charset=utf-8".toMediaType() val requestBody = payload.toString().toRequestBody(mediaType) val request = Request.Builder().patch(requestBody) @@ -67,31 +67,7 @@ class ApiHandler(var ctx:Context) { makeRequest(request, null, cb) } - fun isOnline(): Boolean { - val connectivityManager = ctx.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - 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 isUsingCellularData(): Boolean { - val connectivityManager = ctx.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) - return capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true - } - - fun makeRequest(request:Request, httpClient:OkHttpClient?, cb: (JSObject) -> Unit) { + private fun makeRequest(request:Request, httpClient:OkHttpClient?, cb: (JSObject) -> Unit) { val client = httpClient ?: defaultClient client.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { @@ -137,6 +113,18 @@ class ApiHandler(var ctx:Context) { }) } + fun getCurrentUser(cb: (User?) -> Unit) { + getRequest("/api/me", null, null) { + if (it.has("error")) { + Log.e(tag, it.getString("error") ?: "getCurrentUser Failed") + cb(null) + } else { + val user = jacksonMapper.readValue(it.toString()) + cb(user) + } + } + } + fun getLibraries(cb: (List) -> Unit) { val mapper = jacksonMapper getRequest("/api/libraries", null,null) { @@ -253,59 +241,6 @@ class ApiHandler(var ctx:Context) { } } - fun syncMediaProgress(cb: (LocalMediaProgressSyncResultsPayload) -> Unit) { - if (!isOnline()) { - Log.d(tag, "Error not online") - cb(LocalMediaProgressSyncResultsPayload(0,0,0, mutableListOf())) - return - } - - // Get all local media progress connected to items on the current connected server - val localMediaProgress = DeviceManager.dbManager.getAllLocalMediaProgress().filter { - it.serverConnectionConfigId == DeviceManager.serverConnectionConfig?.id - } - - val localSyncResultsPayload = LocalMediaProgressSyncResultsPayload(localMediaProgress.size,0, 0, mutableListOf()) - - if (localMediaProgress.isNotEmpty()) { - Log.d(tag, "Sending sync local progress request with ${localMediaProgress.size} progress items") - val payload = JSObject(jacksonMapper.writeValueAsString(LocalMediaProgressSyncPayload(localMediaProgress))) - postRequest("/api/me/sync-local-progress", payload, null) { - Log.d(tag, "Media Progress Sync payload $payload - response ${it}") - - if (it.toString() == "{}") { - Log.e(tag, "Progress sync received empty object") - } else if (it.has("error")) { - Log.e(tag, it.getString("error") ?: "Progress sync error") - } else { - val progressSyncResponsePayload = jacksonMapper.readValue(it.toString()) - - localSyncResultsPayload.numLocalProgressUpdates = progressSyncResponsePayload.localProgressUpdates.size - localSyncResultsPayload.serverProgressUpdates = progressSyncResponsePayload.serverProgressUpdates - 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 -> - MediaEventManager.syncEvent(localMediaProgress, "Local progress updated. Received from server sync local API request") - - DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress) - } - } - - progressSyncResponsePayload.serverProgressUpdates.forEach { localMediaProgress -> - MediaEventManager.syncEvent(localMediaProgress, "Server progress updated. Received from server sync local API request") - } - } - - cb(localSyncResultsPayload) - } - } else { - Log.d(tag, "No local media progress to sync") - cb(localSyncResultsPayload) - } - } - fun updateMediaProgress(libraryItemId:String,episodeId:String?,updatePayload:JSObject, cb: () -> Unit) { Log.d(tag, "updateMediaProgress $libraryItemId $episodeId $updatePayload") val endpoint = if(episodeId.isNullOrEmpty()) "/api/me/progress/$libraryItemId" else "/api/me/progress/$libraryItemId/$episodeId" @@ -377,4 +312,55 @@ class ApiHandler(var ctx:Context) { } } } + + fun sendSyncLocalSessions(playbackSessions:List, cb: (Boolean, String?) -> Unit) { + val payload = JSObject(jacksonMapper.writeValueAsString(LocalSessionsSyncRequestPayload(playbackSessions))) + + postRequest("/api/session/local-all", payload, null) { + if (!it.getString("error").isNullOrEmpty()) { + cb(false, it.getString("error")) + } else { + val response = jacksonMapper.readValue(it.toString()) + response.results.forEach { localSessionSyncResult -> + playbackSessions.find { ps -> ps.id == localSessionSyncResult.id }?.let { session -> + if (localSessionSyncResult.progressSynced == true) { + val syncResult = SyncResult(true, true, "Progress synced on server") + MediaEventManager.saveEvent(session, syncResult) + DeviceManager.dbManager.removePlaybackSession(session.id) + Log.i(tag, "Successfully synced session ${session.displayTitle} with server") + } else if (!localSessionSyncResult.success) { + Log.e(tag, "Failed to sync session ${session.displayTitle} with server. Error: ${localSessionSyncResult.error}") + } + } + } + cb(true, null) + } + } + } + + fun syncLocalMediaProgressForUser(cb: () -> Unit) { + // Get all local media progress for this server + val allLocalMediaProgress = DeviceManager.dbManager.getAllLocalMediaProgress().filter { it.serverConnectionConfigId == DeviceManager.serverConnectionConfigId } + if (allLocalMediaProgress.isEmpty()) { + Log.d(tag, "No local media progress to sync") + return cb() + } + + getCurrentUser { _user -> + _user?.let { user-> + // Compare server user progress with local progress + user.mediaProgress.forEach { mediaProgress -> + // Get matching local media progress + allLocalMediaProgress.find { it.isMatch(mediaProgress) }?.let { localMediaProgress -> + if (mediaProgress.lastUpdate > localMediaProgress.lastUpdate) { + Log.d(tag, "Server progress for media item id=\"${mediaProgress.mediaItemId}\" is more recent then local. Updating local current time ${localMediaProgress.currentTime} to ${mediaProgress.currentTime}") + localMediaProgress.updateFromServerMediaProgress(mediaProgress) + MediaEventManager.syncEvent(mediaProgress, "Sync on server connection") + } + } + } + } + cb() + } + } } diff --git a/layouts/default.vue b/layouts/default.vue index 5ea95dda..a1b69ed1 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -169,10 +169,23 @@ export default { this.$eventBus.$emit('library-changed') this.inittingLibraries = false }, + async syncLocalSessions() { + if (!this.user) { + console.log('[default] No need to sync local sessions - not connected to server') + return + } + + console.log('[default] Calling syncLocalSessions') + const response = await this.$db.syncLocalSessionsWithServer() + if (response && response.error) { + console.error('[default] Failed to sync local sessions', response.error) + } else { + console.log('[default] Successfully synced local sessions') + } + }, async syncLocalMediaProgress() { if (!this.user) { console.log('[default] No need to sync local media progress - not connected to server') - this.$store.commit('setLastLocalMediaSyncResults', null) return } @@ -180,15 +193,10 @@ export default { const response = await this.$db.syncLocalMediaProgressWithServer() if (!response) { if (this.$platform != 'web') this.$toast.error('Failed to sync local media with server') - this.$store.commit('setLastLocalMediaSyncResults', null) return } const { numLocalMediaProgressForServer, numServerProgressUpdates, numLocalProgressUpdates, serverProgressUpdates } = response if (numLocalMediaProgressForServer > 0) { - response.syncedAt = Date.now() - response.serverConfigName = this.$store.getters['user/getServerConfigName'] - this.$store.commit('setLastLocalMediaSyncResults', response) - if (serverProgressUpdates && serverProgressUpdates.length) { serverProgressUpdates.forEach((progress) => { console.log(`[default] Server progress was updated ${progress.id}`) @@ -203,7 +211,6 @@ export default { } } else { console.log('[default] syncLocalMediaProgress No local media progress to sync') - this.$store.commit('setLastLocalMediaSyncResults', null) } }, userUpdated(user) { @@ -295,7 +302,12 @@ export default { } console.log(`[default] finished connection attempt or already connected ${!!this.user}`) - await this.syncLocalMediaProgress() + if (this.$platform === 'ios') { + // TODO: Update ios to not use this + await this.syncLocalMediaProgress() + } else { + await this.syncLocalSessions() + } this.loadSavedSettings() this.hasMounted = true diff --git a/pages/localMedia/folders/index.vue b/pages/localMedia/folders/index.vue index 5b28a304..b223e57a 100644 --- a/pages/localMedia/folders/index.vue +++ b/pages/localMedia/folders/index.vue @@ -1,33 +1,5 @@