diff --git a/ios/App/App/plugins/AbsAudioPlayer.swift b/ios/App/App/plugins/AbsAudioPlayer.swift index 38ec9f76..1e5d0c62 100644 --- a/ios/App/App/plugins/AbsAudioPlayer.swift +++ b/ios/App/App/plugins/AbsAudioPlayer.swift @@ -179,12 +179,17 @@ public class AbsAudioPlayer: CAPPlugin { "value": PlayerHandler.getCurrentTime() ]) } + @objc func sendSleepTimerSet() { self.notifyListeners("onSleepTimerSet", data: [ "value": PlayerHandler.remainingSleepTime ]) } + @objc func onLocalMediaProgressUpdate(_ localMediaProgress: [String: Any]) { + self.notifyListeners("onLocalMediaProgressUpdate", data: localMediaProgress) + } + @objc func onPlaybackFailed() { if (PlayerHandler.getPlayMethod() == PlayMethod.directplay.rawValue) { let playbackSession = PlayerHandler.getPlaybackSession() @@ -219,6 +224,7 @@ public class AbsAudioPlayer: CAPPlugin { "playWhenReady": playWhenReady, ]) } + @objc func sendPlaybackSession(session: [String: Any]) { self.notifyListeners("onPlaybackSession", data: session) } diff --git a/ios/App/App/plugins/AbsDatabase.swift b/ios/App/App/plugins/AbsDatabase.swift index 245c0a63..997b2db9 100644 --- a/ios/App/App/plugins/AbsDatabase.swift +++ b/ios/App/App/plugins/AbsDatabase.swift @@ -166,7 +166,7 @@ public class AbsDatabase: CAPPlugin { return } - let localMediaProgress = fetchOrCreateLocalMediaProgress(localMediaProgressId: localMediaProgressId, localLibraryItemId: localLibraryItemId, localEpisodeId: localEpisodeId) + let localMediaProgress = LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: localMediaProgressId, localLibraryItemId: localLibraryItemId, localEpisodeId: localEpisodeId) guard var localMediaProgress = localMediaProgress else { call.reject("Local media progress not found or created") return @@ -190,7 +190,7 @@ public class AbsDatabase: CAPPlugin { NSLog("updateLocalMediaProgressFinished \(localMediaProgressId ?? "Unknown") | Is Finished: \(isFinished)") - let localMediaProgress = fetchOrCreateLocalMediaProgress(localMediaProgressId: localMediaProgressId, localLibraryItemId: localLibraryItemId, localEpisodeId: localEpisodeId) + let localMediaProgress = LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: localMediaProgressId, localLibraryItemId: localLibraryItemId, localEpisodeId: localEpisodeId) guard var localMediaProgress = localMediaProgress else { call.resolve(["error": "Library Item not found"]) return @@ -218,19 +218,6 @@ public class AbsDatabase: CAPPlugin { } } - private func fetchOrCreateLocalMediaProgress(localMediaProgressId: String?, localLibraryItemId: String?, localEpisodeId: String?) -> LocalMediaProgress? { - if let localMediaProgressId = localMediaProgressId { - return Database.shared.getLocalMediaProgress(localMediaProgressId: localMediaProgressId) - } else if let localLibraryItemId = localLibraryItemId { - guard let localLibraryItem = Database.shared.getLocalLibraryItem(localLibraryItemId: localLibraryItemId) else { return nil } - let episode = localLibraryItem.getPodcastEpisode(episodeId: localEpisodeId) - return LocalMediaProgress(localLibraryItem: localLibraryItem, episode: episode) - } else { - return nil - } - - } - @objc func updateDeviceSettings(_ call: CAPPluginCall) { let disableAutoRewind = call.getBool("disableAutoRewind") ?? false let enableAltView = call.getBool("enableAltView") ?? false diff --git a/ios/App/Shared/models/LocalLibraryExtensions.swift b/ios/App/Shared/models/LocalLibraryExtensions.swift index 2fa3532f..d90784f9 100644 --- a/ios/App/Shared/models/LocalLibraryExtensions.swift +++ b/ios/App/Shared/models/LocalLibraryExtensions.swift @@ -196,4 +196,16 @@ extension LocalMediaProgress { self.finishedAt = serverMediaProgress.finishedAt self.startedAt = serverMediaProgress.startedAt } + + static func fetchOrCreateLocalMediaProgress(localMediaProgressId: String?, localLibraryItemId: String?, localEpisodeId: String?) -> LocalMediaProgress? { + if let localMediaProgressId = localMediaProgressId { + return Database.shared.getLocalMediaProgress(localMediaProgressId: localMediaProgressId) + } else if let localLibraryItemId = localLibraryItemId { + guard let localLibraryItem = Database.shared.getLocalLibraryItem(localLibraryItemId: localLibraryItemId) else { return nil } + let episode = localLibraryItem.getPodcastEpisode(episodeId: localEpisodeId) + return LocalMediaProgress(localLibraryItem: localLibraryItem, episode: episode) + } else { + return nil + } + } } diff --git a/ios/App/Shared/models/PlaybackSession.swift b/ios/App/Shared/models/PlaybackSession.swift index 5fb9480e..ae9dfb56 100644 --- a/ios/App/Shared/models/PlaybackSession.swift +++ b/ios/App/Shared/models/PlaybackSession.swift @@ -7,7 +7,7 @@ import Foundation -struct PlaybackSession: Decodable, Encodable { +struct PlaybackSession: Codable { var id: String var userId: String? var libraryItemId: String? @@ -30,10 +30,21 @@ struct PlaybackSession: Decodable, Encodable { var serverConnectionConfigId: String? var serverAddress: String? + var isLocal: Bool { self.localLibraryItem != nil } + + var localMediaProgressId: String { + if let episodeId = episodeId { + return "\(localLibraryItem!.id)-\(episodeId)" + } else { + return localLibraryItem!.id + } + } + var totalDuration: Double { var total = 0.0 self.audioTracks.forEach { total += $0.duration } return total } + var progress: Double { self.currentTime / self.totalDuration } } diff --git a/ios/App/Shared/player/PlayerHandler.swift b/ios/App/Shared/player/PlayerHandler.swift index 3384e5fa..e6a7a8d8 100644 --- a/ios/App/Shared/player/PlayerHandler.swift +++ b/ios/App/Shared/player/PlayerHandler.swift @@ -11,7 +11,7 @@ class PlayerHandler { private static var player: AudioPlayer? private static var session: PlaybackSession? private static var timer: Timer? - private static var lastSyncTime:Double = 0.0 + private static var lastSyncTime: Double = 0.0 private static var _remainingSleepTime: Int? = nil public static var remainingSleepTime: Int? { @@ -69,6 +69,7 @@ class PlayerHandler { } } } + public static func stopPlayback() { player?.destroy() player = nil @@ -82,12 +83,15 @@ class PlayerHandler { public static func getCurrentTime() -> Double? { self.player?.getCurrentTime() } + public static func setPlaybackSpeed(speed: Float) { self.player?.setPlaybackRate(speed) } + public static func getPlayMethod() -> Int? { self.player?.getPlayMethod() } + public static func getPlaybackSession() -> PlaybackSession? { self.player?.getPlaybackSession() } @@ -100,6 +104,7 @@ class PlayerHandler { let destinationTime = player.getCurrentTime() + amount player.seek(destinationTime, from: "handler") } + public static func seekBackward(amount: Double) { guard let player = player else { return @@ -108,9 +113,11 @@ class PlayerHandler { let destinationTime = player.getCurrentTime() - amount player.seek(destinationTime, from: "handler") } + public static func seek(amount: Double) { player?.seek(amount, from: "handler") } + public static func getMetdata() -> [String: Any] { DispatchQueue.main.async { syncProgress() @@ -140,15 +147,15 @@ class PlayerHandler { remainingSleepTime! -= 1 } } + public static func syncProgress() { if session == nil { return } guard let player = player else { return } + // Prevent a sync at the current time let playerCurrentTime = player.getCurrentTime() - if (lastSyncReport != nil && lastSyncReport?.currentTime == playerCurrentTime) { - // No need to syncProgress - return - } + let hasSyncAtCurrentTime = lastSyncReport?.currentTime.isEqual(to: playerCurrentTime) ?? false + if hasSyncAtCurrentTime { return } // Prevent multiple sync requests let timeSinceLastSync = Date().timeIntervalSince1970 - lastSyncTime @@ -158,15 +165,44 @@ class PlayerHandler { } lastSyncTime = Date().timeIntervalSince1970 // seconds - let report = PlaybackReport(currentTime: playerCurrentTime, duration: player.getDuration(), timeListened: listeningTimePassedSinceLastSync) session!.currentTime = playerCurrentTime listeningTimePassedSinceLastSync = 0 lastSyncReport = report - // TODO: check if online - NSLog("sending playback report") - ApiClient.reportPlaybackProgress(report: report, sessionId: session!.id) + let sessionIsLocal = session!.isLocal + if !sessionIsLocal { + if Connectivity.isConnectedToInternet { + NSLog("sending playback report") + ApiClient.reportPlaybackProgress(report: report, sessionId: session!.id) + } + } else { + if let localMediaProgress = syncLocalProgress() { + if Connectivity.isConnectedToInternet { + ApiClient.reportLocalMediaProgress(localMediaProgress) { success in + NSLog("Synced local media progress: \(success)") + } + } + } + } + } + + private static func syncLocalProgress() -> LocalMediaProgress? { + guard let session = session else { return nil } + + let localMediaProgress = LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: session.localMediaProgressId, localLibraryItemId: session.localLibraryItem?.id, localEpisodeId: session.episodeId) + guard var localMediaProgress = localMediaProgress else { + // Local media progress should have been created + // If we're here, it means a library id is invalid + return nil + } + + localMediaProgress.updateFromPlaybackSession(session) + Database.shared.saveLocalMediaProgress(localMediaProgress) + + // TODO: Send local media progress back to UI + + return localMediaProgress } } diff --git a/ios/App/Shared/util/ApiClient.swift b/ios/App/Shared/util/ApiClient.swift index 58507ed6..fd4f17ba 100644 --- a/ios/App/Shared/util/ApiClient.swift +++ b/ios/App/Shared/util/ApiClient.swift @@ -38,7 +38,7 @@ class ApiClient { } } - public static func postResource(endpoint: String, parameters: [String: String], callback: ((_ success: Bool) -> Void)?) { + public static func postResource(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) { if (Store.serverConfig == nil) { NSLog("Server config not set") callback?(false) @@ -144,6 +144,10 @@ class ApiClient { try? postResource(endpoint: "api/session/\(sessionId)/sync", parameters: report.asDictionary().mapValues({ value in "\(value)" }), callback: nil) } + public static func reportLocalMediaProgress(_ localMediaProgress: LocalMediaProgress, callback: @escaping (_ success: Bool) -> Void) { + postResource(endpoint: "/api/session/local", parameters: localMediaProgress, callback: callback) + } + public static func syncMediaProgress(callback: @escaping (_ results: LocalMediaProgressSyncResultsPayload) -> Void) { let localMediaProgressList = Database.shared.getAllLocalMediaProgress().filter { $0.serverConnectionConfigId == Store.serverConfig?.id @@ -205,3 +209,10 @@ struct LocalMediaProgressSyncResultsPayload: Codable { var numServerProgressUpdates: Int? var numLocalProgressUpdates: Int? } + +struct Connectivity { + static private let sharedInstance = NetworkReachabilityManager()! + static var isConnectedToInternet:Bool { + return self.sharedInstance.isReachable + } +}