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:
Clay Jensen-Reimann
2025-09-07 16:31:59 -05:00
parent 00ee7b662e
commit b773ec7690
21 changed files with 431 additions and 211 deletions

View File

@@ -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

View File

@@ -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({

View File

@@ -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 */,

View File

@@ -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 {

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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)
}
}
}

View File

@@ -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

View 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
}

View File

@@ -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)
}
}

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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")
}
}

View File

@@ -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)

View File

@@ -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")
}
}
}

View File

@@ -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())
}
}

View File

@@ -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>

View File

@@ -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
```