mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-31 08:04:46 +02:00
221 lines
9.3 KiB
Swift
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")
|
|
}
|
|
}
|
|
|
|
}
|