diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 7610c2b4..270dea3a 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -28,6 +28,7 @@ 4D66B956282EE951008272D4 /* AbsFileSystem.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B955282EE951008272D4 /* AbsFileSystem.m */; }; 4D66B958282EEA14008272D4 /* AbsFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B957282EEA14008272D4 /* AbsFileSystem.swift */; }; 4D91EEC62A40F28D004807ED /* EBookFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D91EEC52A40F28D004807ED /* EBookFile.swift */; }; + 4DABC04F2B0139CA000F6264 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DABC04E2B0139CA000F6264 /* User.swift */; }; 4DF74912287105C600AC7814 /* DeviceSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DF74911287105C600AC7814 /* DeviceSettings.swift */; }; 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; }; 504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; }; @@ -99,6 +100,7 @@ 4D66B957282EEA14008272D4 /* AbsFileSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsFileSystem.swift; sourceTree = ""; }; 4D8D412C26E187E400BA5F0D /* App-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "App-Bridging-Header.h"; sourceTree = ""; }; 4D91EEC52A40F28D004807ED /* EBookFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EBookFile.swift; sourceTree = ""; }; + 4DABC04E2B0139CA000F6264 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; 4DF74911287105C600AC7814 /* DeviceSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceSettings.swift; sourceTree = ""; }; 50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = ""; }; 504EC3041FED79650016851F /* Audiobookshelf.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Audiobookshelf.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -302,6 +304,7 @@ E9D5505B28AC1C6200C746DD /* LibraryFile.swift */, E9D5505D28AC1C8500C746DD /* MediaProgress.swift */, 4D91EEC52A40F28D004807ED /* EBookFile.swift */, + 4DABC04E2B0139CA000F6264 /* User.swift */, ); path = server; sourceTree = ""; @@ -542,6 +545,7 @@ E9D5505828AC1C1A00C746DD /* Library.swift in Sources */, 3AD4FCEB280443DD006DB301 /* Database.swift in Sources */, 3AD4FCE528043E50006DB301 /* AbsDatabase.swift in Sources */, + 4DABC04F2B0139CA000F6264 /* User.swift in Sources */, 4D66B952282EE822008272D4 /* AbsDownloader.m in Sources */, E9D5506828AC1DC300C746DD /* LocalPodcastEpisode.swift in Sources */, E9D5505228AC1B5D00C746DD /* Chapter.swift in Sources */, diff --git a/ios/App/App/plugins/AbsDatabase.m b/ios/App/App/plugins/AbsDatabase.m index 4741f331..5fb59835 100644 --- a/ios/App/App/plugins/AbsDatabase.m +++ b/ios/App/App/plugins/AbsDatabase.m @@ -21,8 +21,8 @@ CAP_PLUGIN(AbsDatabase, "AbsDatabase", CAP_PLUGIN_METHOD(getLocalLibraryItemsInFolder, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(getAllLocalMediaProgress, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(removeLocalMediaProgress, CAPPluginReturnPromise); - CAP_PLUGIN_METHOD(syncLocalMediaProgressWithServer, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(syncServerMediaProgressWithLocalMediaProgress, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(syncLocalSessionsWithServer, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(updateLocalMediaProgressFinished, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(updateDeviceSettings, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(updateLocalEbookProgress, CAPPluginReturnPromise); diff --git a/ios/App/App/plugins/AbsDatabase.swift b/ios/App/App/plugins/AbsDatabase.swift index 12bc1eec..34a74f2b 100644 --- a/ios/App/App/plugins/AbsDatabase.swift +++ b/ios/App/App/plugins/AbsDatabase.swift @@ -146,17 +146,16 @@ public class AbsDatabase: CAPPlugin { call.resolve() } - @objc func syncLocalMediaProgressWithServer(_ call: CAPPluginCall) { + @objc func syncLocalSessionsWithServer(_ call: CAPPluginCall) { + logger.log("syncLocalSessionsWithServer: Starting") guard Store.serverConfig != nil else { - call.reject("syncLocalMediaProgressWithServer not connected to server") - return + call.reject("syncLocalSessionsWithServer not connected to server") + return call.resolve() } - ApiClient.syncMediaProgress { results in - do { - call.resolve(try results.asDictionary()) - } catch { - call.reject("Failed to report synced media progress") - } + + Task { + await ApiClient.syncLocalSessionsWithServer() + call.resolve() } } diff --git a/ios/App/Podfile.lock b/ios/App/Podfile.lock index 6768b073..d07dbd1b 100644 --- a/ios/App/Podfile.lock +++ b/ios/App/Podfile.lock @@ -90,4 +90,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 7a8fc177ef0646dd60a1ee8aa387964975fcc1e3 -COCOAPODS: 1.12.0 +COCOAPODS: 1.12.1 diff --git a/ios/App/Shared/models/server/User.swift b/ios/App/Shared/models/server/User.swift new file mode 100644 index 00000000..8f85300f --- /dev/null +++ b/ios/App/Shared/models/server/User.swift @@ -0,0 +1,41 @@ +// +// User.swift +// Audiobookshelf +// +// Created by advplyr on 11/12/23. +// + +import Foundation +import RealmSwift + +class User: EmbeddedObject, Codable { + @Persisted var id: String = "" + @Persisted var username: String = "" + @Persisted var mediaProgress = List() + + private enum CodingKeys : String, CodingKey { + case id, username, mediaProgress + } + + override init() { + super.init() + } + + required init(from decoder: Decoder) throws { + super.init() + + let values = try decoder.container(keyedBy: CodingKeys.self) + id = try values.decode(String.self, forKey: .id) + username = try values.decode(String.self, forKey: .username) + if let progresses = try? values.decode([MediaProgress].self, forKey: .mediaProgress) { + mediaProgress.append(objectsIn: progresses) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(username, forKey: .username) + try container.encode(Array(mediaProgress), forKey: .mediaProgress) + } +} diff --git a/ios/App/Shared/player/PlayerHandler.swift b/ios/App/Shared/player/PlayerHandler.swift index aa402489..298e41b5 100644 --- a/ios/App/Shared/player/PlayerHandler.swift +++ b/ios/App/Shared/player/PlayerHandler.swift @@ -19,7 +19,6 @@ class PlayerHandler { // Cleanup and sync old sessions cleanupOldSessions(currentSessionId: sessionId) - Task { await PlayerProgress.shared.syncToServer() } // Set now playing info NowPlayingInfo.shared.setSessionMetadata(metadata: NowPlayingMetadata(id: session.id, itemId: session.libraryItemId!, title: session.displayTitle ?? "Unknown title", author: session.displayAuthor, series: nil)) diff --git a/ios/App/Shared/player/PlayerProgress.swift b/ios/App/Shared/player/PlayerProgress.swift index 9aa15739..18f63719 100644 --- a/ios/App/Shared/player/PlayerProgress.swift +++ b/ios/App/Shared/player/PlayerProgress.swift @@ -36,17 +36,6 @@ class PlayerProgress { await UIApplication.shared.endBackgroundTask(backgroundToken) } - public func syncToServer() async { - let backgroundToken = await UIApplication.shared.beginBackgroundTask(withName: "ABS:syncToServer") - do { - try await updateAllServerSessionFromLocalSession() - } catch { - logger.error("Failed to syncToServer") - logger.error(error) - } - await UIApplication.shared.endBackgroundTask(backgroundToken) - } - public func syncFromServer() async { let backgroundToken = await UIApplication.shared.beginBackgroundTask(withName: "ABS:syncFromServer") do { diff --git a/ios/App/Shared/util/ApiClient.swift b/ios/App/Shared/util/ApiClient.swift index bf3cce57..cd498ac2 100644 --- a/ios/App/Shared/util/ApiClient.swift +++ b/ios/App/Shared/util/ApiClient.swift @@ -189,34 +189,62 @@ class ApiClient { return await postResource(endpoint: "api/session/local", parameters: session) } - public static func syncMediaProgress(callback: @escaping (_ results: LocalMediaProgressSyncResultsPayload) -> Void) { - let localMediaProgressList = Database.shared.getAllLocalMediaProgress().filter { - $0.serverConnectionConfigId == Store.serverConfig?.id - }.map { $0.freeze() } - - if ( !localMediaProgressList.isEmpty ) { - let payload = LocalMediaProgressSyncPayload(localMediaProgress: localMediaProgressList) - logger.log("Sending sync local progress request with \(localMediaProgressList.count) progress items") - postResource(endpoint: "api/me/sync-local-progress", parameters: payload, decodable: MediaProgressSyncResponsePayload.self) { response in - let resultsPayload = LocalMediaProgressSyncResultsPayload(numLocalMediaProgressForServer: localMediaProgressList.count, numServerProgressUpdates: response.numServerProgressUpdates, numLocalProgressUpdates: response.localProgressUpdates?.count) - logger.log("Media Progress Sync | \(String(describing: try? resultsPayload.asDictionary()))") - - if let updates = response.localProgressUpdates { - for update in updates { - do { - try update.save() - } catch { - debugPrint("Failed to update local media progress") - debugPrint(error) + public static func reportAllLocalPlaybackSessions(_ sessions: [PlaybackSession]) async -> Bool { + return await postResource(endpoint: "api/session/local-all", parameters: LocalPlaybackSessionSyncAllPayload(sessions: sessions)) + } + + public static func syncLocalSessionsWithServer() async { + do { + // Sync server progress with local media progress + let localMediaProgressList = Database.shared.getAllLocalMediaProgress().filter { + $0.serverConnectionConfigId == Store.serverConfig?.id + }.map { $0.freeze() } + logger.log("syncLocalSessionsWithServer: Found \(localMediaProgressList.count) local media progress for server") + + if (localMediaProgressList.isEmpty) { + logger.log("syncLocalSessionsWithServer: No local progress to sync") + } else { + let currentUser = await ApiClient.getCurrentUser() + guard let currentUser = currentUser else { + logger.log("syncLocalSessionsWithServer: No User") + return + } + try currentUser.mediaProgress.forEach { mediaProgress in + let localMediaProgress = localMediaProgressList.first { lmp in + if (lmp.episodeId != nil) { + return lmp.episodeId == mediaProgress.episodeId + } else { + return lmp.libraryItemId == mediaProgress.libraryItemId + } + } + if (localMediaProgress != nil && mediaProgress.lastUpdate > localMediaProgress!.lastUpdate) { + logger.log("syncLocalSessionsWithServer: Updating local media progress \(localMediaProgress!.id) with server media progress") + try localMediaProgress?.updateFromServerMediaProgress(mediaProgress) + } else if (localMediaProgress != nil) { + logger.log("syncLocalSessionsWithServer: Local progress for \(localMediaProgress!.id) is more recent then server progress") + } + } + } + + // Send saved playback sessions to server and remove them from db + let playbackSessions = Database.shared.getAllPlaybackSessions().filter { + $0.serverConnectionConfigId == Store.serverConfig?.id + }.map { $0.freeze() } + logger.log("syncLocalSessionsWithServer: Found \(playbackSessions.count) playback sessions for server") + if (!playbackSessions.isEmpty) { + let success = await ApiClient.reportAllLocalPlaybackSessions(playbackSessions) + if (success) { + // Remove sessions from db + try playbackSessions.forEach { session in + if let session = session.thaw() { + try session.delete() } } } - - callback(resultsPayload) } - } else { - logger.log("No local media progress to sync") - callback(LocalMediaProgressSyncResultsPayload(numLocalMediaProgressForServer: 0, numServerProgressUpdates: 0, numLocalProgressUpdates: 0)) + } catch { + debugPrint(error) + return } } @@ -234,6 +262,11 @@ class ApiClient { return await getResource(endpoint: endpoint, decodable: MediaProgress.self) } + public static func getCurrentUser() async -> User? { + logger.log("getCurrentUser") + return await getResource(endpoint: "api/me", decodable: User.self) + } + public static func getLibraryItemWithProgress(libraryItemId:String, episodeId:String?, callback: @escaping (_ param: LibraryItem?) -> Void) { var endpoint = "api/items/\(libraryItemId)?expanded=1&include=progress" if episodeId != nil { @@ -287,6 +320,10 @@ struct LocalMediaProgressSyncResultsPayload: Codable { var numLocalProgressUpdates: Int? } +struct LocalPlaybackSessionSyncAllPayload: Codable { + var sessions: [PlaybackSession] +} + struct Connectivity { static private let sharedInstance = NetworkReachabilityManager()! static var isConnectedToInternet:Bool { diff --git a/ios/App/Shared/util/Database.swift b/ios/App/Shared/util/Database.swift index be39b5c8..c20a63b0 100644 --- a/ios/App/Shared/util/Database.swift +++ b/ios/App/Shared/util/Database.swift @@ -241,6 +241,16 @@ class Database { } } + public func getAllPlaybackSessions() -> [PlaybackSession] { + do { + let realm = try Realm() + return Array(realm.objects(PlaybackSession.self)) + } catch { + debugPrint(error) + return [] + } + } + public func getPlaybackSession(id: String) -> PlaybackSession? { do { let realm = try Realm() diff --git a/layouts/default.vue b/layouts/default.vue index 258b70de..c7f3e935 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -44,12 +44,7 @@ export default { if (timeSinceDisconnect > 5000) { console.log('Time since disconnect was', timeSinceDisconnect, 'sync with server') setTimeout(() => { - if (this.$platform === 'ios') { - // TODO: Update ios to not use this - this.syncLocalMediaProgress() - } else { - this.syncLocalSessions() - } + this.syncLocalSessions() }, 4000) } } @@ -215,36 +210,6 @@ export default { await this.$store.dispatch('globals/loadLocalMediaProgress') } }, - 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') - const response = await this.$db.syncLocalMediaProgressWithServer() - if (!response) { - if (this.$platform != 'web') this.$toast.error('Failed to sync local media with server') - return - } - const { numLocalMediaProgressForServer, numServerProgressUpdates, numLocalProgressUpdates, serverProgressUpdates } = response - if (numLocalMediaProgressForServer > 0) { - if (serverProgressUpdates && serverProgressUpdates.length) { - serverProgressUpdates.forEach((progress) => { - console.log(`[default] Server progress was updated ${progress.id}`) - this.$store.commit('user/updateUserMediaProgress', progress) - }) - } - - 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') - } - }, userUpdated(user) { // console.log('[default] userUpdated:', JSON.stringify(user)) if (this.user && this.user.id == user.id) { @@ -362,12 +327,7 @@ export default { } console.log(`[default] finished connection attempt or already connected ${!!this.user}`) - if (this.$platform === 'ios') { - // TODO: Update ios to not use this - await this.syncLocalMediaProgress() - } else { - await this.syncLocalSessions() - } + await this.syncLocalSessions() this.loadSavedSettings() this.hasMounted = true diff --git a/plugins/capacitor/AbsDatabase.js b/plugins/capacitor/AbsDatabase.js index 909587d9..0d6ee8a6 100644 --- a/plugins/capacitor/AbsDatabase.js +++ b/plugins/capacitor/AbsDatabase.js @@ -194,10 +194,6 @@ class AbsDatabaseWeb extends WebPlugin { return null } - async syncLocalMediaProgressWithServer() { - return null - } - async syncLocalSessionsWithServer() { return null } diff --git a/plugins/db.js b/plugins/db.js index 34d05cc7..1ea7a65e 100644 --- a/plugins/db.js +++ b/plugins/db.js @@ -70,10 +70,6 @@ class DbService { return AbsDatabase.removeLocalMediaProgress({ localMediaProgressId }) } - syncLocalMediaProgressWithServer() { - return AbsDatabase.syncLocalMediaProgressWithServer() - } - syncLocalSessionsWithServer() { return AbsDatabase.syncLocalSessionsWithServer() }