Update:Syncing local sessions rewrite to support offline sessions #381

This commit is contained in:
advplyr 2023-02-05 16:54:46 -06:00
parent 2f243787ce
commit f215efdcd0
17 changed files with 207 additions and 225 deletions

View file

@ -158,7 +158,7 @@ data class DeviceSettings(
data class DeviceData(
var serverConnectionConfigs:MutableList<ServerConnectionConfig>,
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

View file

@ -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) {

View file

@ -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

View file

@ -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
}
}

View file

@ -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<PlaybackSession> {
val sessions:MutableList<PlaybackSession> = mutableListOf()
Paper.book("playbackSession").allKeys.forEach { playbackSessionId ->
Paper.book("playbackSession").read<PlaybackSession>(playbackSessionId)?.let {
sessions.add(it)
}
}
return sessions
}
}

View file

@ -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(),

View file

@ -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 {

View file

@ -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

View file

@ -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<MediaProgress>
)

View file

@ -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)
}
}

View file

@ -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()
}
}
}
}
}

View file

@ -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<LocalMediaProgress>)
data class LocalSessionsSyncRequestPayload(val sessions:List<PlaybackSession>)
@JsonIgnoreProperties(ignoreUnknown = true)
data class MediaProgressSyncResponsePayload(val numServerProgressUpdates:Int, val localProgressUpdates:List<LocalMediaProgress>, val serverProgressUpdates:List<MediaProgress>)
data class LocalMediaProgressSyncResultsPayload(var numLocalMediaProgressForServer:Int, var numServerProgressUpdates:Int, var numLocalProgressUpdates:Int, var serverProgressUpdates:List<MediaProgress>)
data class LocalSessionSyncResult(val id:String, val success:Boolean, val progressSynced:Boolean?, val error:String?)
data class LocalSessionsSyncResponsePayload(val results:List<LocalSessionSyncResult>)
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<User>(it.toString())
cb(user)
}
}
}
fun getLibraries(cb: (List<Library>) -> 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<MediaProgressSyncResponsePayload>(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<PlaybackSession>, 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<LocalSessionsSyncResponsePayload>(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()
}
}
}

View file

@ -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

View file

@ -1,33 +1,5 @@
<template>
<div class="w-full h-full py-6">
<div v-if="localLibraryItemsOnCurrentServer.length" class="flex items-center justify-between mb-4 pb-2 px-2 border-b border-white border-opacity-10">
<p class="text-sm text-gray-100">{{ localLibraryItemsOnCurrentServer.length }} local items on this server</p>
<ui-btn small :loading="syncing" @click="syncLocalMedia">Sync</ui-btn>
</div>
<div v-if="lastLocalMediaSyncResults" class="px-2 mb-4">
<div class="w-full pl-2 pr-2 py-2 bg-black bg-opacity-25 rounded-lg relative">
<div class="flex items-center mb-1">
<span class="material-icons text-success text-xl">sync</span>
<p class="text-sm text-gray-300 pl-2">Local media progress synced with server</p>
</div>
<div class="flex justify-between mb-1.5">
<p class="text-xs text-gray-400 font-semibold">{{ syncedServerConfigName }}</p>
<p class="text-xs text-gray-400 italic">{{ $dateDistanceFromNow(syncedAt) }}</p>
</div>
<div v-if="!numLocalProgressUpdates && !numServerProgressUpdates">
<p class="text-sm text-gray-300">Local media progress was up-to-date with server ({{ numLocalMediaSynced }} item{{ numLocalMediaSynced == 1 ? '' : 's' }})</p>
</div>
<div v-else>
<p v-if="numServerProgressUpdates" class="text-sm text-gray-300">- {{ numServerProgressUpdates }} local media item{{ numServerProgressUpdates === 1 ? '' : 's' }} progress was updated on the server (local more recent).</p>
<p v-else class="text-sm text-gray-300">- No local media progress had to be synced on the server.</p>
<p v-if="numLocalProgressUpdates" class="text-sm text-gray-300">- {{ numLocalProgressUpdates }} local media item{{ numLocalProgressUpdates === 1 ? '' : 's' }} progress was updated to match the server (server more recent).</p>
<p v-else class="text-sm text-gray-300">- No server progress had to be synced with local media progress.</p>
</div>
</div>
</div>
<h1 class="text-base font-semibold px-2 mb-2">Local Folders</h1>
<div v-if="!isIos" class="w-full max-w-full px-2 py-2">
@ -78,67 +50,9 @@ export default {
computed: {
isIos() {
return this.$platform === 'ios'
},
lastLocalMediaSyncResults() {
return this.$store.state.lastLocalMediaSyncResults
},
serverConnectionConfigId() {
return this.$store.getters['user/getServerConnectionConfigId']
},
localLibraryItemsOnCurrentServer() {
return this.localLibraryItems.filter((lli) => {
return lli.serverConnectionConfigId === this.serverConnectionConfigId
})
},
numLocalMediaSynced() {
if (!this.lastLocalMediaSyncResults) return 0
return this.lastLocalMediaSyncResults.numLocalMediaProgressForServer || 0
},
syncedAt() {
if (!this.lastLocalMediaSyncResults) return 0
return this.lastLocalMediaSyncResults.syncedAt || 0
},
syncedServerConfigName() {
if (!this.lastLocalMediaSyncResults) return ''
return this.lastLocalMediaSyncResults.serverConfigName
},
numLocalProgressUpdates() {
if (!this.lastLocalMediaSyncResults) return 0
return this.lastLocalMediaSyncResults.numLocalProgressUpdates || 0
},
numServerProgressUpdates() {
if (!this.lastLocalMediaSyncResults) return 0
return this.lastLocalMediaSyncResults.numServerProgressUpdates || 0
}
},
methods: {
async syncLocalMedia() {
console.log('[localMedia] Calling syncLocalMediaProgress')
this.syncing = true
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)
this.syncing = false
return
}
const { numLocalMediaProgressForServer, numServerProgressUpdates, numLocalProgressUpdates } = response
if (numLocalMediaProgressForServer > 0) {
response.syncedAt = Date.now()
response.serverConfigName = this.$store.getters['user/getServerConfigName']
this.$store.commit('setLastLocalMediaSyncResults', response)
if (numServerProgressUpdates > 0 || numLocalProgressUpdates > 0) {
console.log(`[localMedia] ${numServerProgressUpdates} Server progress updates | ${numLocalProgressUpdates} Local progress updates`)
} else {
console.log('[localMedia] syncLocalMediaProgress No updates were necessary')
}
} else {
console.log('[localMedia] syncLocalMediaProgress No local media progress to sync')
this.$store.commit('setLastLocalMediaSyncResults', null)
}
this.syncing = false
},
async selectFolder() {
if (!this.newFolderMediaType) {
return this.$toast.error('Must select a media type')

View file

@ -198,6 +198,10 @@ class AbsDatabaseWeb extends WebPlugin {
return null
}
async syncLocalSessionsWithServer() {
return null
}
async syncServerMediaProgressWithLocalMediaProgress(payload) {
return null
}

View file

@ -74,6 +74,10 @@ class DbService {
return AbsDatabase.syncLocalMediaProgressWithServer()
}
syncLocalSessionsWithServer() {
return AbsDatabase.syncLocalSessionsWithServer()
}
syncServerMediaProgressWithLocalMediaProgress(payload) {
return AbsDatabase.syncServerMediaProgressWithLocalMediaProgress(payload)
}

View file

@ -21,8 +21,7 @@ export const state = () => ({
showSideDrawer: false,
isNetworkListenerInit: false,
serverSettings: null,
lastBookshelfScrollData: {},
lastLocalMediaSyncResults: null
lastBookshelfScrollData: {}
})
export const getters = {
@ -147,8 +146,5 @@ export const mutations = {
setServerSettings(state, val) {
state.serverSettings = val
this.$localStore.setServerSettings(state.serverSettings)
},
setLastLocalMediaSyncResults(state, val) {
state.lastLocalMediaSyncResults = val
}
}