// // PlayerProgressSync.swift // App // // Created by Ron Heft on 8/19/22. // import Foundation import UIKit import RealmSwift class PlayerProgress { public static let shared = PlayerProgress() private static let TIME_BETWEEN_SESSION_SYNC_IN_SECONDS = 10.0 private init() {} // MARK: - SYNC HOOKS public func syncFromPlayer(currentTime: Double, includesPlayProgress: Bool, isStopping: Bool) async { let backgroundToken = await UIApplication.shared.beginBackgroundTask(withName: "ABS:syncFromPlayer") let session = await updateLocalSessionFromPlayer(currentTime: currentTime, includesPlayProgress: includesPlayProgress) updateLocalMediaProgressFromLocalSession() if let session = session { await updateServerSessionFromLocalSession(session, rateLimitSync: !isStopping) } await UIApplication.shared.endBackgroundTask(backgroundToken) } public func syncToServer() async { let backgroundToken = await UIApplication.shared.beginBackgroundTask(withName: "ABS:syncToServer") updateAllServerSessionFromLocalSession() await UIApplication.shared.endBackgroundTask(backgroundToken) } public func syncFromServer() async { let backgroundToken = await UIApplication.shared.beginBackgroundTask(withName: "ABS:syncFromServer") await updateLocalSessionFromServerMediaProgress() await UIApplication.shared.endBackgroundTask(backgroundToken) } // MARK: - SYNC LOGIC private func updateLocalSessionFromPlayer(currentTime: Double, includesPlayProgress: Bool) async -> PlaybackSession? { guard let session = PlayerHandler.getPlaybackSession() else { return nil } let now = Date().timeIntervalSince1970 * 1000 let lastUpdate = session.updatedAt ?? now let timeSinceLastUpdate = now - lastUpdate session.update { session.currentTime = currentTime session.updatedAt = now if includesPlayProgress { session.timeListening += timeSinceLastUpdate } } return session.freeze() } private func updateLocalMediaProgressFromLocalSession() { guard let session = PlayerHandler.getPlaybackSession() else { return } guard session.isLocal else { return } let localMediaProgress = LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: session.localMediaProgressId, localLibraryItemId: session.localLibraryItem?.id, localEpisodeId: session.episodeId) guard let localMediaProgress = localMediaProgress else { // Local media progress should have been created // If we're here, it means a library id is invalid return } localMediaProgress.updateFromPlaybackSession(session) Database.shared.saveLocalMediaProgress(localMediaProgress) NSLog("Local progress saved to the database") // Send the local progress back to front-end NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.localProgress.rawValue), object: nil) } private func updateAllServerSessionFromLocalSession() { let sessions = try! Realm().objects(PlaybackSession.self).where({ $0.serverConnectionConfigId == Store.serverConfig?.id }) for session in sessions { let session = session.freeze() Task { await updateServerSessionFromLocalSession(session) } } } private func updateServerSessionFromLocalSession(_ session: PlaybackSession, rateLimitSync: Bool = false) async { // If required, rate limit requests based on session last update if rateLimitSync { let now = Date().timeIntervalSince1970 * 1000 let lastUpdate = session.updatedAt ?? now let timeSinceLastSync = now - lastUpdate let timeBetweenSessionSync = PlayerProgress.TIME_BETWEEN_SESSION_SYNC_IN_SECONDS * 1000 guard timeSinceLastSync > timeBetweenSessionSync else { // Skipping sync since last occurred within session sync time return } } NSLog("Sending sessionId(\(session.id)) to server") var success = false if session.isLocal { success = await ApiClient.reportLocalPlaybackProgress(session) } else { let playbackReport = PlaybackReport(currentTime: session.currentTime, duration: session.duration, timeListened: session.timeListening) success = await ApiClient.reportPlaybackProgress(report: playbackReport, sessionId: session.id) } // Remove old sessions after they synced with the server if success && !session.isActiveSession { NSLog("Deleting sessionId(\(session.id)) as is no longer active") session.thaw()?.delete() } } private func updateLocalSessionFromServerMediaProgress() async { NSLog("checkCurrentSessionProgress: Checking if local media progress was updated on server") guard let session = PlayerHandler.getPlaybackSession()?.freeze() else { return } // Fetch the current progress let progress = await ApiClient.getMediaProgress(libraryItemId: session.libraryItemId!, episodeId: session.episodeId) guard let progress = progress else { return } // Determine which session is newer let serverLastUpdate = progress.lastUpdate guard let localLastUpdate = session.updatedAt else { return } let serverCurrentTime = progress.currentTime let localCurrentTime = session.currentTime let serverIsNewerThanLocal = serverLastUpdate > localLastUpdate let currentTimeIsDifferent = serverCurrentTime != localCurrentTime // Update the session, if needed if serverIsNewerThanLocal && currentTimeIsDifferent { guard let session = session.thaw() else { return } session.update { session.currentTime = serverCurrentTime session.updatedAt = serverLastUpdate } PlayerHandler.seek(amount: session.currentTime) } } }