mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-31 15:47:13 +02:00
Fix all kinds of syncing errors
This commit is contained in:
@@ -157,13 +157,11 @@ public class AbsDatabase: CAPPlugin {
|
|||||||
let localMediaProgressId = call.getString("localMediaProgressId")
|
let localMediaProgressId = call.getString("localMediaProgressId")
|
||||||
|
|
||||||
do {
|
do {
|
||||||
guard let localLibraryItemId = localLibraryItemId else {
|
|
||||||
call.reject("localLibraryItemId not specified")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let serverMediaProgress = serverMediaProgress else {
|
guard let serverMediaProgress = serverMediaProgress else {
|
||||||
call.reject("serverMediaProgress not specified")
|
return call.reject("serverMediaProgress not specified")
|
||||||
return
|
}
|
||||||
|
guard localLibraryItemId != nil || localMediaProgressId != nil else {
|
||||||
|
return call.reject("localLibraryItemId or localMediaProgressId must be specified")
|
||||||
}
|
}
|
||||||
|
|
||||||
let localMediaProgress = LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: localMediaProgressId, localLibraryItemId: localLibraryItemId, localEpisodeId: localEpisodeId)
|
let localMediaProgress = LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: localMediaProgressId, localLibraryItemId: localLibraryItemId, localEpisodeId: localEpisodeId)
|
||||||
|
|||||||
@@ -342,13 +342,13 @@ struct MediaProgress: Realmable, Codable {
|
|||||||
id = try values.decode(String.self, forKey: .id)
|
id = try values.decode(String.self, forKey: .id)
|
||||||
libraryItemId = try values.decode(String.self, forKey: .libraryItemId)
|
libraryItemId = try values.decode(String.self, forKey: .libraryItemId)
|
||||||
episodeId = try? values.decode(String.self, forKey: .episodeId)
|
episodeId = try? values.decode(String.self, forKey: .episodeId)
|
||||||
duration = try MediaProgress.doubleOrStringDecoder(from: decoder, with: values, key: .duration)
|
duration = try values.doubleOrStringDecoder(key: .duration)
|
||||||
progress = try MediaProgress.doubleOrStringDecoder(from: decoder, with: values, key: .progress)
|
progress = try values.doubleOrStringDecoder(key: .progress)
|
||||||
currentTime = try MediaProgress.doubleOrStringDecoder(from: decoder, with: values, key: .currentTime)
|
currentTime = try values.doubleOrStringDecoder(key: .currentTime)
|
||||||
isFinished = try values.decode(Bool.self, forKey: .isFinished)
|
isFinished = try values.decode(Bool.self, forKey: .isFinished)
|
||||||
lastUpdate = try MediaProgress.intOrStringDecoder(from: decoder, with: values, key: .lastUpdate)
|
lastUpdate = try values.intOrStringDecoder(key: .lastUpdate)
|
||||||
startedAt = try MediaProgress.intOrStringDecoder(from: decoder, with: values, key: .startedAt)
|
startedAt = try values.intOrStringDecoder(key: .startedAt)
|
||||||
finishedAt = try? MediaProgress.intOrStringDecoder(from: decoder, with: values, key: .finishedAt)
|
finishedAt = try? values.intOrStringDecoder(key: .finishedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
func encode(to encoder: Encoder) throws {
|
func encode(to encoder: Encoder) throws {
|
||||||
@@ -364,24 +364,6 @@ struct MediaProgress: Realmable, Codable {
|
|||||||
try container.encode(startedAt, forKey: .startedAt)
|
try container.encode(startedAt, forKey: .startedAt)
|
||||||
try container.encode(finishedAt, forKey: .finishedAt)
|
try container.encode(finishedAt, forKey: .finishedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
static private func doubleOrStringDecoder(from decoder: Decoder, with values: KeyedDecodingContainer<CodingKeys>, key: MediaProgress.CodingKeys) throws -> Double {
|
|
||||||
do {
|
|
||||||
return try values.decode(Double.self, forKey: key)
|
|
||||||
} catch {
|
|
||||||
let stringDuration = try values.decode(String.self, forKey: key)
|
|
||||||
return Double(stringDuration) ?? 0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static private func intOrStringDecoder(from decoder: Decoder, with values: KeyedDecodingContainer<CodingKeys>, key: MediaProgress.CodingKeys) throws -> Int {
|
|
||||||
do {
|
|
||||||
return try values.decode(Int.self, forKey: key)
|
|
||||||
} catch {
|
|
||||||
let stringDuration = try values.decode(String.self, forKey: key)
|
|
||||||
return Int(stringDuration) ?? 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PlaybackMetadata: Realmable, Codable {
|
struct PlaybackMetadata: Realmable, Codable {
|
||||||
|
|||||||
@@ -267,6 +267,7 @@ class AudioPlayer: NSObject {
|
|||||||
let startOffset = audioTrack.startOffset ?? 0.0
|
let startOffset = audioTrack.startOffset ?? 0.0
|
||||||
return startOffset + currentTrackTime
|
return startOffset + currentTrackTime
|
||||||
}
|
}
|
||||||
|
|
||||||
public func getPlayMethod() -> Int {
|
public func getPlayMethod() -> Int {
|
||||||
return self.playbackSession.playMethod
|
return self.playbackSession.playMethod
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,12 +71,16 @@ class PlayerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static func stopPlayback() {
|
public static func stopPlayback() {
|
||||||
player?.destroy()
|
// Pause playback first, so we can sync our current progress
|
||||||
player = nil
|
player?.pause()
|
||||||
|
|
||||||
|
// Stop updating progress before we destory the player, so we don't receive bad data
|
||||||
timer?.invalidate()
|
timer?.invalidate()
|
||||||
timer = nil
|
timer = nil
|
||||||
|
|
||||||
|
player?.destroy()
|
||||||
|
player = nil
|
||||||
|
|
||||||
NowPlayingInfo.shared.reset()
|
NowPlayingInfo.shared.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,6 +168,9 @@ class PlayerHandler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prevent a sync if we got junk data from the player (occurs when exiting out of memory
|
||||||
|
guard !playerCurrentTime.isNaN else { return }
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,27 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func postResource<T: Encodable, U: Decodable>(endpoint: String, parameters: T, decodable: U.Type = U.self, callback: ((_ param: U) -> Void)?) {
|
||||||
|
if (Store.serverConfig == nil) {
|
||||||
|
NSLog("Server config not set")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let headers: HTTPHeaders = [
|
||||||
|
"Authorization": "Bearer \(Store.serverConfig!.token)"
|
||||||
|
]
|
||||||
|
|
||||||
|
AF.request("\(Store.serverConfig!.address)/\(endpoint)", method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, headers: headers).responseDecodable(of: decodable) { response in
|
||||||
|
switch response.result {
|
||||||
|
case .success(let obj):
|
||||||
|
callback?(obj)
|
||||||
|
case .failure(let error):
|
||||||
|
NSLog("api request to \(endpoint) failed")
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static func postResource<T:Encodable>(endpoint: String, parameters: T, 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")
|
||||||
@@ -62,7 +83,7 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func patchResource<T:Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) {
|
public static func patchResource<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)
|
||||||
@@ -121,7 +142,7 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ApiClient.postResource(endpoint: endpoint, parameters: [
|
let parameters: [String: Any] = [
|
||||||
"forceDirectPlay": !forceTranscode ? "1" : "",
|
"forceDirectPlay": !forceTranscode ? "1" : "",
|
||||||
"forceTranscode": forceTranscode ? "1" : "",
|
"forceTranscode": forceTranscode ? "1" : "",
|
||||||
"mediaPlayer": "AVPlayer",
|
"mediaPlayer": "AVPlayer",
|
||||||
@@ -130,7 +151,8 @@ class ApiClient {
|
|||||||
"model": modelCode,
|
"model": modelCode,
|
||||||
"clientVersion": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
|
"clientVersion": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
|
||||||
]
|
]
|
||||||
], decodable: PlaybackSession.self) { obj in
|
]
|
||||||
|
ApiClient.postResource(endpoint: endpoint, parameters: parameters, decodable: PlaybackSession.self) { obj in
|
||||||
var session = obj
|
var session = obj
|
||||||
|
|
||||||
session.serverConnectionConfigId = Store.serverConfig!.id
|
session.serverConnectionConfigId = Store.serverConfig!.id
|
||||||
@@ -145,7 +167,7 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static func reportLocalMediaProgress(_ localMediaProgress: LocalMediaProgress, callback: @escaping (_ success: Bool) -> Void) {
|
public static func reportLocalMediaProgress(_ localMediaProgress: LocalMediaProgress, callback: @escaping (_ success: Bool) -> Void) {
|
||||||
postResource(endpoint: "/api/session/local", parameters: localMediaProgress, callback: callback)
|
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) {
|
||||||
@@ -156,8 +178,7 @@ class ApiClient {
|
|||||||
if ( !localMediaProgressList.isEmpty ) {
|
if ( !localMediaProgressList.isEmpty ) {
|
||||||
let payload = LocalMediaProgressSyncPayload(localMediaProgress: localMediaProgressList)
|
let payload = LocalMediaProgressSyncPayload(localMediaProgress: localMediaProgressList)
|
||||||
NSLog("Sending sync local progress request with \(localMediaProgressList.count) progress items")
|
NSLog("Sending sync local progress request with \(localMediaProgressList.count) progress items")
|
||||||
try? postResource(endpoint: "/api/me/sync-local-progress", parameters: payload.asDictionary(), decodable: MediaProgressSyncResponsePayload.self) { response in
|
postResource(endpoint: "api/me/sync-local-progress", parameters: payload, decodable: MediaProgressSyncResponsePayload.self) { response in
|
||||||
|
|
||||||
let resultsPayload = LocalMediaProgressSyncResultsPayload(numLocalMediaProgressForServer: localMediaProgressList.count, numServerProgressUpdates: response.numServerProgressUpdates, numLocalProgressUpdates: response.localProgressUpdates?.count)
|
let resultsPayload = LocalMediaProgressSyncResultsPayload(numLocalMediaProgressForServer: localMediaProgressList.count, numServerProgressUpdates: response.numServerProgressUpdates, numLocalProgressUpdates: response.localProgressUpdates?.count)
|
||||||
NSLog("Media Progress Sync | \(String(describing: try? resultsPayload.asDictionary()))")
|
NSLog("Media Progress Sync | \(String(describing: try? resultsPayload.asDictionary()))")
|
||||||
|
|
||||||
@@ -177,7 +198,7 @@ class ApiClient {
|
|||||||
|
|
||||||
public static func updateMediaProgress<T:Encodable>(libraryItemId: String, episodeId: String?, payload: T, callback: @escaping () -> Void) {
|
public static func updateMediaProgress<T:Encodable>(libraryItemId: String, episodeId: String?, payload: T, callback: @escaping () -> Void) {
|
||||||
NSLog("updateMediaProgress \(libraryItemId) \(episodeId ?? "NIL") \(payload)")
|
NSLog("updateMediaProgress \(libraryItemId) \(episodeId ?? "NIL") \(payload)")
|
||||||
let endpoint = episodeId?.isEmpty ?? true ? "/api/me/progress/\(libraryItemId)" : "/api/me/progress/\(libraryItemId)/\(episodeId ?? "")"
|
let endpoint = episodeId?.isEmpty ?? true ? "api/me/progress/\(libraryItemId)" : "api/me/progress/\(libraryItemId)/\(episodeId ?? "")"
|
||||||
patchResource(endpoint: endpoint, parameters: payload) { success in
|
patchResource(endpoint: endpoint, parameters: payload) { success in
|
||||||
callback()
|
callback()
|
||||||
}
|
}
|
||||||
@@ -199,9 +220,19 @@ struct LocalMediaProgressSyncPayload: Codable {
|
|||||||
var localMediaProgress: [LocalMediaProgress]
|
var localMediaProgress: [LocalMediaProgress]
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MediaProgressSyncResponsePayload: Codable {
|
struct MediaProgressSyncResponsePayload: Decodable {
|
||||||
var numServerProgressUpdates: Int?
|
var numServerProgressUpdates: Int?
|
||||||
var localProgressUpdates: [LocalMediaProgress]?
|
var localProgressUpdates: [LocalMediaProgress]?
|
||||||
|
|
||||||
|
private enum CodingKeys : String, CodingKey {
|
||||||
|
case numServerProgressUpdates, localProgressUpdates
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
numServerProgressUpdates = try? values.intOrStringDecoder(key: .numServerProgressUpdates)
|
||||||
|
localProgressUpdates = try? values.decode([LocalMediaProgress].self, forKey: .localProgressUpdates)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LocalMediaProgressSyncResultsPayload: Codable {
|
struct LocalMediaProgressSyncResultsPayload: Codable {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import RealmSwift
|
import RealmSwift
|
||||||
import Capacitor
|
import Capacitor
|
||||||
|
import CoreMedia
|
||||||
|
|
||||||
extension String: Error {}
|
extension String: Error {}
|
||||||
|
|
||||||
@@ -31,11 +32,31 @@ extension Collection where Iterator.Element: Encodable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension KeyedDecodingContainer {
|
||||||
|
func doubleOrStringDecoder(key: KeyedDecodingContainer<K>.Key) throws -> Double {
|
||||||
|
do {
|
||||||
|
return try decode(Double.self, forKey: key)
|
||||||
|
} catch {
|
||||||
|
let stringValue = try decode(String.self, forKey: key)
|
||||||
|
return Double(stringValue) ?? 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func intOrStringDecoder(key: KeyedDecodingContainer<K>.Key) throws -> Int {
|
||||||
|
do {
|
||||||
|
return try decode(Int.self, forKey: key)
|
||||||
|
} catch {
|
||||||
|
let stringValue = try decode(String.self, forKey: key)
|
||||||
|
return Int(stringValue) ?? 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension CAPPluginCall {
|
extension CAPPluginCall {
|
||||||
func getJson<T: Decodable>(_ key: String, type: T.Type) -> T? {
|
func getJson<T: Decodable>(_ key: String, type: T.Type) -> T? {
|
||||||
guard let value = getString(key) else { return nil }
|
guard let value = getObject(key) else { return nil }
|
||||||
guard let valueData = value.data(using: .utf8) else { return nil }
|
guard let json = try? JSONSerialization.data(withJSONObject: value) else { return nil }
|
||||||
return try? JSONDecoder().decode(type, from: valueData)
|
return try? JSONDecoder().decode(type, from: json)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user