advplyr.audiobookshelf-app/ios/App/Shared/player/PlayerProgress.swift
2023-01-24 17:35:12 -05:00

221 lines
9.3 KiB
Swift

//
// 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 var TIME_BETWEEN_SESSION_SYNC_IN_SECONDS = 15.0
private let logger = AppLogger(category: "PlayerProgress")
private init() {}
// MARK: - SYNC HOOKS
public func syncFromPlayer(currentTime: Double, includesPlayProgress: Bool, isStopping: Bool) async {
let backgroundToken = await UIApplication.shared.beginBackgroundTask(withName: "ABS:syncFromPlayer")
do {
let session = try updateLocalSessionFromPlayer(currentTime: currentTime, includesPlayProgress: includesPlayProgress)
try updateLocalMediaProgressFromLocalSession()
if let session = session {
try await updateServerSessionFromLocalSession(session, rateLimitSync: !isStopping)
}
} catch {
logger.error("Failed to syncFromPlayer")
logger.error(error)
}
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 {
try await updateLocalSessionFromServerMediaProgress()
} catch {
logger.error("Failed to syncFromServer")
logger.error(error)
}
await UIApplication.shared.endBackgroundTask(backgroundToken)
}
// MARK: - SYNC LOGIC
private func updateLocalSessionFromPlayer(currentTime: Double, includesPlayProgress: Bool) throws -> PlaybackSession? {
guard let session = PlayerHandler.getPlaybackSession() else { return nil }
guard !currentTime.isNaN else { return nil } // Prevent bad data on player stop
try session.update {
session.realm?.refresh()
let nowInSeconds = Date().timeIntervalSince1970
let nowInMilliseconds = nowInSeconds * 1000
let lastUpdateInMilliseconds = session.updatedAt ?? nowInMilliseconds
let lastUpdateInSeconds = lastUpdateInMilliseconds / 1000
let secondsSinceLastUpdate = nowInSeconds - lastUpdateInSeconds
session.currentTime = currentTime
session.updatedAt = nowInMilliseconds
if includesPlayProgress {
session.timeListening += secondsSinceLastUpdate
}
}
return session.freeze()
}
private func updateLocalMediaProgressFromLocalSession() throws {
guard let session = PlayerHandler.getPlaybackSession() else { return }
guard session.isLocal else { return }
let localMediaProgress = try 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
}
try localMediaProgress.updateFromPlaybackSession(session)
logger.log("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() async throws {
try await withThrowingTaskGroup(of: Void.self) { [self] group in
for session in try Realm(queue: nil).objects(PlaybackSession.self).where({ $0.serverConnectionConfigId == Store.serverConfig?.id }) {
let session = session.freeze()
group.addTask {
try await self.updateServerSessionFromLocalSession(session)
}
}
try await group.waitForAll()
}
}
private func updateServerSessionFromLocalSession(_ session: PlaybackSession, rateLimitSync: Bool = false) async throws {
if ProcessInfo.processInfo.isLowPowerModeEnabled == true {
PlayerProgress.TIME_BETWEEN_SESSION_SYNC_IN_SECONDS = 60.0
} else {
PlayerProgress.TIME_BETWEEN_SESSION_SYNC_IN_SECONDS = 15.0
}
var safeToSync = true
guard var session = session.thaw() else { return }
// We need to update and check the server time in a transaction for thread-safety
try session.update {
session.realm?.refresh()
let nowInMilliseconds = Date().timeIntervalSince1970 * 1000
let lastUpdateInMilliseconds = session.serverUpdatedAt
// If required, rate limit requests based on session last update
if rateLimitSync {
let timeSinceLastSync = nowInMilliseconds - lastUpdateInMilliseconds
let timeBetweenSessionSync = PlayerProgress.TIME_BETWEEN_SESSION_SYNC_IN_SECONDS * 1000
safeToSync = timeSinceLastSync > timeBetweenSessionSync
if !safeToSync {
return // This only exits the update block
}
}
session.serverUpdatedAt = nowInMilliseconds
}
session = session.freeze()
guard safeToSync else { return }
logger.log("Sending sessionId(\(session.id)) to server with currentTime(\(session.currentTime))")
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)
// Reset time listening becuase server expects that time to be since last sync, not for the whole session
if success {
if let session = session.thaw() {
try session.update {
session.timeListening = 0
}
}
}
}
// Remove old sessions after they synced with the server
if success && !session.isActiveSession {
if let session = session.thaw() {
try session.delete()
}
}
}
private func updateLocalSessionFromServerMediaProgress() async throws {
logger.log("updateLocalSessionFromServerMediaProgress: Checking if local media progress was updated on server")
guard let session = try Realm(queue: nil).objects(PlaybackSession.self).last(where: {
$0.isActiveSession == true && $0.serverConnectionConfigId == Store.serverConfig?.id
})?.freeze() else {
logger.log("updateLocalSessionFromServerMediaProgress: Failed to get session")
return
}
// Fetch the current progress
let progress = await ApiClient.getMediaProgress(libraryItemId: session.libraryItemId!, episodeId: session.episodeId)
guard let progress = progress else {
logger.log("updateLocalSessionFromServerMediaProgress: No progress object")
return
}
// Determine which session is newer
let serverLastUpdate = progress.lastUpdate
guard let localLastUpdate = session.updatedAt else {
logger.log("updateLocalSessionFromServerMediaProgress: No local session updatedAt")
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 {
logger.log("updateLocalSessionFromServerMediaProgress: Server has newer time than local serverLastUpdate=\(serverLastUpdate) localLastUpdate=\(localLastUpdate)")
guard let session = session.thaw() else { return }
try session.update {
session.currentTime = serverCurrentTime
session.updatedAt = serverLastUpdate
}
logger.log("updateLocalSessionFromServerMediaProgress: Updated session currentTime newCurrentTime=\(serverCurrentTime) previousCurrentTime=\(localCurrentTime)")
PlayerHandler.seek(amount: session.currentTime)
} else {
logger.log("updateLocalSessionFromServerMediaProgress: Local session does not need updating; local has latest progress")
}
}
}