From f562a1a5dc119254ec818b3d6642525193b3a209 Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Wed, 13 May 2026 21:18:05 -0500 Subject: [PATCH 1/2] Send less data to server with progress update. --- .../audiobookshelf/app/server/ApiHandler.kt | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) 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 d25e7446..9e123d8f 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 @@ -17,6 +17,7 @@ import com.audiobookshelf.app.plugins.AbsLogger import com.audiobookshelf.app.managers.SecureStorage import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.core.json.JsonReadFeature +import com.fasterxml.jackson.databind.node.ObjectNode import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import com.getcapacitor.JSArray @@ -48,7 +49,6 @@ class ApiHandler(var ctx:Context) { private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()) private var secureStorage = SecureStorage(ctx) - data class LocalSessionsSyncRequestPayload(val sessions:List, val deviceInfo:DeviceInfo) @JsonIgnoreProperties(ignoreUnknown = true) data class LocalSessionSyncResult(val id:String, val success:Boolean, val progressSynced:Boolean?, val error:String?) data class LocalSessionsSyncResponsePayload(val results:List) @@ -652,10 +652,29 @@ class ApiHandler(var ctx:Context) { } } - fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: (Boolean, String?) -> Unit) { - val payload = JSObject(jacksonMapper.writeValueAsString(playbackSession)) + private fun createPartialPlaybackSession(playbackSession: PlaybackSession): ObjectNode { + val json = jacksonMapper.createObjectNode() + json.put("id", playbackSession.id) + json.put("userId", playbackSession.userId) + json.put("libraryItemId", playbackSession.libraryItemId) + json.put("episodeId", playbackSession.episodeId) + json.put("mediaType", playbackSession.mediaType) + json.put("displayTitle", playbackSession.displayTitle) + json.put("displayAuthor", playbackSession.displayAuthor) + json.put("duration", playbackSession.duration) + json.put("playMethod", playbackSession.playMethod) + json.put("startedAt", playbackSession.startedAt) + json.put("updatedAt", playbackSession.updatedAt) + json.put("timeListening", playbackSession.timeListening) + json.put("currentTime", playbackSession.currentTime) + json.put("mediaPlayer", playbackSession.mediaPlayer) + return json + } - postRequest("/api/session/local", payload, null) { + fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: (Boolean, String?) -> Unit) { + val partialSession = createPartialPlaybackSession(playbackSession) + partialSession.set("deviceInfo", jacksonMapper.valueToTree(playbackSession.deviceInfo)) + postRequest("/api/session/local", JSObject(partialSession.toString()), null) { if (!it.getString("error").isNullOrEmpty()) { cb(false, it.getString("error")) } else { @@ -748,10 +767,12 @@ class ApiHandler(var ctx:Context) { val deviceId = Settings.Secure.getString(ctx.contentResolver, Settings.Secure.ANDROID_ID) val deviceInfo = DeviceInfo(deviceId, Build.MANUFACTURER, Build.MODEL, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME) - val payload = JSObject(jacksonMapper.writeValueAsString(LocalSessionsSyncRequestPayload(playbackSessions, deviceInfo))) + val json = jacksonMapper.createObjectNode(); + json.putArray("sessions").addAll(playbackSessions.map(::createPartialPlaybackSession)) + json.set("deviceInfo", jacksonMapper.valueToTree(deviceInfo)) AbsLogger.info("ApiHandler", "sendSyncLocalSessions: Sending ${playbackSessions.size} saved local playback sessions to server (${DeviceManager.serverConnectionConfigName})") - postRequest("/api/session/local-all", payload, null) { + postRequest("/api/session/local-all", JSObject(json.toString()), null) { if (!it.getString("error").isNullOrEmpty()) { AbsLogger.error("ApiHandler", "sendSyncLocalSessions: Failed to sync local sessions. (${it.getString("error")})") cb(false, it.getString("error")) From 1d163df77302e3fd83178f7d753420e658fd844e Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 1 Aug 2026 17:47:35 -0500 Subject: [PATCH 2/2] Update iOS to send partial playback session on local syncs --- ios/App/Shared/util/ApiClient.swift | 48 ++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/ios/App/Shared/util/ApiClient.swift b/ios/App/Shared/util/ApiClient.swift index 1b1a7abb..cc5c1dbc 100644 --- a/ios/App/Shared/util/ApiClient.swift +++ b/ios/App/Shared/util/ApiClient.swift @@ -511,7 +511,8 @@ class ApiClient { public static func reportLocalPlaybackProgress(_ session: PlaybackSession) async -> Bool { return await withCheckedContinuation { continuation in - postResourceWithTokenRefresh(endpoint: "api/session/local", parameters: session) { success in + let payload = PartialPlaybackSessionSyncPayload(from: session, includeDeviceInfo: true) + postResourceWithTokenRefresh(endpoint: "api/session/local", parameters: payload) { success in continuation.resume(returning: success) } } @@ -519,7 +520,10 @@ class ApiClient { public static func reportAllLocalPlaybackSessions(_ sessions: [PlaybackSession]) async -> Bool { return await withCheckedContinuation { continuation in - let payload = LocalPlaybackSessionSyncAllPayload(sessions: sessions, deviceInfo: sessions.first?.deviceInfo) + let payload = LocalPlaybackSessionSyncAllPayload( + sessions: sessions.map { PartialPlaybackSessionSyncPayload(from: $0, includeDeviceInfo: false) }, + deviceInfo: sessions.first?.deviceInfo + ) postResourceWithTokenRefresh(endpoint: "api/session/local-all", parameters: payload) { success in continuation.resume(returning: success) } @@ -668,8 +672,44 @@ struct LocalMediaProgressSyncResultsPayload: Codable { var numLocalProgressUpdates: Int? } -struct LocalPlaybackSessionSyncAllPayload: Codable { - var sessions: [PlaybackSession] +struct PartialPlaybackSessionSyncPayload: Encodable { + let id: String + let userId: String? + let libraryItemId: String? + let episodeId: String? + let mediaType: String + let displayTitle: String? + let displayAuthor: String? + let duration: Double + let playMethod: Int + let startedAt: Double? + let updatedAt: Double? + let timeListening: Double + let currentTime: Double + let mediaPlayer: String + let deviceInfo: [String: String?]? + + init(from session: PlaybackSession, includeDeviceInfo: Bool) { + id = session.id + userId = session.userId + libraryItemId = session.libraryItemId + episodeId = session.episodeId + mediaType = session.mediaType + displayTitle = session.displayTitle + displayAuthor = session.displayAuthor + duration = session.duration + playMethod = session.playMethod + startedAt = session.startedAt + updatedAt = session.updatedAt + timeListening = session.timeListening + currentTime = session.currentTime + mediaPlayer = session.mediaPlayer + deviceInfo = includeDeviceInfo ? session.deviceInfo : nil + } +} + +struct LocalPlaybackSessionSyncAllPayload: Encodable { + var sessions: [PartialPlaybackSessionSyncPayload] var deviceInfo: [String: String?]? }