Sync local progress with server progress

This commit is contained in:
ronaldheft 2022-08-12 23:11:09 -04:00
parent 8d38f3358e
commit ef661bba37
6 changed files with 89 additions and 26 deletions

View file

@ -179,12 +179,17 @@ public class AbsAudioPlayer: CAPPlugin {
"value": PlayerHandler.getCurrentTime() "value": PlayerHandler.getCurrentTime()
]) ])
} }
@objc func sendSleepTimerSet() { @objc func sendSleepTimerSet() {
self.notifyListeners("onSleepTimerSet", data: [ self.notifyListeners("onSleepTimerSet", data: [
"value": PlayerHandler.remainingSleepTime "value": PlayerHandler.remainingSleepTime
]) ])
} }
@objc func onLocalMediaProgressUpdate(_ localMediaProgress: [String: Any]) {
self.notifyListeners("onLocalMediaProgressUpdate", data: localMediaProgress)
}
@objc func onPlaybackFailed() { @objc func onPlaybackFailed() {
if (PlayerHandler.getPlayMethod() == PlayMethod.directplay.rawValue) { if (PlayerHandler.getPlayMethod() == PlayMethod.directplay.rawValue) {
let playbackSession = PlayerHandler.getPlaybackSession() let playbackSession = PlayerHandler.getPlaybackSession()
@ -219,6 +224,7 @@ public class AbsAudioPlayer: CAPPlugin {
"playWhenReady": playWhenReady, "playWhenReady": playWhenReady,
]) ])
} }
@objc func sendPlaybackSession(session: [String: Any]) { @objc func sendPlaybackSession(session: [String: Any]) {
self.notifyListeners("onPlaybackSession", data: session) self.notifyListeners("onPlaybackSession", data: session)
} }

View file

@ -166,7 +166,7 @@ public class AbsDatabase: CAPPlugin {
return 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 { guard var localMediaProgress = localMediaProgress else {
call.reject("Local media progress not found or created") call.reject("Local media progress not found or created")
return return
@ -190,7 +190,7 @@ public class AbsDatabase: CAPPlugin {
NSLog("updateLocalMediaProgressFinished \(localMediaProgressId ?? "Unknown") | Is Finished: \(isFinished)") 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 { guard var localMediaProgress = localMediaProgress else {
call.resolve(["error": "Library Item not found"]) call.resolve(["error": "Library Item not found"])
return 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) { @objc func updateDeviceSettings(_ call: CAPPluginCall) {
let disableAutoRewind = call.getBool("disableAutoRewind") ?? false let disableAutoRewind = call.getBool("disableAutoRewind") ?? false
let enableAltView = call.getBool("enableAltView") ?? false let enableAltView = call.getBool("enableAltView") ?? false

View file

@ -196,4 +196,16 @@ extension LocalMediaProgress {
self.finishedAt = serverMediaProgress.finishedAt self.finishedAt = serverMediaProgress.finishedAt
self.startedAt = serverMediaProgress.startedAt 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
}
}
} }

View file

@ -7,7 +7,7 @@
import Foundation import Foundation
struct PlaybackSession: Decodable, Encodable { struct PlaybackSession: Codable {
var id: String var id: String
var userId: String? var userId: String?
var libraryItemId: String? var libraryItemId: String?
@ -30,10 +30,21 @@ struct PlaybackSession: Decodable, Encodable {
var serverConnectionConfigId: String? var serverConnectionConfigId: String?
var serverAddress: 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 totalDuration: Double {
var total = 0.0 var total = 0.0
self.audioTracks.forEach { total += $0.duration } self.audioTracks.forEach { total += $0.duration }
return total return total
} }
var progress: Double { self.currentTime / self.totalDuration } var progress: Double { self.currentTime / self.totalDuration }
} }

View file

@ -11,7 +11,7 @@ class PlayerHandler {
private static var player: AudioPlayer? private static var player: AudioPlayer?
private static var session: PlaybackSession? private static var session: PlaybackSession?
private static var timer: Timer? 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 private static var _remainingSleepTime: Int? = nil
public static var remainingSleepTime: Int? { public static var remainingSleepTime: Int? {
@ -69,6 +69,7 @@ class PlayerHandler {
} }
} }
} }
public static func stopPlayback() { public static func stopPlayback() {
player?.destroy() player?.destroy()
player = nil player = nil
@ -82,12 +83,15 @@ class PlayerHandler {
public static func getCurrentTime() -> Double? { public static func getCurrentTime() -> Double? {
self.player?.getCurrentTime() self.player?.getCurrentTime()
} }
public static func setPlaybackSpeed(speed: Float) { public static func setPlaybackSpeed(speed: Float) {
self.player?.setPlaybackRate(speed) self.player?.setPlaybackRate(speed)
} }
public static func getPlayMethod() -> Int? { public static func getPlayMethod() -> Int? {
self.player?.getPlayMethod() self.player?.getPlayMethod()
} }
public static func getPlaybackSession() -> PlaybackSession? { public static func getPlaybackSession() -> PlaybackSession? {
self.player?.getPlaybackSession() self.player?.getPlaybackSession()
} }
@ -100,6 +104,7 @@ class PlayerHandler {
let destinationTime = player.getCurrentTime() + amount let destinationTime = player.getCurrentTime() + amount
player.seek(destinationTime, from: "handler") player.seek(destinationTime, from: "handler")
} }
public static func seekBackward(amount: Double) { public static func seekBackward(amount: Double) {
guard let player = player else { guard let player = player else {
return return
@ -108,9 +113,11 @@ class PlayerHandler {
let destinationTime = player.getCurrentTime() - amount let destinationTime = player.getCurrentTime() - amount
player.seek(destinationTime, from: "handler") player.seek(destinationTime, from: "handler")
} }
public static func seek(amount: Double) { public static func seek(amount: Double) {
player?.seek(amount, from: "handler") player?.seek(amount, from: "handler")
} }
public static func getMetdata() -> [String: Any] { public static func getMetdata() -> [String: Any] {
DispatchQueue.main.async { DispatchQueue.main.async {
syncProgress() syncProgress()
@ -140,15 +147,15 @@ class PlayerHandler {
remainingSleepTime! -= 1 remainingSleepTime! -= 1
} }
} }
public static func syncProgress() { public static func syncProgress() {
if session == nil { return } if session == nil { return }
guard let player = player else { return } guard let player = player else { return }
// Prevent a sync at the current time
let playerCurrentTime = player.getCurrentTime() let playerCurrentTime = player.getCurrentTime()
if (lastSyncReport != nil && lastSyncReport?.currentTime == playerCurrentTime) { let hasSyncAtCurrentTime = lastSyncReport?.currentTime.isEqual(to: playerCurrentTime) ?? false
// No need to syncProgress if hasSyncAtCurrentTime { return }
return
}
// Prevent multiple sync requests // Prevent multiple sync requests
let timeSinceLastSync = Date().timeIntervalSince1970 - lastSyncTime let timeSinceLastSync = Date().timeIntervalSince1970 - lastSyncTime
@ -158,15 +165,44 @@ class PlayerHandler {
} }
lastSyncTime = Date().timeIntervalSince1970 // seconds lastSyncTime = Date().timeIntervalSince1970 // seconds
let report = PlaybackReport(currentTime: playerCurrentTime, duration: player.getDuration(), timeListened: listeningTimePassedSinceLastSync) let report = PlaybackReport(currentTime: playerCurrentTime, duration: player.getDuration(), timeListened: listeningTimePassedSinceLastSync)
session!.currentTime = playerCurrentTime session!.currentTime = playerCurrentTime
listeningTimePassedSinceLastSync = 0 listeningTimePassedSinceLastSync = 0
lastSyncReport = report lastSyncReport = report
// TODO: check if online let sessionIsLocal = session!.isLocal
if !sessionIsLocal {
if Connectivity.isConnectedToInternet {
NSLog("sending playback report") NSLog("sending playback report")
ApiClient.reportPlaybackProgress(report: report, sessionId: session!.id) 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
}
} }

View file

@ -38,7 +38,7 @@ class ApiClient {
} }
} }
public static func postResource(endpoint: String, parameters: [String: String], callback: ((_ success: Bool) -> Void)?) { public static func postResource<T:Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) {
if (Store.serverConfig == nil) { if (Store.serverConfig == nil) {
NSLog("Server config not set") NSLog("Server config not set")
callback?(false) callback?(false)
@ -144,6 +144,10 @@ class ApiClient {
try? postResource(endpoint: "api/session/\(sessionId)/sync", parameters: report.asDictionary().mapValues({ value in "\(value)" }), callback: nil) 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) { public static func syncMediaProgress(callback: @escaping (_ results: LocalMediaProgressSyncResultsPayload) -> Void) {
let localMediaProgressList = Database.shared.getAllLocalMediaProgress().filter { let localMediaProgressList = Database.shared.getAllLocalMediaProgress().filter {
$0.serverConnectionConfigId == Store.serverConfig?.id $0.serverConnectionConfigId == Store.serverConfig?.id
@ -205,3 +209,10 @@ struct LocalMediaProgressSyncResultsPayload: Codable {
var numServerProgressUpdates: Int? var numServerProgressUpdates: Int?
var numLocalProgressUpdates: Int? var numLocalProgressUpdates: Int?
} }
struct Connectivity {
static private let sharedInstance = NetworkReachabilityManager()!
static var isConnectedToInternet:Bool {
return self.sharedInstance.isReachable
}
}