mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-02-18 00:17:50 +01:00
feat: usable logs on iOS
- enable logs page on iOS - centralize all logging - wrap long identifiers in logs UI - update editorconfig for swift files - update podfile
This commit is contained in:
@@ -6,3 +6,7 @@ indent_size = 2
|
||||
charset = utf-8
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.swift]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
@@ -134,14 +134,12 @@ export default {
|
||||
to: '/settings'
|
||||
})
|
||||
|
||||
if (this.$platform !== 'ios') {
|
||||
items.push({
|
||||
icon: 'bug_report',
|
||||
iconOutlined: true,
|
||||
text: this.$strings.ButtonLogs,
|
||||
to: '/logs'
|
||||
})
|
||||
}
|
||||
items.push({
|
||||
icon: 'bug_report',
|
||||
iconOutlined: true,
|
||||
text: this.$strings.ButtonLogs,
|
||||
to: '/logs'
|
||||
})
|
||||
|
||||
if (this.serverConnectionConfig) {
|
||||
items.push({
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
|
||||
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
|
||||
65643F4DAA661FB0B247247E /* Pods_Audiobookshelf.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8F9A7CCC479333E44DC314BE /* Pods_Audiobookshelf.framework */; };
|
||||
6F2516272E68DA3000F40541 /* LogEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F2516262E68DA3000F40541 /* LogEntry.swift */; };
|
||||
E9D5504628AC1A3900C746DD /* LibraryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5504528AC1A3900C746DD /* LibraryItem.swift */; };
|
||||
E9D5504828AC1A7A00C746DD /* MediaType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5504728AC1A7A00C746DD /* MediaType.swift */; };
|
||||
E9D5504A28AC1AA600C746DD /* Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5504928AC1AA600C746DD /* Metadata.swift */; };
|
||||
@@ -111,6 +112,7 @@
|
||||
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
||||
6F2516262E68DA3000F40541 /* LogEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogEntry.swift; sourceTree = "<group>"; };
|
||||
8F9A7CCC479333E44DC314BE /* Pods_Audiobookshelf.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Audiobookshelf.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BE96D57E131924D520D57057 /* Pods-Audiobookshelf.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Audiobookshelf.debug.xcconfig"; path = "Target Support Files/Pods-Audiobookshelf/Pods-Audiobookshelf.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
D2F7F575384A63F1C47DE984 /* Pods-Audiobookshelf.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Audiobookshelf.release.xcconfig"; path = "Target Support Files/Pods-Audiobookshelf/Pods-Audiobookshelf.release.xcconfig"; sourceTree = "<group>"; };
|
||||
@@ -231,6 +233,7 @@
|
||||
E9D5506128AC1CC900C746DD /* PlayerState.swift */,
|
||||
4DF74911287105C600AC7814 /* DeviceSettings.swift */,
|
||||
E9D5507428AEF93100C746DD /* PlayerSettings.swift */,
|
||||
6F2516262E68DA3000F40541 /* LogEntry.swift */,
|
||||
E9D5506328AC1D3F00C746DD /* server */,
|
||||
E9D5506428AC1D5800C746DD /* local */,
|
||||
E9D5506D28AC1E7400C746DD /* download */,
|
||||
@@ -533,6 +536,7 @@
|
||||
3ABF580928059BAE005DFBE5 /* PlaybackSession.swift in Sources */,
|
||||
E9D5506628AC1D7300C746DD /* LocalLibraryItem.swift in Sources */,
|
||||
E9D5504628AC1A3900C746DD /* LibraryItem.swift in Sources */,
|
||||
6F2516272E68DA3000F40541 /* LogEntry.swift in Sources */,
|
||||
3ABF618F2804325C0070250E /* PlayerHandler.swift in Sources */,
|
||||
3AD4FCED28044E6C006DB301 /* Store.swift in Sources */,
|
||||
4D66B958282EEA14008272D4 /* AbsFileSystem.swift in Sources */,
|
||||
|
||||
@@ -5,8 +5,6 @@ import RealmSwift
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
private let logger = AppLogger(category: "AppDelegate")
|
||||
|
||||
lazy var window: UIWindow? = UIWindow(frame: UIScreen.main.bounds)
|
||||
var backgroundCompletionHandler: (() -> Void)?
|
||||
|
||||
@@ -17,13 +15,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
schemaVersion: 20,
|
||||
migrationBlock: { [weak self] migration, oldSchemaVersion in
|
||||
if (oldSchemaVersion < 1) {
|
||||
self?.logger.log("Realm schema version was \(oldSchemaVersion)")
|
||||
AbsLogger.info(message: "Realm schema version was \(oldSchemaVersion)")
|
||||
migration.enumerateObjects(ofType: DeviceSettings.className()) { oldObject, newObject in
|
||||
newObject?["enableAltView"] = false
|
||||
}
|
||||
}
|
||||
if (oldSchemaVersion < 4) {
|
||||
self?.logger.log("Realm schema version was \(oldSchemaVersion)... Reindexing server configs")
|
||||
AbsLogger.info(message: "Realm schema version was \(oldSchemaVersion)... Reindexing server configs")
|
||||
var indexCounter = 1
|
||||
migration.enumerateObjects(ofType: ServerConnectionConfig.className()) { oldObject, newObject in
|
||||
newObject?["index"] = indexCounter
|
||||
@@ -31,44 +29,44 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
}
|
||||
}
|
||||
if (oldSchemaVersion < 5) {
|
||||
self?.logger.log("Realm schema version was \(oldSchemaVersion)... Adding lockOrientation setting")
|
||||
AbsLogger.info(message: "Realm schema version was \(oldSchemaVersion)... Adding lockOrientation setting")
|
||||
migration.enumerateObjects(ofType: DeviceSettings.className()) { oldObject, newObject in
|
||||
newObject?["lockOrientation"] = "NONE"
|
||||
}
|
||||
}
|
||||
if (oldSchemaVersion < 6) {
|
||||
self?.logger.log("Realm schema version was \(oldSchemaVersion)... Adding hapticFeedback setting")
|
||||
AbsLogger.info(message: "Realm schema version was \(oldSchemaVersion)... Adding hapticFeedback setting")
|
||||
migration.enumerateObjects(ofType: DeviceSettings.className()) { oldObject, newObject in
|
||||
newObject?["hapticFeedback"] = "LIGHT"
|
||||
}
|
||||
}
|
||||
if (oldSchemaVersion < 15) {
|
||||
self?.logger.log("Realm schema version was \(oldSchemaVersion)... Adding languageCode setting")
|
||||
AbsLogger.info(message: "Realm schema version was \(oldSchemaVersion)... Adding languageCode setting")
|
||||
migration.enumerateObjects(ofType: DeviceSettings.className()) { oldObject, newObject in
|
||||
newObject?["languageCode"] = "en-us"
|
||||
}
|
||||
}
|
||||
if (oldSchemaVersion < 16) {
|
||||
self?.logger.log("Realm schema version was \(oldSchemaVersion)... Adding chapterTrack setting")
|
||||
AbsLogger.info(message: "Realm schema version was \(oldSchemaVersion)... Adding chapterTrack setting")
|
||||
migration.enumerateObjects(ofType: PlayerSettings.className()) { oldObject, newObject in
|
||||
newObject?["chapterTrack"] = false
|
||||
}
|
||||
}
|
||||
if (oldSchemaVersion < 17) {
|
||||
self?.logger.log("Realm schema version was \(oldSchemaVersion)... Adding downloadUsingCellular and streamingUsingCellular settings")
|
||||
AbsLogger.info(message: "Realm schema version was \(oldSchemaVersion)... Adding downloadUsingCellular and streamingUsingCellular settings")
|
||||
migration.enumerateObjects(ofType: PlayerSettings.className()) { oldObject, newObject in
|
||||
newObject?["downloadUsingCellular"] = "ALWAYS"
|
||||
newObject?["streamingUsingCellular"] = "ALWAYS"
|
||||
}
|
||||
}
|
||||
if (oldSchemaVersion < 18) {
|
||||
self?.logger.log("Realm schema version was \(oldSchemaVersion)... Adding disableSleepTimerFadeOut settings")
|
||||
AbsLogger.info(message: "Realm schema version was \(oldSchemaVersion)... Adding disableSleepTimerFadeOut settings")
|
||||
migration.enumerateObjects(ofType: PlayerSettings.className()) { oldObject, newObject in
|
||||
newObject?["disableSleepTimerFadeOut"] = false
|
||||
}
|
||||
}
|
||||
if (oldSchemaVersion < 20) {
|
||||
self?.logger.log("Realm schema version was \(oldSchemaVersion)... Adding version to ServerConnectionConfigs")
|
||||
AbsLogger.info(message: "Realm schema version was \(oldSchemaVersion)... Adding version to ServerConnectionConfigs")
|
||||
migration.enumerateObjects(ofType: ServerConnectionConfig.className()) { oldObject, newObject in
|
||||
newObject?["version"] = ""
|
||||
}
|
||||
@@ -88,22 +86,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
||||
logger.log("Audiobookself is now in the background")
|
||||
AbsLogger.info(message: "Audiobookself is now in the background")
|
||||
}
|
||||
|
||||
func applicationWillEnterForeground(_ application: UIApplication) {
|
||||
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
|
||||
logger.log("Audiobookself is now in the foreground")
|
||||
AbsLogger.info(message: "Audiobookself is now in the foreground")
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
logger.log("Audiobookself is now active")
|
||||
AbsLogger.info(message: "Audiobookself is now active")
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
logger.log("Audiobookself is terminating")
|
||||
AbsLogger.info(message: "Audiobookself is terminating")
|
||||
}
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||
|
||||
@@ -33,8 +33,6 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
|
||||
CAPPluginMethod(name: "getSleepTimerTime", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "setSleepTimer", returnType: CAPPluginReturnPromise)
|
||||
]
|
||||
|
||||
private let logger = AppLogger(category: "AbsAudioPlayer")
|
||||
|
||||
private var initialPlayWhenReady = false
|
||||
private var monitor: NWPathMonitor?
|
||||
@@ -82,7 +80,7 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
|
||||
let startTimeOverride = call.getDouble("startTime")
|
||||
|
||||
if libraryItemId == nil {
|
||||
logger.error("provide library item id")
|
||||
AbsLogger.error(message: "No id provided")
|
||||
return call.resolve()
|
||||
}
|
||||
|
||||
@@ -93,7 +91,7 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
|
||||
let item = Database.shared.getLocalLibraryItem(localLibraryItemId: libraryItemId!)
|
||||
let episode = item?.getPodcastEpisode(episodeId: episodeId)
|
||||
guard let playbackSession = item?.getPlaybackSession(episode: episode) else {
|
||||
logger.error("Failed to get local playback session")
|
||||
AbsLogger.error(message: "Failed to get local playback session")
|
||||
return call.resolve([:])
|
||||
}
|
||||
|
||||
@@ -105,7 +103,7 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
|
||||
try self.startPlaybackSession(playbackSession, playWhenReady: playWhenReady, playbackRate: playbackRate)
|
||||
call.resolve(try playbackSession.asDictionary())
|
||||
} catch(let exception) {
|
||||
logger.error("Failed to start session")
|
||||
AbsLogger.error(message: "Failed to start session for local item: \(exception)")
|
||||
debugPrint(exception)
|
||||
call.resolve([:])
|
||||
}
|
||||
@@ -119,7 +117,7 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
|
||||
try self?.startPlaybackSession(session, playWhenReady: playWhenReady, playbackRate: playbackRate)
|
||||
call.resolve(try session.asDictionary())
|
||||
} catch(let exception) {
|
||||
self?.logger.error("Failed to start session")
|
||||
AbsLogger.error(message: "Failed to start streaming session")
|
||||
debugPrint(exception)
|
||||
call.resolve([:])
|
||||
}
|
||||
@@ -127,19 +125,22 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops playback and closes session.
|
||||
@objc func closePlayback(_ call: CAPPluginCall) {
|
||||
logger.log("Close playback")
|
||||
AbsLogger.info(message: "Close playback")
|
||||
|
||||
PlayerHandler.stopPlayback()
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
|
||||
@objc func getCurrentTime(_ call: CAPPluginCall) {
|
||||
call.resolve([
|
||||
"value": PlayerHandler.getCurrentTime() ?? 0,
|
||||
"bufferedTime": PlayerHandler.getCurrentTime() ?? 0,
|
||||
])
|
||||
}
|
||||
|
||||
@objc func setPlaybackSpeed(_ call: CAPPluginCall) {
|
||||
let playbackRate = call.getFloat("value", 1.0)
|
||||
let settings = PlayerSettings.main()
|
||||
@@ -152,7 +153,7 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
|
||||
|
||||
@objc func setChapterTrack(_ call: CAPPluginCall) {
|
||||
let chapterTrack = call.getBool("enabled", true)
|
||||
logger.log(String(chapterTrack))
|
||||
AbsLogger.info(message: String(chapterTrack))
|
||||
let settings = PlayerSettings.main()
|
||||
try? settings.update {
|
||||
settings.chapterTrack = chapterTrack
|
||||
@@ -225,7 +226,7 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
|
||||
|
||||
let seconds = time / 1000
|
||||
|
||||
logger.log("chapter time: \(isChapterTime)")
|
||||
AbsLogger.info(message: "chapter time: \(isChapterTime)")
|
||||
if isChapterTime {
|
||||
PlayerHandler.setChapterSleepTime(stopAt: seconds)
|
||||
return call.resolve([ "success": true ])
|
||||
@@ -262,7 +263,7 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
|
||||
guard let localMediaProgressId = PlayerHandler.getPlaybackSession()?.localMediaProgressId else { return }
|
||||
guard let localMediaProgress = Database.shared.getLocalMediaProgress(localMediaProgressId: localMediaProgressId) else { return }
|
||||
guard let progressUpdate = try? localMediaProgress.asDictionary() else { return }
|
||||
logger.log("Sending local progress back to the UI")
|
||||
AbsLogger.info(message: "Sending local progress back to the UI")
|
||||
self.notifyListeners("onLocalMediaProgressUpdate", data: progressUpdate)
|
||||
}
|
||||
|
||||
@@ -272,7 +273,7 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
|
||||
let playWhenReady = PlayerHandler.getPlayWhenReady()
|
||||
let libraryItemId = session?.libraryItemId ?? ""
|
||||
let episodeId = session?.episodeId ?? nil
|
||||
logger.log("Forcing Transcode")
|
||||
AbsLogger.info(message: "Forcing Transcode")
|
||||
|
||||
// If direct playing then fallback to transcode
|
||||
ApiClient.startPlaybackSession(libraryItemId: libraryItemId, episodeId: episodeId, forceTranscode: true) { [weak self] session in
|
||||
@@ -283,7 +284,7 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
|
||||
self.sendPlaybackSession(session: try session.asDictionary())
|
||||
self.sendMetadata()
|
||||
} catch(let exception) {
|
||||
self?.logger.error("Failed to start transcoded session")
|
||||
AbsLogger.error(message: "Failed to start transcoded session")
|
||||
debugPrint(exception)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,8 +49,7 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
|
||||
CAPPluginMethod(name: "updateDeviceSettings", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "updateLocalEbookProgress", returnType: CAPPluginReturnPromise)
|
||||
]
|
||||
|
||||
private let logger = AppLogger(category: "AbsDatabase")
|
||||
|
||||
private let secureStorage = SecureStorage()
|
||||
|
||||
// Used to notify the webview frontend that the token has been refreshed
|
||||
@@ -80,7 +79,7 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
|
||||
if (refreshToken != "") {
|
||||
// Store refresh token securely if provided
|
||||
let hasRefreshToken = secureStorage.storeRefreshToken(serverConnectionConfigId: id ?? "", refreshToken: refreshToken)
|
||||
logger.log("Refresh token secured = \(hasRefreshToken)")
|
||||
AbsLogger.info(message: "Refresh token secured = \(hasRefreshToken)")
|
||||
}
|
||||
|
||||
let config = ServerConnectionConfig()
|
||||
@@ -148,15 +147,16 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
|
||||
let items = Database.shared.getLocalLibraryItems()
|
||||
call.resolve([ "value": try items.asDictionaryArray()])
|
||||
} catch(let exception) {
|
||||
logger.error("error while readling local library items")
|
||||
AbsLogger.error(message: "error reading local library items \(exception)")
|
||||
debugPrint(exception)
|
||||
call.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func getLocalLibraryItem(_ call: CAPPluginCall) {
|
||||
let id = call.getString("id") ?? ""
|
||||
do {
|
||||
let item = Database.shared.getLocalLibraryItem(localLibraryItemId: call.getString("id") ?? "")
|
||||
let item = Database.shared.getLocalLibraryItem(localLibraryItemId: id)
|
||||
switch item {
|
||||
case .some(let foundItem):
|
||||
call.resolve(try foundItem.asDictionary())
|
||||
@@ -164,7 +164,7 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
|
||||
call.resolve()
|
||||
}
|
||||
} catch(let exception) {
|
||||
logger.error("error while readling local library items")
|
||||
AbsLogger.error(message: "error reading local library item[\(id)] \(exception)")
|
||||
debugPrint(exception)
|
||||
call.resolve()
|
||||
}
|
||||
@@ -180,8 +180,7 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
|
||||
call.resolve()
|
||||
}
|
||||
} catch(let exception) {
|
||||
logger.error("error while readling local library items")
|
||||
debugPrint(exception)
|
||||
AbsLogger.error(message: "error while readling local library items: \(exception)", error: exception)
|
||||
call.resolve()
|
||||
}
|
||||
}
|
||||
@@ -194,8 +193,7 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
|
||||
do {
|
||||
call.resolve([ "value": try Database.shared.getAllLocalMediaProgress().asDictionaryArray() ])
|
||||
} catch {
|
||||
logger.error("Error while loading local media progress")
|
||||
debugPrint(error)
|
||||
AbsLogger.error(message: "Error while loading local media progress", error: error)
|
||||
call.resolve(["value": []])
|
||||
}
|
||||
}
|
||||
@@ -212,7 +210,7 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
|
||||
|
||||
@objc func syncLocalSessionsWithServer(_ call: CAPPluginCall) {
|
||||
let isFirstSync = call.getBool("isFirstSync", false)
|
||||
logger.log("syncLocalSessionsWithServer: Starting (First sync: \(isFirstSync))")
|
||||
AbsLogger.info(message: "Starting syncLocalSessionsWithServer isFirstSync=\(isFirstSync)")
|
||||
guard Store.serverConfig != nil else {
|
||||
call.reject("syncLocalSessionsWithServer not connected to server")
|
||||
return call.resolve()
|
||||
@@ -244,12 +242,13 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
|
||||
return
|
||||
}
|
||||
|
||||
logger.log("syncServerMediaProgressWithLocalMediaProgress: Saving local media progress")
|
||||
AbsLogger.info(message: "Saving local media progress \(serverMediaProgress)")
|
||||
try localMediaProgress.updateFromServerMediaProgress(serverMediaProgress)
|
||||
|
||||
call.resolve(try localMediaProgress.asDictionary())
|
||||
} catch {
|
||||
call.reject("Failed to sync media progress")
|
||||
AbsLogger.error(message: "Failed to sync: \(error)")
|
||||
debugPrint(error)
|
||||
}
|
||||
}
|
||||
@@ -264,7 +263,7 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
|
||||
localMediaProgressId += "-\(localEpisodeId ?? "")"
|
||||
}
|
||||
|
||||
logger.log("updateLocalMediaProgressFinished \(localMediaProgressId) | Is Finished: \(isFinished)")
|
||||
AbsLogger.info(message: "\(localMediaProgressId): isFinished=\(isFinished)")
|
||||
|
||||
do {
|
||||
let localMediaProgress = try LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: localMediaProgressId, localLibraryItemId: localLibraryItemId, localEpisodeId: localEpisodeId)
|
||||
@@ -337,7 +336,7 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
|
||||
let ebookLocation = call.getString("ebookLocation", "")
|
||||
let ebookProgress = call.getDouble("ebookProgress", 0.0)
|
||||
|
||||
logger.log("updateLocalEbookProgress \(localLibraryItemId ?? "Unknown") | ebookLocation: \(ebookLocation) | ebookProgress: \(ebookProgress)")
|
||||
AbsLogger.info(message: "\(localLibraryItemId ?? "Unknown"): ebookLocation=\(ebookLocation) ebookProgress=\(ebookProgress)")
|
||||
|
||||
do {
|
||||
let localMediaProgress = try LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: localLibraryItemId, localLibraryItemId: localLibraryItemId, localEpisodeId: nil)
|
||||
|
||||
@@ -19,8 +19,6 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
|
||||
|
||||
static private let downloadsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
|
||||
private let logger = AppLogger(category: "AbsDownloader")
|
||||
|
||||
private lazy var session: URLSession = {
|
||||
let config = URLSessionConfiguration.background(withIdentifier: "AbsDownloader")
|
||||
let queue = OperationQueue()
|
||||
@@ -104,7 +102,7 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
|
||||
private func handleDownloadTaskUpdate(downloadTask: URLSessionTask, progressHandler: DownloadProgressHandler) {
|
||||
do {
|
||||
guard let downloadItemPartId = downloadTask.taskDescription else { throw LibraryItemDownloadError.noTaskDescription }
|
||||
logger.log("Received download update for \(downloadItemPartId)")
|
||||
AbsLogger.info(message: "Received download update for \(downloadItemPartId)")
|
||||
|
||||
// Find the download item
|
||||
let downloadItem = Database.shared.getDownloadItem(downloadItemPartId: downloadItemPartId)
|
||||
@@ -119,7 +117,7 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
|
||||
try progressHandler(downloadItem, part)
|
||||
try? self.notifyListeners("onDownloadItemPartUpdate", data: part.asDictionary())
|
||||
} catch {
|
||||
logger.error("Error while processing progress")
|
||||
AbsLogger.error(message: "Error while processing progress")
|
||||
debugPrint(error)
|
||||
}
|
||||
|
||||
@@ -130,7 +128,7 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
|
||||
}
|
||||
self.notifyDownloadProgress()
|
||||
} catch {
|
||||
logger.error("DownloadItemError")
|
||||
AbsLogger.error(message: "DownloadItemError")
|
||||
debugPrint(error)
|
||||
}
|
||||
}
|
||||
@@ -138,18 +136,18 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
|
||||
// We want to handle updating the UI in the background and throttled so we don't overload the UI with progress updates
|
||||
private func notifyDownloadProgress() {
|
||||
if self.monitoringProgressTimer?.isValid ?? false {
|
||||
logger.log("Already monitoring progress, no need to start timer again")
|
||||
AbsLogger.info(message: "Already monitoring progress, no need to start timer again")
|
||||
} else {
|
||||
DispatchQueue.runOnMainQueue {
|
||||
self.monitoringProgressTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true, block: { [unowned self] t in
|
||||
self.logger.log("Starting monitoring download progress...")
|
||||
AbsLogger.info(message: "Starting monitoring download progress...")
|
||||
|
||||
// Fetch active downloads in a thread-safe way
|
||||
func fetchActiveDownloads() -> [String: DownloadItem]? {
|
||||
self.progressStatusQueue.sync {
|
||||
let activeDownloads = self.downloadItemProgress
|
||||
if activeDownloads.isEmpty {
|
||||
logger.log("Finishing monitoring download progress...")
|
||||
AbsLogger.info(message: "Finishing monitoring download progress...")
|
||||
t.invalidate()
|
||||
}
|
||||
return activeDownloads
|
||||
@@ -184,7 +182,7 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
|
||||
|
||||
if ( downloadItem.didDownloadSuccessfully() ) {
|
||||
ApiClient.getLibraryItemWithProgress(libraryItemId: downloadItem.libraryItemId!, episodeId: downloadItem.episodeId) { [weak self] libraryItem in
|
||||
guard let libraryItem = libraryItem else { self?.logger.error("LibraryItem not found"); return }
|
||||
guard let libraryItem = libraryItem else { AbsLogger.error(message: "LibraryItem not found"); return }
|
||||
let localDirectory = libraryItem.id
|
||||
var coverFile: String?
|
||||
|
||||
@@ -231,12 +229,12 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
|
||||
var episodeId = call.getString("episodeId")
|
||||
if ( episodeId == "null" ) { episodeId = nil }
|
||||
|
||||
logger.log("Download library item \(libraryItemId ?? "N/A") / episode \(episodeId ?? "N/A")")
|
||||
AbsLogger.info(message: "Download library item \(libraryItemId ?? "N/A") / episode \(episodeId ?? "N/A")")
|
||||
guard let libraryItemId = libraryItemId else { return call.resolve(["error": "libraryItemId not specified"]) }
|
||||
|
||||
ApiClient.getLibraryItemWithProgress(libraryItemId: libraryItemId, episodeId: episodeId) { [weak self] libraryItem in
|
||||
if let libraryItem = libraryItem {
|
||||
self?.logger.log("Got library item from server \(libraryItem.id)")
|
||||
AbsLogger.info(message: "Got library item from server \(libraryItem.id)")
|
||||
do {
|
||||
if let episodeId = episodeId {
|
||||
// Download a podcast episode
|
||||
@@ -317,7 +315,7 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
|
||||
}
|
||||
|
||||
private func startLibraryItemTrackDownload(downloadItemId: String, item: LibraryItem, position: Int, track: AudioTrack, episode: PodcastEpisode?) throws -> DownloadItemPartTask {
|
||||
logger.log("TRACK \(track.contentUrl!)")
|
||||
AbsLogger.info(message: "TRACK \(track.contentUrl!)")
|
||||
|
||||
// If we don't name metadata, then we can't proceed
|
||||
guard let filename = track.metadata?.filename else {
|
||||
@@ -393,10 +391,10 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
|
||||
|
||||
private func createLibraryItemFileDirectory(item: LibraryItem) throws -> String {
|
||||
let itemDirectory = item.id
|
||||
logger.log("ITEM DIR \(itemDirectory)")
|
||||
AbsLogger.info(message: "ITEM DIR \(itemDirectory)")
|
||||
|
||||
guard AbsDownloader.itemDownloadFolder(path: itemDirectory) != nil else {
|
||||
logger.error("Failed to CREATE LI DIRECTORY \(itemDirectory)")
|
||||
AbsLogger.error(message: "Failed to CREATE LI DIRECTORY \(itemDirectory)")
|
||||
throw LibraryItemDownloadError.failedDirectory
|
||||
}
|
||||
|
||||
@@ -418,7 +416,7 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
|
||||
|
||||
return itemFolder
|
||||
} catch {
|
||||
AppLogger().error("Failed to CREATE LI DIRECTORY \(error)")
|
||||
AbsLogger.error(message: "Failed to CREATE LI DIRECTORY \(error)", error: error)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,10 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
|
||||
CAPPluginMethod(name: "deleteTrackFromItem", returnType: CAPPluginReturnPromise)
|
||||
]
|
||||
|
||||
private let logger = AppLogger(category: "AbsFileSystem")
|
||||
|
||||
@objc func selectFolder(_ call: CAPPluginCall) {
|
||||
let mediaType = call.getString("mediaType")
|
||||
|
||||
logger.log("Select Folder for media type \(mediaType ?? "UNSET")")
|
||||
AbsLogger.info(message: "Select Folder for media type \(mediaType ?? "UNSET")")
|
||||
|
||||
call.unavailable("Not available on iOS")
|
||||
}
|
||||
@@ -36,7 +34,7 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
|
||||
@objc func checkFolderPermission(_ call: CAPPluginCall) {
|
||||
let folderUrl = call.getString("folderUrl")
|
||||
|
||||
logger.log("checkFolderPermission for folder \(folderUrl ?? "UNSET")")
|
||||
AbsLogger.info(message: "checkFolderPermission for folder \(folderUrl ?? "UNSET")")
|
||||
|
||||
call.unavailable("Not available on iOS")
|
||||
}
|
||||
@@ -45,7 +43,7 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
|
||||
let folderId = call.getString("folderId")
|
||||
let forceAudioProbe = call.getBool("forceAudioProbe", false)
|
||||
|
||||
logger.log("scanFolder \(folderId ?? "UNSET") | Force Probe = \(forceAudioProbe)")
|
||||
AbsLogger.info(message: "scanFolder \(folderId ?? "UNSET") | Force Probe = \(forceAudioProbe)")
|
||||
|
||||
call.unavailable("Not available on iOS")
|
||||
}
|
||||
@@ -53,7 +51,7 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
|
||||
@objc func removeFolder(_ call: CAPPluginCall) {
|
||||
let folderId = call.getString("folderId")
|
||||
|
||||
logger.log("removeFolder \(folderId ?? "UNSET")")
|
||||
AbsLogger.info(message: "removeFolder \(folderId ?? "UNSET")")
|
||||
|
||||
call.unavailable("Not available on iOS")
|
||||
}
|
||||
@@ -61,7 +59,7 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
|
||||
@objc func removeLocalLibraryItem(_ call: CAPPluginCall) {
|
||||
let localLibraryItemId = call.getString("localLibraryItemId")
|
||||
|
||||
logger.log("removeLocalLibraryItem \(localLibraryItemId ?? "UNSET")")
|
||||
AbsLogger.info(message: "removeLocalLibraryItem \(localLibraryItemId ?? "UNSET")")
|
||||
|
||||
call.unavailable("Not available on iOS")
|
||||
}
|
||||
@@ -70,7 +68,7 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
|
||||
let localLibraryItemId = call.getString("localLibraryItemId")
|
||||
let forceAudioProbe = call.getBool("forceAudioProbe", false)
|
||||
|
||||
logger.log("scanLocalLibraryItem \(localLibraryItemId ?? "UNSET") | Force Probe = \(forceAudioProbe)")
|
||||
AbsLogger.info(message: "scanLocalLibraryItem \(localLibraryItemId ?? "UNSET") | Force Probe = \(forceAudioProbe)")
|
||||
|
||||
call.unavailable("Not available on iOS")
|
||||
}
|
||||
@@ -79,7 +77,7 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
|
||||
let localLibraryItemId = call.getString("id")
|
||||
let contentUrl = call.getString("contentUrl")
|
||||
|
||||
logger.log("deleteItem \(localLibraryItemId ?? "UNSET") url \(contentUrl ?? "UNSET")")
|
||||
AbsLogger.info(message: "deleteItem \(localLibraryItemId ?? "UNSET") url \(contentUrl ?? "UNSET")")
|
||||
|
||||
var success = false
|
||||
do {
|
||||
@@ -89,7 +87,7 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
|
||||
success = true
|
||||
}
|
||||
} catch {
|
||||
logger.error("Failed to delete \(error)")
|
||||
AbsLogger.error(message: "Failed to delete \(error)")
|
||||
success = false
|
||||
}
|
||||
|
||||
@@ -100,7 +98,7 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
|
||||
let localLibraryItemId = call.getString("id")
|
||||
let trackLocalFileId = call.getString("trackLocalFileId")
|
||||
|
||||
logger.log("deleteTrackFromItem \(localLibraryItemId ?? "UNSET") track file \(trackLocalFileId ?? "UNSET")")
|
||||
AbsLogger.info(message: "deleteTrackFromItem \(localLibraryItemId ?? "UNSET") track file \(trackLocalFileId ?? "UNSET")")
|
||||
|
||||
var success = false
|
||||
if let localLibraryItemId = localLibraryItemId, let trackLocalFileId = trackLocalFileId, let item = Database.shared.getLocalLibraryItem(localLibraryItemId: localLibraryItemId) {
|
||||
@@ -120,12 +118,12 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
|
||||
success = true
|
||||
}
|
||||
} catch {
|
||||
logger.error("Failed to delete \(error)")
|
||||
AbsLogger.error(message: "Failed to delete \(error)")
|
||||
success = false
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
logger.error("Failed to delete \(error)")
|
||||
AbsLogger.error(message: "Failed to delete \(error)")
|
||||
success = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,16 @@
|
||||
import Foundation
|
||||
import Capacitor
|
||||
|
||||
enum AbsLogLevel {
|
||||
case info
|
||||
case warn
|
||||
case error
|
||||
}
|
||||
|
||||
func fileName(file: String = #file) -> String {
|
||||
return (file as NSString).lastPathComponent
|
||||
}
|
||||
|
||||
@objc(AbsLogger)
|
||||
public class AbsLogger: CAPPlugin, CAPBridgedPlugin {
|
||||
public var identifier = "AbsLoggerPlugin"
|
||||
@@ -19,29 +29,89 @@ public class AbsLogger: CAPPlugin, CAPBridgedPlugin {
|
||||
CAPPluginMethod(name: "clearLogs", returnType: CAPPluginReturnPromise)
|
||||
]
|
||||
|
||||
private let logger = AppLogger(category: "AbsLogger")
|
||||
private static let shared: AbsLogger = {
|
||||
AbsLogger()
|
||||
}()
|
||||
|
||||
public static func info(_ tag: String = #file, message: String) {
|
||||
try? shared.info(tag: fileName(file: tag), message: message)
|
||||
}
|
||||
|
||||
public static func error(_ tag: String = #file, message: String, error: Error? = nil) {
|
||||
try? shared.error(tag: fileName(file: tag), message: message)
|
||||
}
|
||||
|
||||
private let loggers: Dictionary<String, AppLogger> = [:]
|
||||
|
||||
private func _logger(_ tag: String) -> AppLogger {
|
||||
return loggers[tag, default: AppLogger(category: tag)]
|
||||
}
|
||||
|
||||
private func saveLogEntry(_ level: AbsLogLevel, tag: String, message: String) throws {
|
||||
let entry = LogEntry()
|
||||
entry.tag = tag
|
||||
entry.message = message
|
||||
entry.level = "\(level)"
|
||||
entry.timestamp = Int(Date().timeIntervalSince1970 * 1000)
|
||||
try Database.shared.saveLog(entry)
|
||||
self.notifyListeners("onLog", data: ["value": entry])
|
||||
}
|
||||
|
||||
public func info(tag: String = "\(#file)", message: String) throws {
|
||||
_logger(tag).log("[\(tag)] \(message)")
|
||||
try saveLogEntry(.info, tag: tag, message: message)
|
||||
}
|
||||
|
||||
public func error(tag: String = "\(#file)", message: String, error: Error? = nil) throws {
|
||||
_logger(tag).error("[\(tag)] \(message)")
|
||||
if let error = error { _logger(tag).error(error) }
|
||||
try saveLogEntry(.error, tag: tag, message: message)
|
||||
}
|
||||
|
||||
@objc func info(_ call: CAPPluginCall) {
|
||||
let message = call.getString("message") ?? ""
|
||||
let tag = call.getString("tag") ?? ""
|
||||
|
||||
logger.log("[\(tag)] \(message)")
|
||||
call.resolve()
|
||||
|
||||
do {
|
||||
try info(tag: tag, message: message)
|
||||
call.resolve()
|
||||
} catch {
|
||||
call.reject("Failed to log \(message)", "101", error)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@objc func error(_ call: CAPPluginCall) {
|
||||
let message = call.getString("message") ?? ""
|
||||
let tag = call.getString("tag") ?? ""
|
||||
|
||||
logger.error("[\(tag)] \(message)")
|
||||
call.resolve()
|
||||
|
||||
do {
|
||||
try error(tag: tag, message: message)
|
||||
call.resolve()
|
||||
} catch {
|
||||
call.reject("Failed to log \(message)", "101", error)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@objc func getAllLogs(_ call: CAPPluginCall) {
|
||||
call.unimplemented("Not implemented on iOS")
|
||||
do {
|
||||
let logs = Database.shared.getAllLogs()
|
||||
call.resolve([ "value": try logs.asDictionaryArray()])
|
||||
} catch {
|
||||
call.reject("Failed to get logs", "100", error)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@objc func clearLogs(_ call: CAPPluginCall) {
|
||||
call.unimplemented("Not implemented on iOS")
|
||||
do {
|
||||
try Database.shared.clearLogs()
|
||||
call.resolve()
|
||||
} catch {
|
||||
call.reject("Failed to clear logs", "100", error)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,22 +89,22 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
|
||||
Capacitor: 106e7a4205f4618d582b886a975657c61179138d
|
||||
CapacitorApp: d63334c052278caf5d81585d80b21905c6f93f39
|
||||
CapacitorBrowser: 081852cf532acf77b9d2953f3a88fe5b9711fb06
|
||||
CapacitorClipboard: b98aead5dc7ec595547fc2c5d75bacd2ae3338bc
|
||||
CapacitorCommunityKeepAwake: 00dfd8fa3cca0df003c9a3e2cd7bee678aeec68b
|
||||
CapacitorCommunityVolumeButtons: 8a0443a202ed659688d85f4d44d66f42f62f2b56
|
||||
Capacitor: 03bc7cbdde6a629a8b910a9d7d78c3cc7ed09ea7
|
||||
CapacitorApp: febecbb9582cb353aed037e18ec765141f880fe9
|
||||
CapacitorBrowser: 6299776d496e968505464884d565992faa20444a
|
||||
CapacitorClipboard: 70bfdb42b877b320a6e511ab94fa7a6a55d57ecb
|
||||
CapacitorCommunityKeepAwake: ae762ce29b53147d28cfcaae5273cd1db0c38fc4
|
||||
CapacitorCommunityVolumeButtons: 1b84f7abf29cd9476cef9e8979b2854a64d2eed5
|
||||
CapacitorCordova: 5967b9ba03915ef1d585469d6e31f31dc49be96f
|
||||
CapacitorDialog: 9b934329026b2b0ffa56939bb06df3c67541a2ab
|
||||
CapacitorHaptics: 70e47470fa1a6bd6338cd102552e3846b7f9a1b3
|
||||
CapacitorNetwork: 07ec4c69c1bb696f41c23e00d31bda1bbb221bba
|
||||
CapacitorPreferences: cbf154e5e5519b7f5ab33817a334dda1e98387f9
|
||||
CapacitorStatusBar: 275cbf2f4dfc00388f519ef80c7ec22edda342c9
|
||||
CordovaPlugins: 5a72a85b45469e68556bb172409f1b6d57b27236
|
||||
CapacitorDialog: 0e09f242f6c3f5e82e4dc76b20f2a056be57a579
|
||||
CapacitorHaptics: 1f1e17041f435d8ead9ff2a34edd592c6aa6a8d6
|
||||
CapacitorNetwork: 15cb4385f0913a8ceb5e9a4d7af1ec554bdb8de8
|
||||
CapacitorPreferences: 6c98117d4d7508034a4af9db64d6b26fc75d7b94
|
||||
CapacitorStatusBar: 6e7af040d8fc4dd655999819625cae9c2d74c36f
|
||||
CordovaPlugins: 2ecbba09775516c41764dbf78ade612427311b7e
|
||||
Realm: 8b5cda39a41f17a1734da2f39c6004eb8745587a
|
||||
RealmSwift: 0b4f808fed6898f1f6c26f501f740efd80dff0b4
|
||||
WebnativellcCapacitorFilesharer: 10b111373d4dc49608935600dcbcc14605258c73
|
||||
WebnativellcCapacitorFilesharer: e3a5930240633db3335040251d66aac6762ff111
|
||||
|
||||
PODFILE CHECKSUM: 498821c0cfa2508609567fa95d7244c01cbef538
|
||||
|
||||
|
||||
17
ios/App/Shared/models/LogEntry.swift
Normal file
17
ios/App/Shared/models/LogEntry.swift
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// LogEntry.swift
|
||||
// Audiobookshelf
|
||||
//
|
||||
// Created by Christopher Jensen-Reimann on 9/3/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RealmSwift
|
||||
|
||||
class LogEntry: Object, Codable {
|
||||
@Persisted(primaryKey: true) var id: String = UUID().uuidString
|
||||
@Persisted var tag: String = ""
|
||||
@Persisted var level: String = ""
|
||||
@Persisted var message: String = ""
|
||||
@Persisted var timestamp: Int = 0
|
||||
}
|
||||
@@ -26,7 +26,6 @@ enum PlayerStatus: Int {
|
||||
|
||||
class AudioPlayer: NSObject {
|
||||
internal let queue = DispatchQueue(label: "ABSAudioPlayerQueue")
|
||||
internal let logger = AppLogger(category: "AudioPlayer")
|
||||
|
||||
private var status: PlayerStatus
|
||||
internal var rateManager: AudioPlayerRateManager
|
||||
@@ -81,7 +80,7 @@ class AudioPlayer: NSObject {
|
||||
|
||||
let playbackSession = self.getPlaybackSession()
|
||||
guard let playbackSession = playbackSession else {
|
||||
logger.error("Failed to fetch playback session. Player will not initialize")
|
||||
AbsLogger.error(message:"Failed to fetch playback session. Player will not initialize")
|
||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
|
||||
return
|
||||
}
|
||||
@@ -115,10 +114,10 @@ class AudioPlayer: NSObject {
|
||||
}
|
||||
|
||||
self.currentTrackIndex = getItemIndexForTime(time: playbackSession.currentTime)
|
||||
logger.log("Starting track index \(self.currentTrackIndex) for start time \(playbackSession.currentTime)")
|
||||
AbsLogger.info(message:"Starting track index \(self.currentTrackIndex) for start time \(playbackSession.currentTime)")
|
||||
|
||||
let playerItems = self.allPlayerItems[self.currentTrackIndex..<self.allPlayerItems.count]
|
||||
logger.log("Setting player items \(playerItems.count)")
|
||||
AbsLogger.info(message:"Setting player items \(playerItems.count)")
|
||||
|
||||
for item in Array(playerItems) {
|
||||
self.audioPlayer.insert(item, after:self.audioPlayer.items().last)
|
||||
@@ -128,7 +127,7 @@ class AudioPlayer: NSObject {
|
||||
setupQueueObserver()
|
||||
setupQueueItemStatusObserver()
|
||||
|
||||
logger.log("Audioplayer ready")
|
||||
AbsLogger.info(message:"Audioplayer ready")
|
||||
}
|
||||
|
||||
deinit {
|
||||
@@ -146,8 +145,7 @@ class AudioPlayer: NSObject {
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setActive(false)
|
||||
} catch {
|
||||
logger.error("Failed to set AVAudioSession inactive")
|
||||
logger.error(error)
|
||||
AbsLogger.error(message: "Failed to set AVAudioSession inactive", error: error)
|
||||
}
|
||||
|
||||
self.removeAudioSessionNotifications()
|
||||
@@ -253,14 +251,14 @@ class AudioPlayer: NSObject {
|
||||
self.audioPlayer.currentItem.map { item in
|
||||
self.currentTrackIndex = self.allPlayerItems.firstIndex(of:item) ?? 0
|
||||
if (self.currentTrackIndex != prevTrackIndex) {
|
||||
self.logger.log("New Current track index \(self.currentTrackIndex)")
|
||||
AbsLogger.info(message:"New Current track index \(self.currentTrackIndex)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupQueueItemStatusObserver() {
|
||||
logger.log("queueStatusObserver: Setting up")
|
||||
AbsLogger.info(message:"queueStatusObserver: Setting up")
|
||||
|
||||
// Listen for player item updates
|
||||
self.queueItemStatusObserver?.invalidate()
|
||||
@@ -275,13 +273,13 @@ class AudioPlayer: NSObject {
|
||||
}
|
||||
|
||||
private func handleQueueItemStatus(playerItem: AVPlayerItem) {
|
||||
logger.log("queueStatusObserver: Current item status changed")
|
||||
AbsLogger.info(message:"queueStatusObserver: Current item status changed")
|
||||
guard let playbackSession = self.getPlaybackSession() else {
|
||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
|
||||
return
|
||||
}
|
||||
if (playerItem.status == .readyToPlay) {
|
||||
logger.log("queueStatusObserver: Current Item Ready to play. PlayWhenReady: \(self.playWhenReady)")
|
||||
AbsLogger.info(message:"queueStatusObserver: Current Item Ready to play. PlayWhenReady: \(self.playWhenReady)")
|
||||
|
||||
// Seek the player before initializing, so a currentTime of 0 does not appear in MediaProgress / session
|
||||
let firstReady = self.status == .uninitialized
|
||||
@@ -300,7 +298,7 @@ class AudioPlayer: NSObject {
|
||||
self.status = .paused
|
||||
}
|
||||
} else if (playerItem.status == .failed) {
|
||||
logger.error("queueStatusObserver: FAILED \(playerItem.error?.localizedDescription ?? "")")
|
||||
AbsLogger.error(message:"queueStatusObserver: FAILED \(playerItem.error?.localizedDescription ?? "")")
|
||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
|
||||
}
|
||||
}
|
||||
@@ -332,7 +330,7 @@ class AudioPlayer: NSObject {
|
||||
}
|
||||
|
||||
private func resumePlayback() {
|
||||
logger.log("PLAY: Resuming playback")
|
||||
AbsLogger.info(message:"PLAY: Resuming playback")
|
||||
|
||||
self.markAudioSessionAs(active: true)
|
||||
DispatchQueue.runOnMainQueue {
|
||||
@@ -351,7 +349,7 @@ class AudioPlayer: NSObject {
|
||||
public func pause() {
|
||||
guard self.isInitialized() else { return }
|
||||
|
||||
logger.log("PAUSE: Pausing playback")
|
||||
AbsLogger.info(message:"PAUSE: Pausing playback")
|
||||
DispatchQueue.runOnMainQueue {
|
||||
self.audioPlayer.pause()
|
||||
}
|
||||
@@ -371,7 +369,7 @@ class AudioPlayer: NSObject {
|
||||
public func startFadeOut() {
|
||||
guard self.isInitialized() else { return }
|
||||
guard let currentTime = self.getCurrentTime() else { return }
|
||||
logger.log("fadeOut: Fading out playback")
|
||||
AbsLogger.info(message:"fadeOut: Fading out playback")
|
||||
|
||||
// Define fade parameters.
|
||||
let fadeDuration: Float = 60.0 // total fade duration in seconds
|
||||
@@ -409,7 +407,7 @@ class AudioPlayer: NSObject {
|
||||
// Ensure volume is exactly zero and end fade.
|
||||
self.audioPlayer.volume = targetVolume
|
||||
t.invalidate()
|
||||
self.logger.log("Fadeout: Fade complete, pausing playback")
|
||||
AbsLogger.info(message:"Fadeout: Fade complete, pausing playback")
|
||||
self.pause()
|
||||
self.audioPlayer.volume = initialVolume
|
||||
self.seek(currentTime, from: "fadeOut")
|
||||
@@ -419,12 +417,12 @@ class AudioPlayer: NSObject {
|
||||
}
|
||||
|
||||
public func seek(_ to: Double, from: String) {
|
||||
logger.log("SEEK: Seek to \(to) from \(from)")
|
||||
AbsLogger.info(message:"SEEK: Seek to \(to) from \(from)")
|
||||
|
||||
guard let playbackSession = self.getPlaybackSession() else { return }
|
||||
|
||||
let indexOfSeek = getItemIndexForTime(time: to)
|
||||
logger.log("SEEK: Seek to index \(indexOfSeek) | Current index \(self.currentTrackIndex)")
|
||||
AbsLogger.info(message:"SEEK: Seek to index \(indexOfSeek) | Current index \(self.currentTrackIndex)")
|
||||
|
||||
if self.audioPlayer.currentItem == nil {
|
||||
self.currentTrackIndex = indexOfSeek
|
||||
@@ -488,16 +486,16 @@ class AudioPlayer: NSObject {
|
||||
let currentTrack = playbackSession.audioTracks[self.currentTrackIndex]
|
||||
let ctso = currentTrack.startOffset ?? 0.0
|
||||
let trackEnd = ctso + currentTrack.duration
|
||||
logger.log("SEEK: Seeking in current item \(to) (track START = \(ctso) END = \(trackEnd))")
|
||||
AbsLogger.info(message:"SEEK: Seeking in current item \(to) (track START = \(ctso) END = \(trackEnd))")
|
||||
|
||||
let boundedTime = min(max(to, ctso), trackEnd)
|
||||
let seekTime = boundedTime - ctso
|
||||
|
||||
DispatchQueue.runOnMainQueue {
|
||||
self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000)) { [weak self] completed in
|
||||
self?.logger.log("SEEK: Completion handler called")
|
||||
AbsLogger.info(message:"SEEK: Completion handler called")
|
||||
guard completed else {
|
||||
self?.logger.log("SEEK: WARNING: seeking not completed (to \(seekTime)")
|
||||
AbsLogger.info(message:"SEEK: WARNING: seeking not completed (to \(seekTime)")
|
||||
return
|
||||
}
|
||||
guard let self = self else { return }
|
||||
@@ -574,7 +572,7 @@ class AudioPlayer: NSObject {
|
||||
} else if (playbackSession.playMethod == PlayMethod.local.rawValue) {
|
||||
guard let localFile = track.getLocalFile() else {
|
||||
// Worst case we can stream the file
|
||||
logger.log("Unable to play local file. Resulting to streaming \(track.localFileId ?? "Unknown")")
|
||||
AbsLogger.info(message:"Unable to play local file. Resulting to streaming \(track.localFileId ?? "Unknown")")
|
||||
let urlstr = "\(Store.serverConfig!.address)/api/items/\(itemId)/file/\(ino)?token=\(Store.serverConfig!.token)"
|
||||
let url = URL(string: urlstr)!
|
||||
return AVURLAsset(url: url)
|
||||
@@ -594,8 +592,7 @@ class AudioPlayer: NSObject {
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio)
|
||||
} catch {
|
||||
logger.error("Failed to set AVAudioSession category")
|
||||
logger.error(error)
|
||||
AbsLogger.error(message: "Failed to set AVAudioSession category", error: error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -603,7 +600,7 @@ class AudioPlayer: NSObject {
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setActive(active)
|
||||
} catch {
|
||||
logger.error("Failed to set audio session as active=\(active)")
|
||||
AbsLogger.error(message:"Failed to set audio session as active=\(active)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -620,7 +617,7 @@ class AudioPlayer: NSObject {
|
||||
let reasonValue = userInfo[AVAudioSessionInterruptionReasonKey] as? UInt ?? 0
|
||||
let reason = AVAudioSession.InterruptionReason(rawValue: reasonValue)
|
||||
if (reason == .appWasSuspended) {
|
||||
logger.log("AVAudioSession was suspended")
|
||||
AbsLogger.info(message:"AVAudioSession was suspended")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -812,14 +809,14 @@ class AudioPlayer: NSObject {
|
||||
if context == &playerContext {
|
||||
if keyPath == #keyPath(AVPlayer.currentItem) {
|
||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil)
|
||||
logger.log("WARNING: Item ended")
|
||||
AbsLogger.info(message:"WARNING: Item ended")
|
||||
|
||||
if audioPlayer.currentItem == nil {
|
||||
// if the queue is rebuilding, we expect the current item may be nil
|
||||
if self.isRebuildingQueue {
|
||||
return
|
||||
}
|
||||
logger.log("Player ended or next item is nil, marking ended")
|
||||
AbsLogger.info(message:"Player ended or next item is nil, marking ended")
|
||||
self.markAudioSessionAs(active: false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ extension AudioPlayer {
|
||||
}
|
||||
|
||||
public func setSleepTimer(secondsUntilSleep: Double) {
|
||||
logger.log("SLEEP TIMER: Sleeping in \(secondsUntilSleep) seconds")
|
||||
AbsLogger.info(message: "SLEEP TIMER: Sleeping in \(secondsUntilSleep) seconds")
|
||||
self.removeSleepTimer()
|
||||
self.sleepTimeRemaining = secondsUntilSleep
|
||||
|
||||
@@ -53,7 +53,7 @@ extension AudioPlayer {
|
||||
guard let currentTime = self.getCurrentTime() else { return }
|
||||
guard stopAt >= currentTime else { return }
|
||||
|
||||
logger.log("SLEEP TIMER: Scheduling for chapter end \(stopAt)")
|
||||
AbsLogger.info(message: "SLEEP TIMER: Scheduling for chapter end \(stopAt)")
|
||||
|
||||
// Schedule the observation time
|
||||
self.sleepTimeChapterStopAt = stopAt
|
||||
@@ -138,7 +138,7 @@ extension AudioPlayer {
|
||||
}
|
||||
|
||||
private func handleSleepEnd() {
|
||||
logger.log("SLEEP TIMER: Pausing audio")
|
||||
AbsLogger.info(message: "SLEEP TIMER: Pausing audio")
|
||||
self.pause()
|
||||
self.removeSleepTimer()
|
||||
}
|
||||
|
||||
@@ -10,8 +10,7 @@ import AVFoundation
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
class DefaultedAudioPlayerRateManager: NSObject, AudioPlayerRateManager {
|
||||
internal let logger = AppLogger(category: "DefaultedAudioPlayerRateManager")
|
||||
|
||||
|
||||
internal var audioPlayer: AVPlayer
|
||||
|
||||
// MARK: - AudioPlayerRateManager
|
||||
|
||||
@@ -9,7 +9,6 @@ import Foundation
|
||||
import AVFoundation
|
||||
|
||||
class LegacyAudioPlayerRateManager: NSObject, AudioPlayerRateManager {
|
||||
internal let logger = AppLogger(category: "AudioPlayer")
|
||||
|
||||
internal var audioPlayer: AVPlayer
|
||||
|
||||
@@ -54,7 +53,7 @@ class LegacyAudioPlayerRateManager: NSObject, AudioPlayerRateManager {
|
||||
let playbackSpeedChanged = rate > 0.0 && rate != self.defaultRate && !(observed && rate == 1)
|
||||
|
||||
if self.audioPlayer.rate != rate {
|
||||
logger.log("setPlaybakRate rate changed from \(self.audioPlayer.rate) to \(rate)")
|
||||
AbsLogger.info(message: "setPlaybakRate rate changed from \(self.audioPlayer.rate) to \(rate)")
|
||||
DispatchQueue.runOnMainQueue {
|
||||
self.audioPlayer.rate = rate
|
||||
}
|
||||
@@ -73,7 +72,7 @@ class LegacyAudioPlayerRateManager: NSObject, AudioPlayerRateManager {
|
||||
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
if context == &managerContext {
|
||||
if keyPath == #keyPath(AVPlayer.rate) {
|
||||
logger.log("playerContext observer player rate")
|
||||
AbsLogger.info(message: "playerContext observer player rate")
|
||||
self.handlePlaybackRateChange(change?[.newKey] as? Float ?? 1.0, observed: true)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -14,8 +14,6 @@ class PlayerProgress {
|
||||
|
||||
private static var TIME_BETWEEN_SESSION_SYNC_IN_SECONDS = 15.0
|
||||
|
||||
private let logger = AppLogger(category: "PlayerProgress")
|
||||
|
||||
private init() {}
|
||||
|
||||
|
||||
@@ -30,8 +28,7 @@ class PlayerProgress {
|
||||
try await updateServerSessionFromLocalSession(session, rateLimitSync: !isStopping)
|
||||
}
|
||||
} catch {
|
||||
logger.error("Failed to syncFromPlayer")
|
||||
logger.error(error)
|
||||
AbsLogger.error(message: "Failed to syncFromPlayer \(error)", error: error)
|
||||
}
|
||||
await UIApplication.shared.endBackgroundTask(backgroundToken)
|
||||
}
|
||||
@@ -76,7 +73,7 @@ class PlayerProgress {
|
||||
|
||||
try localMediaProgress.updateFromPlaybackSession(session)
|
||||
|
||||
logger.log("Local progress saved to the database")
|
||||
AbsLogger.info(message:"Local progress saved to the database")
|
||||
|
||||
// Send the local progress back to front-end
|
||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.localProgress.rawValue), object: nil)
|
||||
@@ -126,7 +123,7 @@ class PlayerProgress {
|
||||
session = session.freeze()
|
||||
|
||||
guard safeToSync else { return }
|
||||
logger.log("Sending sessionId(\(session.id)) to server with currentTime(\(session.currentTime))")
|
||||
AbsLogger.info(message:"Sending sessionId(\(session.id)) to server with currentTime(\(session.currentTime))")
|
||||
|
||||
var success = false
|
||||
if session.isLocal {
|
||||
@@ -155,25 +152,25 @@ class PlayerProgress {
|
||||
|
||||
// TODO: Unused for now
|
||||
private func updateLocalSessionFromServerMediaProgress() async throws {
|
||||
logger.log("updateLocalSessionFromServerMediaProgress: Checking if local media progress was updated on server")
|
||||
AbsLogger.info(message:"updateLocalSessionFromServerMediaProgress: Checking if local media progress was updated on server")
|
||||
guard let session = try Realm(queue: nil).objects(PlaybackSession.self).last(where: {
|
||||
$0.isActiveSession == true && $0.serverConnectionConfigId == Store.serverConfig?.id
|
||||
})?.freeze() else {
|
||||
logger.log("updateLocalSessionFromServerMediaProgress: Failed to get session")
|
||||
AbsLogger.info(message:"updateLocalSessionFromServerMediaProgress: Failed to get session")
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch the current progress
|
||||
let progress = await ApiClient.getMediaProgress(libraryItemId: session.libraryItemId!, episodeId: session.episodeId)
|
||||
guard let progress = progress else {
|
||||
logger.log("updateLocalSessionFromServerMediaProgress: No progress object")
|
||||
AbsLogger.info(message:"updateLocalSessionFromServerMediaProgress: No progress object")
|
||||
return
|
||||
}
|
||||
|
||||
// Determine which session is newer
|
||||
let serverLastUpdate = progress.lastUpdate
|
||||
guard let localLastUpdate = session.updatedAt else {
|
||||
logger.log("updateLocalSessionFromServerMediaProgress: No local session updatedAt")
|
||||
AbsLogger.info(message:"updateLocalSessionFromServerMediaProgress: No local session updatedAt")
|
||||
return
|
||||
}
|
||||
let serverCurrentTime = progress.currentTime
|
||||
@@ -184,16 +181,16 @@ class PlayerProgress {
|
||||
|
||||
// Update the session, if needed
|
||||
if serverIsNewerThanLocal && currentTimeIsDifferent {
|
||||
logger.log("updateLocalSessionFromServerMediaProgress: Server has newer time than local serverLastUpdate=\(serverLastUpdate) localLastUpdate=\(localLastUpdate)")
|
||||
AbsLogger.info(message:"updateLocalSessionFromServerMediaProgress: Server has newer time than local serverLastUpdate=\(serverLastUpdate) localLastUpdate=\(localLastUpdate)")
|
||||
guard let session = session.thaw() else { return }
|
||||
try session.update {
|
||||
session.currentTime = serverCurrentTime
|
||||
session.updatedAt = serverLastUpdate
|
||||
}
|
||||
logger.log("updateLocalSessionFromServerMediaProgress: Updated session currentTime newCurrentTime=\(serverCurrentTime) previousCurrentTime=\(localCurrentTime)")
|
||||
AbsLogger.info(message:"updateLocalSessionFromServerMediaProgress: Updated session currentTime newCurrentTime=\(serverCurrentTime) previousCurrentTime=\(localCurrentTime)")
|
||||
PlayerHandler.seek(amount: session.currentTime)
|
||||
} else {
|
||||
logger.log("updateLocalSessionFromServerMediaProgress: Local session does not need updating; local has latest progress")
|
||||
AbsLogger.info(message:"updateLocalSessionFromServerMediaProgress: Local session does not need updating; local has latest progress")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import Foundation
|
||||
import Alamofire
|
||||
|
||||
class ApiClient {
|
||||
private static let logger = AppLogger(category: "ApiClient")
|
||||
private static let secureStorage = SecureStorage()
|
||||
|
||||
public static func getData(from url: URL, completion: @escaping (UIImage?) -> Void) {
|
||||
@@ -22,7 +21,7 @@ class ApiClient {
|
||||
|
||||
public static func postResource<T: Decodable>(endpoint: String, parameters: [String: Any], decodable: T.Type = T.self, callback: ((_ param: T) -> Void)?) {
|
||||
if (Store.serverConfig == nil) {
|
||||
logger.error("Server config not set")
|
||||
AbsLogger.error(message: "Server config not set")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -35,7 +34,7 @@ class ApiClient {
|
||||
case .success(let obj):
|
||||
callback?(obj)
|
||||
case .failure(let error):
|
||||
logger.error("api request to \(endpoint) failed")
|
||||
AbsLogger.error(message: "api request to \(endpoint) failed")
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
@@ -43,7 +42,7 @@ 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) {
|
||||
logger.error("Server config not set")
|
||||
AbsLogger.error(message: "Server config not set")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -56,7 +55,7 @@ class ApiClient {
|
||||
case .success(let obj):
|
||||
callback?(obj)
|
||||
case .failure(let error):
|
||||
logger.error("api request to \(endpoint) failed")
|
||||
AbsLogger.error(message: "api request to \(endpoint) failed")
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
@@ -72,7 +71,7 @@ class ApiClient {
|
||||
|
||||
public static func postResource<T:Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) {
|
||||
if (Store.serverConfig == nil) {
|
||||
logger.error("Server config not set")
|
||||
AbsLogger.error(message: "Server config not set")
|
||||
callback?(false)
|
||||
return
|
||||
}
|
||||
@@ -86,7 +85,7 @@ class ApiClient {
|
||||
case .success(_):
|
||||
callback?(true)
|
||||
case .failure(let error):
|
||||
logger.error("api request to \(endpoint) failed")
|
||||
AbsLogger.error(message: "api request to \(endpoint) failed")
|
||||
print(error)
|
||||
|
||||
callback?(false)
|
||||
@@ -96,7 +95,7 @@ class ApiClient {
|
||||
|
||||
public static func patchResource<T: Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) {
|
||||
if (Store.serverConfig == nil) {
|
||||
logger.error("Server config not set")
|
||||
AbsLogger.error(message: "Server config not set")
|
||||
callback?(false)
|
||||
return
|
||||
}
|
||||
@@ -110,7 +109,7 @@ class ApiClient {
|
||||
case .success(_):
|
||||
callback?(true)
|
||||
case .failure(let error):
|
||||
logger.error("api request to \(endpoint) failed")
|
||||
AbsLogger.error(message: "api request to \(endpoint) failed")
|
||||
print(error)
|
||||
callback?(false)
|
||||
}
|
||||
@@ -127,7 +126,7 @@ class ApiClient {
|
||||
|
||||
public static func getResource<T: Decodable>(endpoint: String, decodable: T.Type = T.self, callback: ((_ param: T?) -> Void)?) {
|
||||
if (Store.serverConfig == nil) {
|
||||
logger.error("Server config not set")
|
||||
AbsLogger.error(message: "Server config not set")
|
||||
callback?(nil)
|
||||
return
|
||||
}
|
||||
@@ -141,7 +140,7 @@ class ApiClient {
|
||||
case .success(let obj):
|
||||
callback?(obj)
|
||||
case .failure(let error):
|
||||
logger.error("api request to \(endpoint) failed")
|
||||
AbsLogger.error(message: "api request to \(endpoint) failed")
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
@@ -160,22 +159,22 @@ class ApiClient {
|
||||
*/
|
||||
private static func handleTokenRefresh<T: Decodable>(originalRequest: DataRequest, endpoint: String, method: HTTPMethod, parameters: Any?, decodable: T.Type, callback: ((_ param: T?) -> Void)?) {
|
||||
guard let serverConfig = Store.serverConfig else {
|
||||
logger.error("handleTokenRefresh: No server config available")
|
||||
AbsLogger.error(message: "handleTokenRefresh: No server config available")
|
||||
callback?(nil)
|
||||
return
|
||||
}
|
||||
|
||||
logger.log("handleTokenRefresh: Attempting to refresh auth tokens for server \(serverConfig.name)")
|
||||
AbsLogger.info(message: "handleTokenRefresh: Attempting to refresh auth tokens for server \(serverConfig.name)")
|
||||
|
||||
// Get refresh token from secure storage
|
||||
guard let refreshToken = secureStorage.getRefreshToken(serverConnectionConfigId: serverConfig.id) else {
|
||||
logger.error("handleTokenRefresh: No refresh token available for server \(serverConfig.name)")
|
||||
AbsLogger.error(message: "handleTokenRefresh: No refresh token available for server \(serverConfig.name)")
|
||||
handleRefreshFailure()
|
||||
callback?(nil)
|
||||
return
|
||||
}
|
||||
|
||||
logger.log("handleTokenRefresh: Retrieved refresh token, attempting to refresh access token")
|
||||
AbsLogger.info(message: "handleTokenRefresh: Retrieved refresh token, attempting to refresh access token")
|
||||
|
||||
// Create refresh token request
|
||||
let refreshHeaders: HTTPHeaders = [
|
||||
@@ -190,23 +189,23 @@ class ApiClient {
|
||||
case .success(let refreshResponse):
|
||||
guard let user = refreshResponse.user,
|
||||
!user.accessToken.isEmpty else {
|
||||
logger.error("handleTokenRefresh: No access token in refresh response for server \(serverConfig.name)")
|
||||
AbsLogger.error(message: "handleTokenRefresh: No access token in refresh response for server \(serverConfig.name)")
|
||||
handleRefreshFailure()
|
||||
callback?(nil)
|
||||
return
|
||||
}
|
||||
|
||||
logger.log("handleTokenRefresh: Successfully obtained new access token")
|
||||
AbsLogger.info(message: "handleTokenRefresh: Successfully obtained new access token")
|
||||
|
||||
// Update tokens in secure storage and store
|
||||
updateTokens(newAccessToken: user.accessToken, newRefreshToken: user.refreshToken ?? refreshToken, serverConnectionConfigId: serverConfig.id)
|
||||
|
||||
// Retry the original request with the new access token
|
||||
logger.log("handleTokenRefresh: Retrying original request with new token")
|
||||
AbsLogger.info(message: "handleTokenRefresh: Retrying original request with new token")
|
||||
retryOriginalRequest(endpoint: endpoint, method: method, parameters: parameters, decodable: decodable, newAccessToken: user.accessToken, callback: callback)
|
||||
|
||||
case .failure(let error):
|
||||
logger.error("handleTokenRefresh: Refresh request failed for server \(serverConfig.name): \(error)")
|
||||
AbsLogger.error(message: "handleTokenRefresh: Refresh request failed for server \(serverConfig.name): \(error)")
|
||||
handleRefreshFailure()
|
||||
callback?(nil)
|
||||
}
|
||||
@@ -220,14 +219,14 @@ class ApiClient {
|
||||
// Update the refresh token in secure storage if it's new
|
||||
if newRefreshToken != secureStorage.getRefreshToken(serverConnectionConfigId: serverConnectionConfigId) {
|
||||
let hasStored = secureStorage.storeRefreshToken(serverConnectionConfigId: serverConnectionConfigId, refreshToken: newRefreshToken)
|
||||
logger.log("updateTokens: Updated refresh token in secure storage. Stored=\(hasStored)")
|
||||
AbsLogger.info(message: "updateTokens: Updated refresh token in secure storage. Stored=\(hasStored)")
|
||||
}
|
||||
|
||||
// Update access token on server connection config
|
||||
Database.shared.updateServerConnectionConfigToken(newToken: newAccessToken)
|
||||
logger.log("updateTokens: Updated access token in server connection config")
|
||||
AbsLogger.info(message: "updateTokens: Updated access token in server connection config")
|
||||
|
||||
logger.log("updateTokens: Successfully refreshed auth tokens for server \(Store.serverConfig?.name ?? "unknown")")
|
||||
AbsLogger.info(message: "updateTokens: Successfully refreshed auth tokens for server \(Store.serverConfig?.name ?? "unknown")")
|
||||
|
||||
// Notify webview frontend about token refresh
|
||||
if let callback = AbsDatabase.tokenRefreshCallback {
|
||||
@@ -241,7 +240,7 @@ class ApiClient {
|
||||
*/
|
||||
private static func retryOriginalRequest<T: Decodable>(endpoint: String, method: HTTPMethod, parameters: Any?, decodable: T.Type, newAccessToken: String, callback: ((_ param: T?) -> Void)?) {
|
||||
guard let serverConfig = Store.serverConfig else {
|
||||
logger.error("retryOriginalRequest: No server config available")
|
||||
AbsLogger.error(message: "retryOriginalRequest: No server config available")
|
||||
callback?(nil)
|
||||
return
|
||||
}
|
||||
@@ -270,7 +269,7 @@ class ApiClient {
|
||||
retryRequest = AF.request("\(serverConfig.address)/\(endpoint)", method: .patch, headers: headers)
|
||||
}
|
||||
default:
|
||||
logger.error("retryOriginalRequest: Unsupported method \(method)")
|
||||
AbsLogger.error(message: "retryOriginalRequest: Unsupported method \(method)")
|
||||
callback?(nil)
|
||||
return
|
||||
}
|
||||
@@ -282,7 +281,7 @@ class ApiClient {
|
||||
if let data = response.data, !data.isEmpty {
|
||||
// If it is a string return nil (e.g. express returns OK for 200 status codes)
|
||||
if let responseString = String(data: data, encoding: .utf8) {
|
||||
logger.log("retryOriginalRequest: Got string response '\(responseString)'")
|
||||
AbsLogger.info(message: "retryOriginalRequest: Got string response '\(responseString)'")
|
||||
callback?(nil)
|
||||
return
|
||||
}
|
||||
@@ -292,16 +291,16 @@ class ApiClient {
|
||||
let decodedObject = try JSONDecoder().decode(decodable, from: data)
|
||||
callback?(decodedObject)
|
||||
} catch {
|
||||
logger.error("retryOriginalRequest: JSON decode failed: \(error)")
|
||||
AbsLogger.error(message: "retryOriginalRequest: JSON decode failed: \(error)", error: error)
|
||||
callback?(nil)
|
||||
}
|
||||
} else {
|
||||
// Empty response
|
||||
logger.log("retryOriginalRequest: Empty response with success status \(statusCode)")
|
||||
AbsLogger.info(message: "retryOriginalRequest: Empty response with success status \(statusCode)")
|
||||
callback?(nil)
|
||||
}
|
||||
} else {
|
||||
logger.error("retryOriginalRequest: Request failed with status \(response.response?.statusCode ?? 0)")
|
||||
AbsLogger.error(message: "retryOriginalRequest: Request failed with status \(response.response?.statusCode ?? 0)")
|
||||
callback?(nil)
|
||||
}
|
||||
}
|
||||
@@ -312,7 +311,7 @@ class ApiClient {
|
||||
* This will clear the current server connection and notify webview
|
||||
*/
|
||||
private static func handleRefreshFailure() {
|
||||
logger.log("handleRefreshFailure: Token refresh failed, clearing session")
|
||||
AbsLogger.info(message: "handleRefreshFailure: Token refresh failed, clearing session")
|
||||
|
||||
// Clear the current server connection
|
||||
Store.serverConfig = nil
|
||||
@@ -332,7 +331,7 @@ class ApiClient {
|
||||
|
||||
public static func getResourceWithTokenRefresh<T: Decodable>(endpoint: String, decodable: T.Type = T.self, callback: ((_ param: T?) -> Void)?) {
|
||||
if (Store.serverConfig == nil) {
|
||||
logger.error("Server config not set")
|
||||
AbsLogger.error(message: "Server config not set")
|
||||
callback?(nil)
|
||||
return
|
||||
}
|
||||
@@ -345,14 +344,14 @@ class ApiClient {
|
||||
|
||||
request.responseDecodable(of: decodable) { response in
|
||||
if let statusCode = response.response?.statusCode, statusCode == 401 {
|
||||
logger.log("getResourceWithTokenRefresh: 401 Unauthorized for request to \(endpoint) - attempting token refresh")
|
||||
AbsLogger.info(message: "getResourceWithTokenRefresh: 401 Unauthorized for request to \(endpoint) - attempting token refresh")
|
||||
handleTokenRefresh(originalRequest: request, endpoint: endpoint, method: .get, parameters: nil, decodable: decodable, callback: callback)
|
||||
} else {
|
||||
switch response.result {
|
||||
case .success(let obj):
|
||||
callback?(obj)
|
||||
case .failure(let error):
|
||||
logger.error("api request to \(endpoint) failed")
|
||||
AbsLogger.error(message: "api request to \(endpoint) failed")
|
||||
print(error)
|
||||
callback?(nil)
|
||||
}
|
||||
@@ -362,7 +361,7 @@ class ApiClient {
|
||||
|
||||
public static func postResourceWithTokenRefresh<T: Encodable, U: Decodable>(endpoint: String, parameters: T, decodable: U.Type = U.self, callback: ((_ param: U?) -> Void)?) {
|
||||
if (Store.serverConfig == nil) {
|
||||
logger.error("Server config not set")
|
||||
AbsLogger.error(message: "Server config not set")
|
||||
callback?(nil)
|
||||
return
|
||||
}
|
||||
@@ -375,14 +374,14 @@ class ApiClient {
|
||||
|
||||
request.responseDecodable(of: decodable) { response in
|
||||
if let statusCode = response.response?.statusCode, statusCode == 401 {
|
||||
logger.log("postResourceWithTokenRefresh: 401 Unauthorized for request to \(endpoint) - attempting token refresh")
|
||||
AbsLogger.info(message: "postResourceWithTokenRefresh: 401 Unauthorized for request to \(endpoint) - attempting token refresh")
|
||||
handleTokenRefresh(originalRequest: request, endpoint: endpoint, method: .post, parameters: parameters, decodable: decodable, callback: callback)
|
||||
} else {
|
||||
switch response.result {
|
||||
case .success(let obj):
|
||||
callback?(obj)
|
||||
case .failure(let error):
|
||||
logger.error("api request to \(endpoint) failed")
|
||||
AbsLogger.error(message: "api request to \(endpoint) failed")
|
||||
print(error)
|
||||
callback?(nil)
|
||||
}
|
||||
@@ -395,7 +394,7 @@ class ApiClient {
|
||||
*/
|
||||
public static func postResourceWithTokenRefresh<T: Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) {
|
||||
if (Store.serverConfig == nil) {
|
||||
logger.error("Server config not set")
|
||||
AbsLogger.error(message: "Server config not set")
|
||||
callback?(false)
|
||||
return
|
||||
}
|
||||
@@ -408,7 +407,7 @@ class ApiClient {
|
||||
|
||||
request.response { response in
|
||||
if let statusCode = response.response?.statusCode, statusCode == 401 {
|
||||
logger.log("postResourceWithTokenRefresh: 401 Unauthorized for request to \(endpoint) - attempting token refresh")
|
||||
AbsLogger.info(message: "postResourceWithTokenRefresh: 401 Unauthorized for request to \(endpoint) - attempting token refresh")
|
||||
handleTokenRefresh(originalRequest: request, endpoint: endpoint, method: .post, parameters: parameters, decodable: EmptyResponse.self) { result in
|
||||
callback?(result != nil)
|
||||
}
|
||||
@@ -417,7 +416,7 @@ class ApiClient {
|
||||
case .success(_):
|
||||
callback?(true)
|
||||
case .failure(let error):
|
||||
logger.error("api request to \(endpoint) failed")
|
||||
AbsLogger.error(message: "api request to \(endpoint) failed")
|
||||
print(error)
|
||||
callback?(false)
|
||||
}
|
||||
@@ -427,7 +426,7 @@ class ApiClient {
|
||||
|
||||
public static func patchResourceWithTokenRefresh<T: Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) {
|
||||
if (Store.serverConfig == nil) {
|
||||
logger.error("Server config not set")
|
||||
AbsLogger.error(message: "Server config not set")
|
||||
callback?(false)
|
||||
return
|
||||
}
|
||||
@@ -440,7 +439,7 @@ class ApiClient {
|
||||
|
||||
request.response { response in
|
||||
if let statusCode = response.response?.statusCode, statusCode == 401 {
|
||||
logger.log("patchResourceWithTokenRefresh: 401 Unauthorized for request to \(endpoint) - attempting token refresh")
|
||||
AbsLogger.info(message: "patchResourceWithTokenRefresh: 401 Unauthorized for request to \(endpoint) - attempting token refresh")
|
||||
handleTokenRefresh(originalRequest: request, endpoint: endpoint, method: .patch, parameters: parameters, decodable: EmptyResponse.self) { result in
|
||||
callback?(result != nil)
|
||||
}
|
||||
@@ -449,7 +448,7 @@ class ApiClient {
|
||||
case .success(_):
|
||||
callback?(true)
|
||||
case .failure(let error):
|
||||
logger.error("api request to \(endpoint) failed")
|
||||
AbsLogger.error(message: "api request to \(endpoint) failed")
|
||||
print(error)
|
||||
callback?(false)
|
||||
}
|
||||
@@ -489,7 +488,7 @@ class ApiClient {
|
||||
// Use the new token refresh-enabled method
|
||||
postResourceWithTokenRefresh(endpoint: endpoint, parameters: parameters, decodable: PlaybackSession.self) { session in
|
||||
guard let session = session else {
|
||||
logger.error("startPlaybackSession: Failed to create playback session")
|
||||
AbsLogger.error(message: "startPlaybackSession: Failed to create playback session")
|
||||
callback(PlaybackSession()) // Return empty session on failure
|
||||
return
|
||||
}
|
||||
@@ -533,14 +532,14 @@ class ApiClient {
|
||||
let localMediaProgressList = Database.shared.getAllLocalMediaProgress().filter {
|
||||
$0.serverConnectionConfigId == Store.serverConfig?.id
|
||||
}.map { $0.freeze() }
|
||||
logger.log("syncLocalSessionsWithServer: Found \(localMediaProgressList.count) local media progress for server")
|
||||
AbsLogger.info(message: "syncLocalSessionsWithServer: Found \(localMediaProgressList.count) local media progress for server")
|
||||
|
||||
if (localMediaProgressList.isEmpty) {
|
||||
logger.log("syncLocalSessionsWithServer: No local progress to sync")
|
||||
AbsLogger.info(message: "syncLocalSessionsWithServer: No local progress to sync")
|
||||
} else {
|
||||
let currentUser = await ApiClient.getCurrentUser()
|
||||
guard let currentUser = currentUser else {
|
||||
logger.log("syncLocalSessionsWithServer: No User")
|
||||
AbsLogger.info(message: "syncLocalSessionsWithServer: No User")
|
||||
return
|
||||
}
|
||||
try currentUser.mediaProgress.forEach { mediaProgress in
|
||||
@@ -552,12 +551,12 @@ class ApiClient {
|
||||
}
|
||||
}
|
||||
if (localMediaProgress != nil && mediaProgress.lastUpdate > localMediaProgress!.lastUpdate) {
|
||||
logger.log("syncLocalSessionsWithServer: Updating local media progress \(localMediaProgress!.id) with server media progress")
|
||||
AbsLogger.info(message: "syncLocalSessionsWithServer: Updating local media progress \(localMediaProgress!.id) with server media progress")
|
||||
if let localMediaProgress = localMediaProgress?.thaw() {
|
||||
try localMediaProgress.updateFromServerMediaProgress(mediaProgress)
|
||||
}
|
||||
} else if (localMediaProgress != nil) {
|
||||
logger.log("syncLocalSessionsWithServer: Local progress for \(localMediaProgress!.id) is more recent then server progress")
|
||||
AbsLogger.info(message: "syncLocalSessionsWithServer: Local progress for \(localMediaProgress!.id) is more recent then server progress")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -566,13 +565,13 @@ class ApiClient {
|
||||
let playbackSessions = Database.shared.getAllPlaybackSessions().filter {
|
||||
$0.serverConnectionConfigId == Store.serverConfig?.id
|
||||
}.map { $0.freeze() }
|
||||
logger.log("syncLocalSessionsWithServer: Found \(playbackSessions.count) playback sessions for server (first sync: \(isFirstSync))")
|
||||
AbsLogger.info(message: "syncLocalSessionsWithServer: Found \(playbackSessions.count) playback sessions for server (first sync: \(isFirstSync))")
|
||||
if (!playbackSessions.isEmpty) {
|
||||
let success = await ApiClient.reportAllLocalPlaybackSessions(playbackSessions)
|
||||
if (success) {
|
||||
// Remove sessions from db
|
||||
try playbackSessions.forEach { session in
|
||||
logger.log("syncLocalSessionsWithServer: Handling \(session.displayTitle ?? "") (\(session.id)) \(session.isActiveSession)")
|
||||
AbsLogger.info(message: "syncLocalSessionsWithServer: Handling \(session.displayTitle ?? "") (\(session.id)) \(session.isActiveSession)")
|
||||
// On first sync then remove all sessions
|
||||
if (!session.isActiveSession || isFirstSync) {
|
||||
if let session = session.thaw() {
|
||||
@@ -589,7 +588,7 @@ class ApiClient {
|
||||
}
|
||||
|
||||
public static func updateMediaProgress<T:Encodable>(libraryItemId: String, episodeId: String?, payload: T, callback: @escaping () -> Void) {
|
||||
logger.log("updateMediaProgress \(libraryItemId) \(episodeId ?? "NIL") \(payload)")
|
||||
AbsLogger.info(message: "updateMediaProgress \(libraryItemId) \(episodeId ?? "NIL") \(payload)")
|
||||
let endpoint = episodeId?.isEmpty ?? true ? "api/me/progress/\(libraryItemId)" : "api/me/progress/\(libraryItemId)/\(episodeId ?? "")"
|
||||
patchResourceWithTokenRefresh(endpoint: endpoint, parameters: payload) { _ in
|
||||
callback()
|
||||
@@ -597,7 +596,7 @@ class ApiClient {
|
||||
}
|
||||
|
||||
public static func getMediaProgress(libraryItemId: String, episodeId: String?) async -> MediaProgress? {
|
||||
logger.log("getMediaProgress \(libraryItemId) \(episodeId ?? "NIL")")
|
||||
AbsLogger.info(message: "getMediaProgress \(libraryItemId) \(episodeId ?? "NIL")")
|
||||
let endpoint = episodeId?.isEmpty ?? true ? "api/me/progress/\(libraryItemId)" : "api/me/progress/\(libraryItemId)/\(episodeId ?? "")"
|
||||
return await withCheckedContinuation { continuation in
|
||||
getResourceWithTokenRefresh(endpoint: endpoint, decodable: MediaProgress.self) { result in
|
||||
@@ -607,7 +606,7 @@ class ApiClient {
|
||||
}
|
||||
|
||||
public static func getCurrentUser() async -> User? {
|
||||
logger.log("getCurrentUser")
|
||||
AbsLogger.info(message: "getCurrentUser")
|
||||
return await withCheckedContinuation { continuation in
|
||||
getResourceWithTokenRefresh(endpoint: "api/me", decodable: User.self) { result in
|
||||
continuation.resume(returning: result)
|
||||
|
||||
@@ -12,10 +12,14 @@ class Database {
|
||||
public static var shared = {
|
||||
return Database()
|
||||
}()
|
||||
|
||||
private let logger = AppLogger(category: "Database")
|
||||
|
||||
private init() {}
|
||||
private init() {
|
||||
do {
|
||||
try cleanExpiredLogs()
|
||||
} catch {
|
||||
debugPrint(error)
|
||||
}
|
||||
}
|
||||
|
||||
public func setServerConnectionConfig(config: ServerConnectionConfig) {
|
||||
let config = config
|
||||
@@ -33,7 +37,7 @@ class Database {
|
||||
existing.token = config.token
|
||||
}
|
||||
} catch {
|
||||
logger.error("failed to update server config")
|
||||
AbsLogger.error("setServerConn", message: "failed to update server config")
|
||||
debugPrint(error)
|
||||
}
|
||||
|
||||
@@ -54,7 +58,7 @@ class Database {
|
||||
realm.add(config)
|
||||
}
|
||||
} catch(let exception) {
|
||||
logger.error("failed to save server config")
|
||||
AbsLogger.error(message: "failed to save server config")
|
||||
debugPrint(exception)
|
||||
}
|
||||
|
||||
@@ -86,7 +90,7 @@ class Database {
|
||||
}
|
||||
}
|
||||
} catch(let exception) {
|
||||
logger.error("failed to delete server config")
|
||||
AbsLogger.error(message: "failed to delete server config")
|
||||
debugPrint(exception)
|
||||
}
|
||||
}
|
||||
@@ -117,7 +121,7 @@ class Database {
|
||||
}
|
||||
}
|
||||
} catch(let exception) {
|
||||
logger.error("failed to save server config active index")
|
||||
AbsLogger.error(message: "failed to save server config active index")
|
||||
debugPrint(exception)
|
||||
}
|
||||
}
|
||||
@@ -137,7 +141,7 @@ class Database {
|
||||
realm.add(deviceSettings)
|
||||
}
|
||||
} catch {
|
||||
logger.error("failed to save device settings")
|
||||
AbsLogger.error(message: "failed to save device settings")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,4 +279,54 @@ class Database {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func saveLog(_ log: LogEntry) throws {
|
||||
let realm = try Realm()
|
||||
return try realm.write { realm.add(log) }
|
||||
}
|
||||
|
||||
public func getAllLogs() -> [LogEntry] {
|
||||
do {
|
||||
let realm = try Realm()
|
||||
return realm.objects(LogEntry.self).toArray()
|
||||
} catch {
|
||||
debugPrint(error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
public func clearLogs() throws {
|
||||
do {
|
||||
let realm = try! Realm()
|
||||
try realm.write {
|
||||
realm.objects(LogEntry.self).forEach { log in
|
||||
realm.delete(log)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
AbsLogger.error(message: "\(error)", error: error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private func cleanExpiredLogs() throws {
|
||||
let realm = try Realm()
|
||||
let numberOfHoursToKeep = 48
|
||||
let keepLogCutoff = Date().addingTimeInterval(TimeInterval(-1 * numberOfHoursToKeep * 3600))
|
||||
|
||||
let allLogs = getAllLogs()
|
||||
var logsRemoved = 0
|
||||
try? realm.write {
|
||||
allLogs.forEach { log in
|
||||
if log.timestamp < Int(keepLogCutoff.timeIntervalSince1970) {
|
||||
realm.delete(log)
|
||||
logsRemoved += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if logsRemoved > 0 {
|
||||
AbsLogger.info(message: "cleanLogs: Removed \(logsRemoved) logs older than \(numberOfHoursToKeep) hours")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,8 +65,7 @@ extension CAPPluginCall {
|
||||
let json = try JSONSerialization.data(withJSONObject: value)
|
||||
return try JSONDecoder().decode(type, from: json)
|
||||
} catch {
|
||||
AppLogger().error("Failed to get json for \(key)")
|
||||
debugPrint(error)
|
||||
AbsLogger.error(message: "Failed to get json for \(key)", error: error)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -106,3 +105,80 @@ extension URL {
|
||||
return attributes?[.creationDate] as? Date
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - RealmSwift helpers
|
||||
// From https://github.com/realm/realm-swift/issues/5859#issuecomment-589026869
|
||||
|
||||
extension Results {
|
||||
func toArray<T: Object>() -> [T] {
|
||||
var array = [T]()
|
||||
|
||||
for i in 0 ..< self.count {
|
||||
if let result = self[i] as? T {
|
||||
array.append(result.detached())
|
||||
}
|
||||
}
|
||||
|
||||
return array
|
||||
}
|
||||
}
|
||||
|
||||
extension List {
|
||||
func toArray<T: Object>() -> [T] {
|
||||
var array = [T]()
|
||||
|
||||
for i in 0 ..< self.count {
|
||||
if let result = self[i] as? T {
|
||||
array.append(result.detached())
|
||||
}
|
||||
}
|
||||
|
||||
return array
|
||||
}
|
||||
}
|
||||
|
||||
protocol DetachableObject: AnyObject {
|
||||
func detached() -> Self
|
||||
}
|
||||
|
||||
extension Object: DetachableObject {
|
||||
func detached() -> Self {
|
||||
let detached = type(of: self).init()
|
||||
for property in objectSchema.properties {
|
||||
guard let value = value(forKey: property.name) else { continue }
|
||||
if property.isArray == true {
|
||||
//Realm List property support
|
||||
let detachable = value as? DetachableObject
|
||||
detached.setValue(detachable?.detached(), forKey: property.name)
|
||||
} else if property.type == .object {
|
||||
//Realm Object property support
|
||||
let detachable = value as? DetachableObject
|
||||
detached.setValue(detachable?.detached(), forKey: property.name)
|
||||
} else {
|
||||
detached.setValue(value, forKey: property.name)
|
||||
}
|
||||
}
|
||||
return detached
|
||||
}
|
||||
}
|
||||
|
||||
extension List: DetachableObject {
|
||||
func detached() -> List<Element> {
|
||||
let result = List<Element>()
|
||||
|
||||
forEach {
|
||||
if let detachable = $0 as? DetachableObject {
|
||||
let detached = detachable.detached() as! Element
|
||||
result.append(detached)
|
||||
} else {
|
||||
result.append($0) //Primtives are pass by value; don't need to recreate
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func toArray() -> [Element] {
|
||||
return Array(self.detached())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<div class="flex-grow"></div>
|
||||
<div class="text-xs text-gray-400">{{ log.tag }}</div>
|
||||
</div>
|
||||
<div class="text-xs">{{ maskServerAddress ? log.maskedMessage : log.message }}</div>
|
||||
<div class="text-xs break-words">{{ maskServerAddress ? log.maskedMessage : log.message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
12
readme.md
12
readme.md
@@ -144,6 +144,12 @@ npx cap open android
|
||||
|
||||
Start coding!
|
||||
|
||||
After making changes to the JS layer you need to rebuild the nuxt pages and sync them to the native shells:
|
||||
|
||||
```shell
|
||||
npm run sync
|
||||
```
|
||||
|
||||
### Mac Environment Setup for iOS
|
||||
|
||||
Required Software:
|
||||
@@ -213,3 +219,9 @@ npx cap open ios
|
||||
<br>
|
||||
|
||||
Start coding!
|
||||
|
||||
After making changes to the JS layer you need to rebuild the nuxt pages and sync them to the native shells:
|
||||
|
||||
```shell
|
||||
npm run sync
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user