2022-08-19 22:15:06 -04:00
//
// P l a y e r P r o g r e s s S y n c . s w i f t
// A p p
//
// C r e a t e d b y R o n H e f t o n 8 / 1 9 / 2 2 .
//
import Foundation
import UIKit
import RealmSwift
class PlayerProgress {
2022-08-21 12:06:37 -04:00
public static let shared = PlayerProgress ( )
2023-01-24 17:35:12 -05:00
private static var TIME_BETWEEN_SESSION_SYNC_IN_SECONDS = 15.0
2022-08-22 17:04:48 -04:00
2022-09-06 21:26:48 -04:00
private let logger = AppLogger ( category : " PlayerProgress " )
2022-08-19 22:15:06 -04:00
private init ( ) { }
2022-08-22 17:04:48 -04:00
// MARK: - S Y N C H O O K S
public func syncFromPlayer ( currentTime : Double , includesPlayProgress : Bool , isStopping : Bool ) async {
2022-08-21 12:36:29 -04:00
let backgroundToken = await UIApplication . shared . beginBackgroundTask ( withName : " ABS:syncFromPlayer " )
2022-08-25 15:42:37 -04:00
do {
let session = try updateLocalSessionFromPlayer ( currentTime : currentTime , includesPlayProgress : includesPlayProgress )
try updateLocalMediaProgressFromLocalSession ( )
if let session = session {
try await updateServerSessionFromLocalSession ( session , rateLimitSync : ! isStopping )
}
} catch {
2022-09-06 21:26:48 -04:00
logger . error ( " Failed to syncFromPlayer " )
logger . error ( error )
2022-08-22 17:04:48 -04:00
}
2022-08-21 12:36:29 -04:00
await UIApplication . shared . endBackgroundTask ( backgroundToken )
2022-08-19 22:15:06 -04:00
}
2022-08-21 12:06:37 -04:00
public func syncToServer ( ) async {
2022-08-19 23:00:40 -04:00
let backgroundToken = await UIApplication . shared . beginBackgroundTask ( withName : " ABS:syncToServer " )
2022-08-25 15:42:37 -04:00
do {
try await updateAllServerSessionFromLocalSession ( )
} catch {
2022-09-06 21:26:48 -04:00
logger . error ( " Failed to syncToServer " )
logger . error ( error )
2022-08-25 15:42:37 -04:00
}
2022-08-19 23:00:40 -04:00
await UIApplication . shared . endBackgroundTask ( backgroundToken )
2022-08-19 22:15:06 -04:00
}
2022-08-21 12:06:37 -04:00
public func syncFromServer ( ) async {
2022-08-19 23:00:40 -04:00
let backgroundToken = await UIApplication . shared . beginBackgroundTask ( withName : " ABS:syncFromServer " )
2022-08-25 15:42:37 -04:00
do {
try await updateLocalSessionFromServerMediaProgress ( )
} catch {
2022-09-06 21:26:48 -04:00
logger . error ( " Failed to syncFromServer " )
logger . error ( error )
2022-08-25 15:42:37 -04:00
}
2022-08-19 22:15:06 -04:00
await UIApplication . shared . endBackgroundTask ( backgroundToken )
}
2022-08-22 17:04:48 -04:00
// MARK: - S Y N C L O G I C
2022-08-25 15:42:37 -04:00
private func updateLocalSessionFromPlayer ( currentTime : Double , includesPlayProgress : Bool ) throws -> PlaybackSession ? {
2022-08-24 19:57:39 -04:00
guard let session = PlayerHandler . getPlaybackSession ( ) else { return nil }
guard ! currentTime . isNaN else { return nil } // P r e v e n t b a d d a t a o n p l a y e r s t o p
2022-08-25 15:42:37 -04:00
try session . update {
2022-08-24 19:57:39 -04:00
session . realm ? . refresh ( )
2022-08-22 17:04:48 -04:00
2022-08-24 19:57:39 -04:00
let nowInSeconds = Date ( ) . timeIntervalSince1970
let nowInMilliseconds = nowInSeconds * 1000
let lastUpdateInMilliseconds = session . updatedAt ? ? nowInMilliseconds
let lastUpdateInSeconds = lastUpdateInMilliseconds / 1000
let secondsSinceLastUpdate = nowInSeconds - lastUpdateInSeconds
2022-08-23 22:37:28 -04:00
2022-08-24 19:57:39 -04:00
session . currentTime = currentTime
session . updatedAt = nowInMilliseconds
if includesPlayProgress {
session . timeListening += secondsSinceLastUpdate
}
2022-08-22 17:04:48 -04:00
}
2022-08-24 19:57:39 -04:00
return session . freeze ( )
2022-08-21 12:36:29 -04:00
}
2022-08-25 15:42:37 -04:00
private func updateLocalMediaProgressFromLocalSession ( ) throws {
2022-08-24 19:57:39 -04:00
guard let session = PlayerHandler . getPlaybackSession ( ) else { return }
guard session . isLocal else { return }
2022-08-25 15:42:37 -04:00
let localMediaProgress = try LocalMediaProgress . fetchOrCreateLocalMediaProgress ( localMediaProgressId : session . localMediaProgressId , localLibraryItemId : session . localLibraryItem ? . id , localEpisodeId : session . episodeId )
2022-08-24 19:57:39 -04:00
guard let localMediaProgress = localMediaProgress else {
// L o c a l m e d i a p r o g r e s s s h o u l d h a v e b e e n c r e a t e d
// I f w e ' r e h e r e , i t m e a n s a l i b r a r y i d i s i n v a l i d
return
2022-08-23 22:37:28 -04:00
}
2022-08-24 19:57:39 -04:00
2022-08-25 15:42:37 -04:00
try localMediaProgress . updateFromPlaybackSession ( session )
2022-08-24 19:57:39 -04:00
2022-09-08 20:09:35 -04:00
logger . log ( " Local progress saved to the database " )
2022-08-24 19:57:39 -04:00
// S e n d t h e l o c a l p r o g r e s s b a c k t o f r o n t - e n d
NotificationCenter . default . post ( name : NSNotification . Name ( PlayerEvents . localProgress . rawValue ) , object : nil )
2022-08-19 22:15:06 -04:00
}
2022-08-25 15:42:37 -04:00
private func updateAllServerSessionFromLocalSession ( ) async throws {
try await withThrowingTaskGroup ( of : Void . self ) { [ self ] group in
2022-09-17 17:47:18 -04:00
for session in try Realm ( queue : nil ) . objects ( PlaybackSession . self ) . where ( { $0 . serverConnectionConfigId = = Store . serverConfig ? . id } ) {
2022-08-23 18:22:11 -04:00
let session = session . freeze ( )
group . addTask {
2022-08-25 15:42:37 -04:00
try await self . updateServerSessionFromLocalSession ( session )
2022-08-23 18:22:11 -04:00
}
}
2022-08-25 15:42:37 -04:00
try await group . waitForAll ( )
2022-08-19 23:00:40 -04:00
}
}
2022-08-25 15:42:37 -04:00
private func updateServerSessionFromLocalSession ( _ session : PlaybackSession , rateLimitSync : Bool = false ) async throws {
2023-01-24 17:35:12 -05:00
if ProcessInfo . processInfo . isLowPowerModeEnabled = = true {
PlayerProgress . TIME_BETWEEN_SESSION_SYNC_IN_SECONDS = 60.0
} else {
PlayerProgress . TIME_BETWEEN_SESSION_SYNC_IN_SECONDS = 15.0
}
2022-08-24 19:57:39 -04:00
var safeToSync = true
guard var session = session . thaw ( ) else { return }
// W e n e e d t o u p d a t e a n d c h e c k t h e s e r v e r t i m e i n a t r a n s a c t i o n f o r t h r e a d - s a f e t y
2022-08-25 15:42:37 -04:00
try session . update {
2022-08-24 19:57:39 -04:00
session . realm ? . refresh ( )
2022-08-23 18:51:30 -04:00
2022-08-24 19:57:39 -04:00
let nowInMilliseconds = Date ( ) . timeIntervalSince1970 * 1000
let lastUpdateInMilliseconds = session . serverUpdatedAt
// I f r e q u i r e d , r a t e l i m i t r e q u e s t s b a s e d o n s e s s i o n l a s t u p d a t e
if rateLimitSync {
let timeSinceLastSync = nowInMilliseconds - lastUpdateInMilliseconds
let timeBetweenSessionSync = PlayerProgress . TIME_BETWEEN_SESSION_SYNC_IN_SECONDS * 1000
safeToSync = timeSinceLastSync > timeBetweenSessionSync
if ! safeToSync {
return // T h i s o n l y e x i t s t h e u p d a t e b l o c k
2022-08-23 18:51:30 -04:00
}
2022-08-22 17:04:48 -04:00
}
2022-08-24 19:57:39 -04:00
session . serverUpdatedAt = nowInMilliseconds
2022-08-22 17:04:48 -04:00
}
2022-08-24 19:57:39 -04:00
session = session . freeze ( )
2022-08-22 17:04:48 -04:00
2022-08-24 19:57:39 -04:00
guard safeToSync else { return }
2022-09-08 20:09:35 -04:00
logger . log ( " Sending sessionId( \( session . id ) ) to server with currentTime( \( session . currentTime ) ) " )
2022-08-19 22:15:06 -04:00
2022-08-19 23:00:40 -04:00
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 )
2022-12-07 14:39:26 -05:00
// R e s e t t i m e l i s t e n i n g b e c u a s e s e r v e r e x p e c t s t h a t t i m e t o b e s i n c e l a s t s y n c , n o t f o r t h e w h o l e s e s s i o n
if success {
if let session = session . thaw ( ) {
try session . update {
session . timeListening = 0
}
}
}
2022-08-19 23:00:40 -04:00
}
2022-08-23 22:37:28 -04:00
2022-08-23 18:51:30 -04:00
// R e m o v e o l d s e s s i o n s a f t e r t h e y s y n c e d w i t h t h e s e r v e r
if success && ! session . isActiveSession {
2022-08-24 19:57:39 -04:00
if let session = session . thaw ( ) {
2022-08-25 15:42:37 -04:00
try session . delete ( )
2022-08-23 17:32:43 -04:00
}
2022-08-19 23:00:40 -04:00
}
2022-08-19 22:15:06 -04:00
}
2022-08-25 15:42:37 -04:00
private func updateLocalSessionFromServerMediaProgress ( ) async throws {
2022-09-08 20:09:35 -04:00
logger . log ( " updateLocalSessionFromServerMediaProgress: Checking if local media progress was updated on server " )
2022-09-17 17:47:18 -04:00
guard let session = try Realm ( queue : nil ) . objects ( PlaybackSession . self ) . last ( where : {
2022-08-23 18:56:08 -04:00
$0 . isActiveSession = = true && $0 . serverConnectionConfigId = = Store . serverConfig ? . id
} ) ? . freeze ( ) else {
2022-09-08 20:09:35 -04:00
logger . log ( " updateLocalSessionFromServerMediaProgress: Failed to get session " )
2022-08-21 10:59:43 -04:00
return
}
2022-08-19 22:15:06 -04:00
// F e t c h t h e c u r r e n t p r o g r e s s
let progress = await ApiClient . getMediaProgress ( libraryItemId : session . libraryItemId ! , episodeId : session . episodeId )
2022-08-21 10:59:43 -04:00
guard let progress = progress else {
2022-09-08 20:09:35 -04:00
logger . log ( " updateLocalSessionFromServerMediaProgress: No progress object " )
2022-08-21 10:59:43 -04:00
return
}
2022-08-19 22:15:06 -04:00
// D e t e r m i n e w h i c h s e s s i o n i s n e w e r
let serverLastUpdate = progress . lastUpdate
2022-08-21 10:59:43 -04:00
guard let localLastUpdate = session . updatedAt else {
2022-09-08 20:09:35 -04:00
logger . log ( " updateLocalSessionFromServerMediaProgress: No local session updatedAt " )
2022-08-21 10:59:43 -04:00
return
}
2022-08-19 22:15:06 -04:00
let serverCurrentTime = progress . currentTime
let localCurrentTime = session . currentTime
let serverIsNewerThanLocal = serverLastUpdate > localLastUpdate
let currentTimeIsDifferent = serverCurrentTime != localCurrentTime
// U p d a t e t h e s e s s i o n , i f n e e d e d
if serverIsNewerThanLocal && currentTimeIsDifferent {
2022-09-08 20:09:35 -04:00
logger . log ( " updateLocalSessionFromServerMediaProgress: Server has newer time than local serverLastUpdate= \( serverLastUpdate ) localLastUpdate= \( localLastUpdate ) " )
2022-08-24 19:57:39 -04:00
guard let session = session . thaw ( ) else { return }
2022-08-25 15:42:37 -04:00
try session . update {
2022-08-24 19:57:39 -04:00
session . currentTime = serverCurrentTime
session . updatedAt = serverLastUpdate
2022-08-19 22:15:06 -04:00
}
2022-09-08 20:09:35 -04:00
logger . log ( " updateLocalSessionFromServerMediaProgress: Updated session currentTime newCurrentTime= \( serverCurrentTime ) previousCurrentTime= \( localCurrentTime ) " )
2022-08-24 19:57:39 -04:00
PlayerHandler . seek ( amount : session . currentTime )
2022-08-21 10:59:43 -04:00
} else {
2022-09-08 20:09:35 -04:00
logger . log ( " updateLocalSessionFromServerMediaProgress: Local session does not need updating; local has latest progress " )
2022-08-19 22:15:06 -04:00
}
}
}