mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-12 15:04:43 +02:00
API functions to sync local progress
This commit is contained in:
parent
f4e39ec7ca
commit
8d38f3358e
5 changed files with 218 additions and 10 deletions
|
@ -20,6 +20,9 @@ CAP_PLUGIN(AbsDatabase, "AbsDatabase",
|
||||||
CAP_PLUGIN_METHOD(getLocalLibraryItemByLId, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(getLocalLibraryItemByLId, CAPPluginReturnPromise);
|
||||||
CAP_PLUGIN_METHOD(getLocalLibraryItemsInFolder, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(getLocalLibraryItemsInFolder, CAPPluginReturnPromise);
|
||||||
CAP_PLUGIN_METHOD(getAllLocalMediaProgress, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(getAllLocalMediaProgress, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(removeLocalMediaProgress, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(syncLocalMediaProgressWithServer, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(syncServerMediaProgressWithLocalMediaProgress, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(updateLocalMediaProgressFinished, CAPPluginReturnPromise);
|
||||||
CAP_PLUGIN_METHOD(updateDeviceSettings, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(updateDeviceSettings, CAPPluginReturnPromise);
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import Capacitor
|
import Capacitor
|
||||||
import RealmSwift
|
import RealmSwift
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
extension String {
|
extension String {
|
||||||
|
|
||||||
|
@ -125,6 +126,111 @@ public class AbsDatabase: CAPPlugin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func removeLocalMediaProgress(_ call: CAPPluginCall) {
|
||||||
|
let localMediaProgressId = call.getString("localMediaProgressId")
|
||||||
|
guard let localMediaProgressId = localMediaProgressId else {
|
||||||
|
call.reject("localMediaProgressId not specificed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Database.shared.removeLocalMediaProgress(localMediaProgressId: localMediaProgressId)
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func syncLocalMediaProgressWithServer(_ call: CAPPluginCall) {
|
||||||
|
guard Store.serverConfig != nil else {
|
||||||
|
call.reject("syncLocalMediaProgressWithServer not connected to server")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ApiClient.syncMediaProgress { results in
|
||||||
|
do {
|
||||||
|
call.resolve(try results.asDictionary())
|
||||||
|
} catch {
|
||||||
|
call.reject("Failed to report synced media progress")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func syncServerMediaProgressWithLocalMediaProgress(_ call: CAPPluginCall) {
|
||||||
|
let serverMediaProgress = call.getJson("mediaProgress", type: MediaProgress.self)
|
||||||
|
let localLibraryItemId = call.getString("localLibraryItemId")
|
||||||
|
let localEpisodeId = call.getString("localEpisodeId")
|
||||||
|
let localMediaProgressId = call.getString("localMediaProgressId")
|
||||||
|
|
||||||
|
do {
|
||||||
|
guard let localLibraryItemId = localLibraryItemId else {
|
||||||
|
call.reject("localLibraryItemId not specified")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let serverMediaProgress = serverMediaProgress else {
|
||||||
|
call.reject("serverMediaProgress not specified")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let localMediaProgress = fetchOrCreateLocalMediaProgress(localMediaProgressId: localMediaProgressId, localLibraryItemId: localLibraryItemId, localEpisodeId: localEpisodeId)
|
||||||
|
guard var localMediaProgress = localMediaProgress else {
|
||||||
|
call.reject("Local media progress not found or created")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
localMediaProgress.updateFromServerMediaProgress(serverMediaProgress)
|
||||||
|
|
||||||
|
NSLog("syncServerMediaProgressWithLocalMediaProgress: Saving local media progress")
|
||||||
|
Database.shared.saveLocalMediaProgress(localMediaProgress)
|
||||||
|
call.resolve(try localMediaProgress.asDictionary())
|
||||||
|
} catch {
|
||||||
|
call.reject("Failed to sync media progress")
|
||||||
|
debugPrint(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func updateLocalMediaProgressFinished(_ call: CAPPluginCall) {
|
||||||
|
let localLibraryItemId = call.getString("localLibraryItemId")
|
||||||
|
let localEpisodeId = call.getString("localEpisodeId")
|
||||||
|
let localMediaProgressId = call.getString("localMediaProgressId")
|
||||||
|
let isFinished = call.getBool("isFinished", false)
|
||||||
|
|
||||||
|
NSLog("updateLocalMediaProgressFinished \(localMediaProgressId ?? "Unknown") | Is Finished: \(isFinished)")
|
||||||
|
|
||||||
|
let localMediaProgress = fetchOrCreateLocalMediaProgress(localMediaProgressId: localMediaProgressId, localLibraryItemId: localLibraryItemId, localEpisodeId: localEpisodeId)
|
||||||
|
guard var localMediaProgress = localMediaProgress else {
|
||||||
|
call.resolve(["error": "Library Item not found"])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update finished status
|
||||||
|
localMediaProgress.updateIsFinished(isFinished)
|
||||||
|
Database.shared.saveLocalMediaProgress(localMediaProgress)
|
||||||
|
|
||||||
|
// Build API response
|
||||||
|
let progressDictionary = try? localMediaProgress.asDictionary()
|
||||||
|
var response: [String: Any] = ["local": true, "server": false, "localMediaProgress": progressDictionary ?? ""]
|
||||||
|
|
||||||
|
// Send update to the server if logged in
|
||||||
|
let hasLinkedServer = localMediaProgress.serverConnectionConfigId != nil
|
||||||
|
let loggedIntoServer = Store.serverConfig?.id == localMediaProgress.serverConnectionConfigId
|
||||||
|
if hasLinkedServer && loggedIntoServer {
|
||||||
|
response["server"] = true
|
||||||
|
let payload = ["isFinished": isFinished]
|
||||||
|
ApiClient.updateMediaProgress(libraryItemId: localMediaProgress.libraryItemId!, episodeId: localEpisodeId, payload: payload) {
|
||||||
|
call.resolve(response)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
call.resolve(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
|
@ -130,24 +130,36 @@ extension LocalFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LocalMediaProgress {
|
extension LocalMediaProgress {
|
||||||
init(localLibraryItem: LocalLibraryItem, episode: PodcastEpisode?, progress: MediaProgress) {
|
init(localLibraryItem: LocalLibraryItem, episode: PodcastEpisode?) {
|
||||||
self.id = localLibraryItem.id
|
self.id = localLibraryItem.id
|
||||||
self.localLibraryItemId = localLibraryItem.id
|
self.localLibraryItemId = localLibraryItem.id
|
||||||
self.libraryItemId = localLibraryItem.libraryItemId
|
self.libraryItemId = localLibraryItem.libraryItemId
|
||||||
|
|
||||||
if let episode = episode {
|
|
||||||
self.id += "-\(episode.id)"
|
|
||||||
self.episodeId = episode.id
|
|
||||||
}
|
|
||||||
|
|
||||||
self.serverAddress = localLibraryItem.serverAddress
|
self.serverAddress = localLibraryItem.serverAddress
|
||||||
self.serverUserId = localLibraryItem.serverUserId
|
self.serverUserId = localLibraryItem.serverUserId
|
||||||
self.serverConnectionConfigId = localLibraryItem.serverConnectionConfigId
|
self.serverConnectionConfigId = localLibraryItem.serverConnectionConfigId
|
||||||
|
|
||||||
|
self.duration = localLibraryItem.getDuration()
|
||||||
|
self.progress = 0.0
|
||||||
|
self.currentTime = 0.0
|
||||||
|
self.isFinished = false
|
||||||
|
self.lastUpdate = Int(Date().timeIntervalSince1970)
|
||||||
|
self.startedAt = 0
|
||||||
|
self.finishedAt = nil
|
||||||
|
|
||||||
|
if let episode = episode {
|
||||||
|
self.id += "-\(episode.id)"
|
||||||
|
self.episodeId = episode.id
|
||||||
|
self.duration = episode.duration ?? 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(localLibraryItem: LocalLibraryItem, episode: PodcastEpisode?, progress: MediaProgress) {
|
||||||
|
self.init(localLibraryItem: localLibraryItem, episode: episode)
|
||||||
self.duration = progress.duration
|
self.duration = progress.duration
|
||||||
self.progress = progress.progress
|
self.progress = progress.progress
|
||||||
self.currentTime = progress.currentTime
|
self.currentTime = progress.currentTime
|
||||||
self.isFinished = false
|
self.isFinished = progress.isFinished
|
||||||
self.lastUpdate = progress.lastUpdate
|
self.lastUpdate = progress.lastUpdate
|
||||||
self.startedAt = progress.startedAt
|
self.startedAt = progress.startedAt
|
||||||
self.finishedAt = progress.finishedAt
|
self.finishedAt = progress.finishedAt
|
||||||
|
@ -158,6 +170,10 @@ extension LocalMediaProgress {
|
||||||
self.progress = finished ? 1.0 : 0.0
|
self.progress = finished ? 1.0 : 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.startedAt == 0 && finished {
|
||||||
|
self.startedAt = Int(Date().timeIntervalSince1970)
|
||||||
|
}
|
||||||
|
|
||||||
self.isFinished = finished
|
self.isFinished = finished
|
||||||
self.lastUpdate = Int(Date().timeIntervalSince1970)
|
self.lastUpdate = Int(Date().timeIntervalSince1970)
|
||||||
self.finishedAt = finished ? lastUpdate : nil
|
self.finishedAt = finished ? lastUpdate : nil
|
||||||
|
|
|
@ -37,6 +37,7 @@ class ApiClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func postResource(endpoint: String, parameters: [String: String], callback: ((_ success: Bool) -> Void)?) {
|
public static func postResource(endpoint: String, parameters: [String: String], callback: ((_ success: Bool) -> Void)?) {
|
||||||
if (Store.serverConfig == nil) {
|
if (Store.serverConfig == nil) {
|
||||||
NSLog("Server config not set")
|
NSLog("Server config not set")
|
||||||
|
@ -50,7 +51,7 @@ class ApiClient {
|
||||||
|
|
||||||
AF.request("\(Store.serverConfig!.address)/\(endpoint)", method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, headers: headers).response { response in
|
AF.request("\(Store.serverConfig!.address)/\(endpoint)", method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, headers: headers).response { response in
|
||||||
switch response.result {
|
switch response.result {
|
||||||
case .success(let _):
|
case .success(_):
|
||||||
callback?(true)
|
callback?(true)
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
NSLog("api request to \(endpoint) failed")
|
NSLog("api request to \(endpoint) failed")
|
||||||
|
@ -60,6 +61,30 @@ class ApiClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func patchResource<T:Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) {
|
||||||
|
if (Store.serverConfig == nil) {
|
||||||
|
NSLog("Server config not set")
|
||||||
|
callback?(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let headers: HTTPHeaders = [
|
||||||
|
"Authorization": "Bearer \(Store.serverConfig!.token)"
|
||||||
|
]
|
||||||
|
|
||||||
|
AF.request("\(Store.serverConfig!.address)/\(endpoint)", method: .patch, parameters: parameters, encoder: JSONParameterEncoder.default, headers: headers).response { response in
|
||||||
|
switch response.result {
|
||||||
|
case .success(_):
|
||||||
|
callback?(true)
|
||||||
|
case .failure(let error):
|
||||||
|
NSLog("api request to \(endpoint) failed")
|
||||||
|
print(error)
|
||||||
|
callback?(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static func getResource<T: Decodable>(endpoint: String, decodable: T.Type = T.self, callback: ((_ param: T?) -> Void)?) {
|
public static func getResource<T: Decodable>(endpoint: String, decodable: T.Type = T.self, callback: ((_ param: T?) -> Void)?) {
|
||||||
if (Store.serverConfig == nil) {
|
if (Store.serverConfig == nil) {
|
||||||
NSLog("Server config not set")
|
NSLog("Server config not set")
|
||||||
|
@ -119,6 +144,41 @@ 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 syncMediaProgress(callback: @escaping (_ results: LocalMediaProgressSyncResultsPayload) -> Void) {
|
||||||
|
let localMediaProgressList = Database.shared.getAllLocalMediaProgress().filter {
|
||||||
|
$0.serverConnectionConfigId == Store.serverConfig?.id
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !localMediaProgressList.isEmpty ) {
|
||||||
|
let payload = LocalMediaProgressSyncPayload(localMediaProgress: localMediaProgressList)
|
||||||
|
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
|
||||||
|
|
||||||
|
let resultsPayload = LocalMediaProgressSyncResultsPayload(numLocalMediaProgressForServer: localMediaProgressList.count, numServerProgressUpdates: response.numServerProgressUpdates, numLocalProgressUpdates: response.localProgressUpdates?.count)
|
||||||
|
NSLog("Media Progress Sync | \(String(describing: try? resultsPayload.asDictionary()))")
|
||||||
|
|
||||||
|
if let updates = response.localProgressUpdates {
|
||||||
|
for update in updates {
|
||||||
|
Database.shared.saveLocalMediaProgress(update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(resultsPayload)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
NSLog("No local media progress to sync")
|
||||||
|
callback(LocalMediaProgressSyncResultsPayload(numLocalMediaProgressForServer: 0, numServerProgressUpdates: 0, numLocalProgressUpdates: 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func updateMediaProgress<T:Encodable>(libraryItemId: String, episodeId: String?, payload: T, callback: @escaping () -> Void) {
|
||||||
|
NSLog("updateMediaProgress \(libraryItemId) \(episodeId ?? "NIL") \(payload)")
|
||||||
|
let endpoint = episodeId?.isEmpty ?? true ? "/api/me/progress/\(libraryItemId)" : "/api/me/progress/\(libraryItemId)/\(episodeId ?? "")"
|
||||||
|
patchResource(endpoint: endpoint, parameters: payload) { success in
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static func getLibraryItemWithProgress(libraryItemId:String, episodeId:String?, callback: @escaping (_ param: LibraryItem?) -> Void) {
|
public static func getLibraryItemWithProgress(libraryItemId:String, episodeId:String?, callback: @escaping (_ param: LibraryItem?) -> Void) {
|
||||||
var endpoint = "api/items/\(libraryItemId)?expanded=1&include=progress"
|
var endpoint = "api/items/\(libraryItemId)?expanded=1&include=progress"
|
||||||
if episodeId != nil {
|
if episodeId != nil {
|
||||||
|
@ -130,3 +190,18 @@ class ApiClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct LocalMediaProgressSyncPayload: Codable {
|
||||||
|
var localMediaProgress: [LocalMediaProgress]
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MediaProgressSyncResponsePayload: Codable {
|
||||||
|
var numServerProgressUpdates: Int?
|
||||||
|
var localProgressUpdates: [LocalMediaProgress]?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LocalMediaProgressSyncResultsPayload: Codable {
|
||||||
|
var numLocalMediaProgressForServer: Int?
|
||||||
|
var numServerProgressUpdates: Int?
|
||||||
|
var numLocalProgressUpdates: Int?
|
||||||
|
}
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
|
||||||
import RealmSwift
|
import RealmSwift
|
||||||
|
import Capacitor
|
||||||
|
|
||||||
extension String: Error {}
|
extension String: Error {}
|
||||||
|
|
||||||
|
@ -31,6 +31,14 @@ extension Collection where Iterator.Element: Encodable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension CAPPluginCall {
|
||||||
|
func getJson<T: Decodable>(_ key: String, type: T.Type) -> T? {
|
||||||
|
guard let value = getString(key) else { return nil }
|
||||||
|
guard let valueData = value.data(using: .utf8) else { return nil }
|
||||||
|
return try? JSONDecoder().decode(type, from: valueData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension DispatchQueue {
|
extension DispatchQueue {
|
||||||
static func runOnMainQueue(callback: @escaping (() -> Void)) {
|
static func runOnMainQueue(callback: @escaping (() -> Void)) {
|
||||||
if Thread.isMainThread {
|
if Thread.isMainThread {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue