mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-08-05 02:25:45 +02:00
Merge branch 'ios-downloads' into ios-downloads-realm-native
This commit is contained in:
commit
934a07a5ad
20 changed files with 850 additions and 208 deletions
|
@ -37,7 +37,29 @@ class ApiClient {
|
|||
}
|
||||
}
|
||||
}
|
||||
public static func postResource(endpoint: String, parameters: [String: String], callback: ((_ success: Bool) -> Void)?) {
|
||||
|
||||
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)?) {
|
||||
if (Store.serverConfig == nil) {
|
||||
NSLog("Server config not set")
|
||||
callback?(false)
|
||||
|
@ -50,7 +72,7 @@ class ApiClient {
|
|||
|
||||
AF.request("\(Store.serverConfig!.address)/\(endpoint)", method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, headers: headers).response { response in
|
||||
switch response.result {
|
||||
case .success(let _):
|
||||
case .success(_):
|
||||
callback?(true)
|
||||
case .failure(let error):
|
||||
NSLog("api request to \(endpoint) failed")
|
||||
|
@ -60,6 +82,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)?) {
|
||||
if (Store.serverConfig == nil) {
|
||||
NSLog("Server config not set")
|
||||
|
@ -96,7 +142,7 @@ class ApiClient {
|
|||
}
|
||||
}
|
||||
|
||||
ApiClient.postResource(endpoint: endpoint, parameters: [
|
||||
let parameters: [String: Any] = [
|
||||
"forceDirectPlay": !forceTranscode ? "1" : "",
|
||||
"forceTranscode": forceTranscode ? "1" : "",
|
||||
"mediaPlayer": "AVPlayer",
|
||||
|
@ -105,7 +151,8 @@ class ApiClient {
|
|||
"model": modelCode,
|
||||
"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
|
||||
|
||||
session.serverConnectionConfigId = Store.serverConfig!.id
|
||||
|
@ -119,6 +166,44 @@ class ApiClient {
|
|||
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) {
|
||||
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")
|
||||
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)
|
||||
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) {
|
||||
var endpoint = "api/items/\(libraryItemId)?expanded=1&include=progress"
|
||||
if episodeId != nil {
|
||||
|
@ -130,3 +215,35 @@ class ApiClient {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LocalMediaProgressSyncPayload: Codable {
|
||||
var localMediaProgress: [LocalMediaProgress]
|
||||
}
|
||||
|
||||
struct MediaProgressSyncResponsePayload: Decodable {
|
||||
var numServerProgressUpdates: Int?
|
||||
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 {
|
||||
var numLocalMediaProgressForServer: Int?
|
||||
var numServerProgressUpdates: Int?
|
||||
var numLocalProgressUpdates: Int?
|
||||
}
|
||||
|
||||
struct Connectivity {
|
||||
static private let sharedInstance = NetworkReachabilityManager()!
|
||||
static var isConnectedToInternet:Bool {
|
||||
return self.sharedInstance.isReachable
|
||||
}
|
||||
}
|
||||
|
|
|
@ -116,14 +116,14 @@ class Database {
|
|||
return Array(realm.objects(LocalLibraryItem.self))
|
||||
}
|
||||
|
||||
public func getLocalLibraryItemByLLId(libraryItem: String) -> LocalLibraryItem? {
|
||||
public func getLocalLibraryItem(byServerLibraryItemId: String) -> LocalLibraryItem? {
|
||||
let realm = try! Realm()
|
||||
return realm.objects(LocalLibraryItem.self).first(where: { $0.libraryItemId == libraryItem })
|
||||
return realm.objects(LocalLibraryItem.self).first(where: { $0.libraryItemId == byServerLibraryItemId })
|
||||
}
|
||||
|
||||
public func getLocalLibraryItem(localLibraryItem: String) -> LocalLibraryItem? {
|
||||
public func getLocalLibraryItem(localLibraryItemId: String) -> LocalLibraryItem? {
|
||||
let realm = try! Realm()
|
||||
return realm.object(ofType: LocalLibraryItem.self, forPrimaryKey: localLibraryItem)
|
||||
return realm.object(ofType: LocalLibraryItem.self, forPrimaryKey: localLibraryItemId)
|
||||
}
|
||||
|
||||
public func saveLocalLibraryItem(localLibraryItem: LocalLibraryItem) {
|
||||
|
@ -131,6 +131,19 @@ class Database {
|
|||
try! realm.write { realm.add(localLibraryItem, update: .modified) }
|
||||
}
|
||||
|
||||
public func removeLocalLibraryItem(localLibraryItemId: String) {
|
||||
let realm = try! Realm()
|
||||
try! realm.write {
|
||||
let item = getLocalLibraryItem(localLibraryItemId: localLibraryItemId)
|
||||
realm.delete(item!)
|
||||
}
|
||||
}
|
||||
|
||||
public func getLocalFile(localFileId: String) -> LocalFile? {
|
||||
let realm = try! Realm()
|
||||
return realm.object(ofType: LocalFile.self, forPrimaryKey: localFileId)
|
||||
}
|
||||
|
||||
public func getDownloadItem(downloadItemId: String) -> DownloadItem? {
|
||||
let realm = try! Realm()
|
||||
return realm.object(ofType: DownloadItem.self, forPrimaryKey: downloadItemId)
|
||||
|
@ -166,17 +179,14 @@ class Database {
|
|||
return realm.objects(DeviceSettings.self).first ?? getDefaultDeviceSettings()
|
||||
}
|
||||
|
||||
public func removeLocalLibraryItem(localLibraryItemId: String) {
|
||||
public func getAllLocalMediaProgress() -> [LocalMediaProgress] {
|
||||
let realm = try! Realm()
|
||||
try! realm.write {
|
||||
let item = getLocalLibraryItemByLLId(libraryItem: localLibraryItemId)
|
||||
realm.delete(item!)
|
||||
}
|
||||
return Array(realm.objects(LocalMediaProgress.self))
|
||||
}
|
||||
|
||||
public func saveLocalMediaProgress(_ mediaProgress: LocalMediaProgress) {
|
||||
let realm = try! Realm()
|
||||
try! realm.write { realm.add(mediaProgress) }
|
||||
try! realm.write { realm.add(mediaProgress, update: .modified) }
|
||||
}
|
||||
|
||||
// For books this will just be the localLibraryItemId for podcast episodes this will be "{localLibraryItemId}-{episodeId}"
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import RealmSwift
|
||||
import Capacitor
|
||||
import CoreMedia
|
||||
|
||||
extension String: Error {}
|
||||
|
||||
|
@ -31,6 +32,34 @@ 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 {
|
||||
func getJson<T: Decodable>(_ key: String, type: T.Type) -> T? {
|
||||
guard let value = getObject(key) else { return nil }
|
||||
guard let json = try? JSONSerialization.data(withJSONObject: value) else { return nil }
|
||||
return try? JSONDecoder().decode(type, from: json)
|
||||
}
|
||||
}
|
||||
|
||||
extension DispatchQueue {
|
||||
static func runOnMainQueue(callback: @escaping (() -> Void)) {
|
||||
if Thread.isMainThread {
|
||||
|
|
|
@ -15,6 +15,10 @@ struct NowPlayingMetadata {
|
|||
var title: String
|
||||
var author: String?
|
||||
var series: String?
|
||||
var coverUrl: URL? {
|
||||
guard let url = URL(string: "\(Store.serverConfig!.address)/api/items/\(itemId)/cover?token=\(Store.serverConfig!.token)") else { return nil }
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
class NowPlayingInfo {
|
||||
|
@ -30,18 +34,27 @@ class NowPlayingInfo {
|
|||
public func setSessionMetadata(metadata: NowPlayingMetadata) {
|
||||
setMetadata(artwork: nil, metadata: metadata)
|
||||
|
||||
guard let url = URL(string: "\(Store.serverConfig!.address)/api/items/\(metadata.itemId)/cover?token=\(Store.serverConfig!.token)") else {
|
||||
return
|
||||
}
|
||||
ApiClient.getData(from: url) { [self] image in
|
||||
guard let downloadedImage = image else {
|
||||
return
|
||||
let isLocalItem = metadata.itemId.starts(with: "local_")
|
||||
if isLocalItem {
|
||||
guard let artworkUrl = metadata.artworkUrl else { return }
|
||||
let coverImage = UIImage(contentsOfFile: artworkUrl)
|
||||
guard let coverImage = coverImage else { return }
|
||||
let artwork = MPMediaItemArtwork(boundsSize: coverImage.size) { _ -> UIImage in
|
||||
return coverImage
|
||||
}
|
||||
let artwork = MPMediaItemArtwork.init(boundsSize: downloadedImage.size, requestHandler: { _ -> UIImage in
|
||||
return downloadedImage
|
||||
})
|
||||
|
||||
self.setMetadata(artwork: artwork, metadata: metadata)
|
||||
} else {
|
||||
guard let url = metadata.coverUrl else { return }
|
||||
ApiClient.getData(from: url) { [self] image in
|
||||
guard let downloadedImage = image else {
|
||||
return
|
||||
}
|
||||
let artwork = MPMediaItemArtwork.init(boundsSize: downloadedImage.size, requestHandler: { _ -> UIImage in
|
||||
return downloadedImage
|
||||
})
|
||||
|
||||
self.setMetadata(artwork: artwork, metadata: metadata)
|
||||
}
|
||||
}
|
||||
}
|
||||
public func update(duration: Double, currentTime: Double, rate: Float) {
|
||||
|
@ -52,6 +65,7 @@ class NowPlayingInfo {
|
|||
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
||||
}
|
||||
|
||||
public func reset() {
|
||||
nowPlayingInfo = [:]
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
|
||||
|
@ -76,6 +90,7 @@ class NowPlayingInfo {
|
|||
nowPlayingInfo[MPMediaItemPropertyArtist] = metadata!.author ?? "unknown"
|
||||
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = metadata!.series
|
||||
}
|
||||
|
||||
private func shouldFetchCover(id: String) -> Bool {
|
||||
nowPlayingInfo[MPNowPlayingInfoPropertyExternalContentIdentifier] as? String != id || nowPlayingInfo[MPMediaItemPropertyArtwork] == nil
|
||||
}
|
||||
|
|
|
@ -13,4 +13,5 @@ enum PlayerEvents: String {
|
|||
case sleepSet = "com.audiobookshelf.app.player.sleep.set"
|
||||
case sleepEnded = "com.audiobookshelf.app.player.sleep.ended"
|
||||
case failed = "com.audiobookshelf.app.player.failed"
|
||||
case localProgress = "com.audiobookshelf.app.player.localProgress"
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue