From e0e2f02e0ba78c30623434321b8857b90ffc7035 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Sat, 13 Aug 2022 12:41:20 -0400 Subject: [PATCH] Fix all kinds of syncing errors --- ios/App/App/plugins/AbsDatabase.swift | 10 ++--- ios/App/Shared/models/DataClasses.swift | 30 +++------------ ios/App/Shared/player/AudioPlayer.swift | 1 + ios/App/Shared/player/PlayerHandler.swift | 11 +++++- ios/App/Shared/util/ApiClient.swift | 47 +++++++++++++++++++---- ios/App/Shared/util/Extensions.swift | 27 +++++++++++-- 6 files changed, 83 insertions(+), 43 deletions(-) diff --git a/ios/App/App/plugins/AbsDatabase.swift b/ios/App/App/plugins/AbsDatabase.swift index 997b2db9..99e02a8f 100644 --- a/ios/App/App/plugins/AbsDatabase.swift +++ b/ios/App/App/plugins/AbsDatabase.swift @@ -157,13 +157,11 @@ public class AbsDatabase: CAPPlugin { let localMediaProgressId = call.getString("localMediaProgressId") do { - guard let localLibraryItemId = localLibraryItemId else { - call.reject("localLibraryItemId not specified") - return - } guard let serverMediaProgress = serverMediaProgress else { - call.reject("serverMediaProgress not specified") - return + return call.reject("serverMediaProgress not specified") + } + guard localLibraryItemId != nil || localMediaProgressId != nil else { + return call.reject("localLibraryItemId or localMediaProgressId must be specified") } let localMediaProgress = LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: localMediaProgressId, localLibraryItemId: localLibraryItemId, localEpisodeId: localEpisodeId) diff --git a/ios/App/Shared/models/DataClasses.swift b/ios/App/Shared/models/DataClasses.swift index 069b7818..d7148b8e 100644 --- a/ios/App/Shared/models/DataClasses.swift +++ b/ios/App/Shared/models/DataClasses.swift @@ -342,13 +342,13 @@ struct MediaProgress: Realmable, Codable { id = try values.decode(String.self, forKey: .id) libraryItemId = try values.decode(String.self, forKey: .libraryItemId) episodeId = try? values.decode(String.self, forKey: .episodeId) - duration = try MediaProgress.doubleOrStringDecoder(from: decoder, with: values, key: .duration) - progress = try MediaProgress.doubleOrStringDecoder(from: decoder, with: values, key: .progress) - currentTime = try MediaProgress.doubleOrStringDecoder(from: decoder, with: values, key: .currentTime) + duration = try values.doubleOrStringDecoder(key: .duration) + progress = try values.doubleOrStringDecoder(key: .progress) + currentTime = try values.doubleOrStringDecoder(key: .currentTime) isFinished = try values.decode(Bool.self, forKey: .isFinished) - lastUpdate = try MediaProgress.intOrStringDecoder(from: decoder, with: values, key: .lastUpdate) - startedAt = try MediaProgress.intOrStringDecoder(from: decoder, with: values, key: .startedAt) - finishedAt = try? MediaProgress.intOrStringDecoder(from: decoder, with: values, key: .finishedAt) + lastUpdate = try values.intOrStringDecoder(key: .lastUpdate) + startedAt = try values.intOrStringDecoder(key: .startedAt) + finishedAt = try? values.intOrStringDecoder(key: .finishedAt) } func encode(to encoder: Encoder) throws { @@ -364,24 +364,6 @@ struct MediaProgress: Realmable, Codable { try container.encode(startedAt, forKey: .startedAt) try container.encode(finishedAt, forKey: .finishedAt) } - - static private func doubleOrStringDecoder(from decoder: Decoder, with values: KeyedDecodingContainer, key: MediaProgress.CodingKeys) throws -> Double { - do { - return try values.decode(Double.self, forKey: key) - } catch { - let stringDuration = try values.decode(String.self, forKey: key) - return Double(stringDuration) ?? 0.0 - } - } - - static private func intOrStringDecoder(from decoder: Decoder, with values: KeyedDecodingContainer, key: MediaProgress.CodingKeys) throws -> Int { - do { - return try values.decode(Int.self, forKey: key) - } catch { - let stringDuration = try values.decode(String.self, forKey: key) - return Int(stringDuration) ?? 0 - } - } } struct PlaybackMetadata: Realmable, Codable { diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 9f1aeb21..61398f2a 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -267,6 +267,7 @@ class AudioPlayer: NSObject { let startOffset = audioTrack.startOffset ?? 0.0 return startOffset + currentTrackTime } + public func getPlayMethod() -> Int { return self.playbackSession.playMethod } diff --git a/ios/App/Shared/player/PlayerHandler.swift b/ios/App/Shared/player/PlayerHandler.swift index 16b0372a..b1ba5d66 100644 --- a/ios/App/Shared/player/PlayerHandler.swift +++ b/ios/App/Shared/player/PlayerHandler.swift @@ -71,12 +71,16 @@ class PlayerHandler { } public static func stopPlayback() { - player?.destroy() - player = nil + // Pause playback first, so we can sync our current progress + player?.pause() + // Stop updating progress before we destory the player, so we don't receive bad data timer?.invalidate() timer = nil + player?.destroy() + player = nil + NowPlayingInfo.shared.reset() } @@ -164,6 +168,9 @@ class PlayerHandler { return } + // Prevent a sync if we got junk data from the player (occurs when exiting out of memory + guard !playerCurrentTime.isNaN else { return } + lastSyncTime = Date().timeIntervalSince1970 // seconds let report = PlaybackReport(currentTime: playerCurrentTime, duration: player.getDuration(), timeListened: listeningTimePassedSinceLastSync) diff --git a/ios/App/Shared/util/ApiClient.swift b/ios/App/Shared/util/ApiClient.swift index fd4f17ba..7addc065 100644 --- a/ios/App/Shared/util/ApiClient.swift +++ b/ios/App/Shared/util/ApiClient.swift @@ -38,6 +38,27 @@ class ApiClient { } } + public static func postResource(endpoint: String, parameters: T, decodable: U.Type = U.self, callback: ((_ param: U) -> Void)?) { + if (Store.serverConfig == nil) { + NSLog("Server config not set") + return + } + + let headers: HTTPHeaders = [ + "Authorization": "Bearer \(Store.serverConfig!.token)" + ] + + AF.request("\(Store.serverConfig!.address)/\(endpoint)", method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, headers: headers).responseDecodable(of: decodable) { response in + switch response.result { + case .success(let obj): + callback?(obj) + case .failure(let error): + NSLog("api request to \(endpoint) failed") + print(error) + } + } + } + public static func postResource(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) { if (Store.serverConfig == nil) { NSLog("Server config not set") @@ -62,7 +83,7 @@ class ApiClient { } } - public static func patchResource(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) { + public static func patchResource(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) { if (Store.serverConfig == nil) { NSLog("Server config not set") callback?(false) @@ -121,7 +142,7 @@ class ApiClient { } } - ApiClient.postResource(endpoint: endpoint, parameters: [ + let parameters: [String: Any] = [ "forceDirectPlay": !forceTranscode ? "1" : "", "forceTranscode": forceTranscode ? "1" : "", "mediaPlayer": "AVPlayer", @@ -130,7 +151,8 @@ class ApiClient { "model": modelCode, "clientVersion": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ] - ], decodable: PlaybackSession.self) { obj in + ] + ApiClient.postResource(endpoint: endpoint, parameters: parameters, decodable: PlaybackSession.self) { obj in var session = obj session.serverConnectionConfigId = Store.serverConfig!.id @@ -145,7 +167,7 @@ class ApiClient { } public static func reportLocalMediaProgress(_ localMediaProgress: LocalMediaProgress, callback: @escaping (_ success: Bool) -> Void) { - postResource(endpoint: "/api/session/local", parameters: localMediaProgress, callback: callback) + postResource(endpoint: "api/session/local", parameters: localMediaProgress, callback: callback) } public static func syncMediaProgress(callback: @escaping (_ results: LocalMediaProgressSyncResultsPayload) -> Void) { @@ -156,8 +178,7 @@ class ApiClient { if ( !localMediaProgressList.isEmpty ) { let payload = LocalMediaProgressSyncPayload(localMediaProgress: localMediaProgressList) NSLog("Sending sync local progress request with \(localMediaProgressList.count) progress items") - try? postResource(endpoint: "/api/me/sync-local-progress", parameters: payload.asDictionary(), decodable: MediaProgressSyncResponsePayload.self) { response in - + 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) NSLog("Media Progress Sync | \(String(describing: try? resultsPayload.asDictionary()))") @@ -177,7 +198,7 @@ class ApiClient { public static func updateMediaProgress(libraryItemId: String, episodeId: String?, payload: T, callback: @escaping () -> Void) { NSLog("updateMediaProgress \(libraryItemId) \(episodeId ?? "NIL") \(payload)") - let endpoint = episodeId?.isEmpty ?? true ? "/api/me/progress/\(libraryItemId)" : "/api/me/progress/\(libraryItemId)/\(episodeId ?? "")" + let endpoint = episodeId?.isEmpty ?? true ? "api/me/progress/\(libraryItemId)" : "api/me/progress/\(libraryItemId)/\(episodeId ?? "")" patchResource(endpoint: endpoint, parameters: payload) { success in callback() } @@ -199,9 +220,19 @@ struct LocalMediaProgressSyncPayload: Codable { var localMediaProgress: [LocalMediaProgress] } -struct MediaProgressSyncResponsePayload: Codable { +struct MediaProgressSyncResponsePayload: Decodable { var numServerProgressUpdates: Int? var localProgressUpdates: [LocalMediaProgress]? + + private enum CodingKeys : String, CodingKey { + case numServerProgressUpdates, localProgressUpdates + } + + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + numServerProgressUpdates = try? values.intOrStringDecoder(key: .numServerProgressUpdates) + localProgressUpdates = try? values.decode([LocalMediaProgress].self, forKey: .localProgressUpdates) + } } struct LocalMediaProgressSyncResultsPayload: Codable { diff --git a/ios/App/Shared/util/Extensions.swift b/ios/App/Shared/util/Extensions.swift index ba7c4531..c76af20c 100644 --- a/ios/App/Shared/util/Extensions.swift +++ b/ios/App/Shared/util/Extensions.swift @@ -8,6 +8,7 @@ import Foundation import RealmSwift import Capacitor +import CoreMedia extension String: Error {} @@ -31,11 +32,31 @@ extension Collection where Iterator.Element: Encodable { } } +extension KeyedDecodingContainer { + func doubleOrStringDecoder(key: KeyedDecodingContainer.Key) throws -> Double { + do { + return try decode(Double.self, forKey: key) + } catch { + let stringValue = try decode(String.self, forKey: key) + return Double(stringValue) ?? 0.0 + } + } + + func intOrStringDecoder(key: KeyedDecodingContainer.Key) throws -> Int { + do { + return try decode(Int.self, forKey: key) + } catch { + let stringValue = try decode(String.self, forKey: key) + return Int(stringValue) ?? 0 + } + } +} + extension CAPPluginCall { func getJson(_ key: String, type: T.Type) -> T? { - guard let value = getString(key) else { return nil } - guard let valueData = value.data(using: .utf8) else { return nil } - return try? JSONDecoder().decode(type, from: valueData) + guard let value = getObject(key) else { return nil } + guard let json = try? JSONSerialization.data(withJSONObject: value) else { return nil } + return try? JSONDecoder().decode(type, from: json) } }