mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-25 21:04:02 +02:00
Merge pull request #1679 from clayreimann/feat-ios-logs
feat: add logs on iOS
This commit is contained in:
@@ -6,3 +6,7 @@ indent_size = 2
|
|||||||
charset = utf-8
|
charset = utf-8
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.swift]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|||||||
@@ -134,14 +134,12 @@ export default {
|
|||||||
to: '/settings'
|
to: '/settings'
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this.$platform !== 'ios') {
|
items.push({
|
||||||
items.push({
|
icon: 'bug_report',
|
||||||
icon: 'bug_report',
|
iconOutlined: true,
|
||||||
iconOutlined: true,
|
text: this.$strings.ButtonLogs,
|
||||||
text: this.$strings.ButtonLogs,
|
to: '/logs'
|
||||||
to: '/logs'
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.serverConnectionConfig) {
|
if (this.serverConnectionConfig) {
|
||||||
items.push({
|
items.push({
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
|
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
|
||||||
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
|
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
|
||||||
65643F4DAA661FB0B247247E /* Pods_Audiobookshelf.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8F9A7CCC479333E44DC314BE /* Pods_Audiobookshelf.framework */; };
|
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 */; };
|
E9D5504628AC1A3900C746DD /* LibraryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5504528AC1A3900C746DD /* LibraryItem.swift */; };
|
||||||
E9D5504828AC1A7A00C746DD /* MediaType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5504728AC1A7A00C746DD /* MediaType.swift */; };
|
E9D5504828AC1A7A00C746DD /* MediaType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5504728AC1A7A00C746DD /* MediaType.swift */; };
|
||||||
E9D5504A28AC1AA600C746DD /* Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5504928AC1AA600C746DD /* Metadata.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>"; };
|
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>"; };
|
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; 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; };
|
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>"; };
|
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>"; };
|
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 */,
|
E9D5506128AC1CC900C746DD /* PlayerState.swift */,
|
||||||
4DF74911287105C600AC7814 /* DeviceSettings.swift */,
|
4DF74911287105C600AC7814 /* DeviceSettings.swift */,
|
||||||
E9D5507428AEF93100C746DD /* PlayerSettings.swift */,
|
E9D5507428AEF93100C746DD /* PlayerSettings.swift */,
|
||||||
|
6F2516262E68DA3000F40541 /* LogEntry.swift */,
|
||||||
E9D5506328AC1D3F00C746DD /* server */,
|
E9D5506328AC1D3F00C746DD /* server */,
|
||||||
E9D5506428AC1D5800C746DD /* local */,
|
E9D5506428AC1D5800C746DD /* local */,
|
||||||
E9D5506D28AC1E7400C746DD /* download */,
|
E9D5506D28AC1E7400C746DD /* download */,
|
||||||
@@ -533,6 +536,7 @@
|
|||||||
3ABF580928059BAE005DFBE5 /* PlaybackSession.swift in Sources */,
|
3ABF580928059BAE005DFBE5 /* PlaybackSession.swift in Sources */,
|
||||||
E9D5506628AC1D7300C746DD /* LocalLibraryItem.swift in Sources */,
|
E9D5506628AC1D7300C746DD /* LocalLibraryItem.swift in Sources */,
|
||||||
E9D5504628AC1A3900C746DD /* LibraryItem.swift in Sources */,
|
E9D5504628AC1A3900C746DD /* LibraryItem.swift in Sources */,
|
||||||
|
6F2516272E68DA3000F40541 /* LogEntry.swift in Sources */,
|
||||||
3ABF618F2804325C0070250E /* PlayerHandler.swift in Sources */,
|
3ABF618F2804325C0070250E /* PlayerHandler.swift in Sources */,
|
||||||
3AD4FCED28044E6C006DB301 /* Store.swift in Sources */,
|
3AD4FCED28044E6C006DB301 /* Store.swift in Sources */,
|
||||||
4D66B958282EEA14008272D4 /* AbsFileSystem.swift in Sources */,
|
4D66B958282EEA14008272D4 /* AbsFileSystem.swift in Sources */,
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import RealmSwift
|
|||||||
@UIApplicationMain
|
@UIApplicationMain
|
||||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
|
||||||
private let logger = AppLogger(category: "AppDelegate")
|
|
||||||
|
|
||||||
lazy var window: UIWindow? = UIWindow(frame: UIScreen.main.bounds)
|
lazy var window: UIWindow? = UIWindow(frame: UIScreen.main.bounds)
|
||||||
var backgroundCompletionHandler: (() -> Void)?
|
var backgroundCompletionHandler: (() -> Void)?
|
||||||
|
|
||||||
@@ -17,13 +15,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
schemaVersion: 20,
|
schemaVersion: 20,
|
||||||
migrationBlock: { [weak self] migration, oldSchemaVersion in
|
migrationBlock: { [weak self] migration, oldSchemaVersion in
|
||||||
if (oldSchemaVersion < 1) {
|
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
|
migration.enumerateObjects(ofType: DeviceSettings.className()) { oldObject, newObject in
|
||||||
newObject?["enableAltView"] = false
|
newObject?["enableAltView"] = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldSchemaVersion < 4) {
|
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
|
var indexCounter = 1
|
||||||
migration.enumerateObjects(ofType: ServerConnectionConfig.className()) { oldObject, newObject in
|
migration.enumerateObjects(ofType: ServerConnectionConfig.className()) { oldObject, newObject in
|
||||||
newObject?["index"] = indexCounter
|
newObject?["index"] = indexCounter
|
||||||
@@ -31,44 +29,44 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldSchemaVersion < 5) {
|
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
|
migration.enumerateObjects(ofType: DeviceSettings.className()) { oldObject, newObject in
|
||||||
newObject?["lockOrientation"] = "NONE"
|
newObject?["lockOrientation"] = "NONE"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldSchemaVersion < 6) {
|
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
|
migration.enumerateObjects(ofType: DeviceSettings.className()) { oldObject, newObject in
|
||||||
newObject?["hapticFeedback"] = "LIGHT"
|
newObject?["hapticFeedback"] = "LIGHT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldSchemaVersion < 15) {
|
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
|
migration.enumerateObjects(ofType: DeviceSettings.className()) { oldObject, newObject in
|
||||||
newObject?["languageCode"] = "en-us"
|
newObject?["languageCode"] = "en-us"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldSchemaVersion < 16) {
|
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
|
migration.enumerateObjects(ofType: PlayerSettings.className()) { oldObject, newObject in
|
||||||
newObject?["chapterTrack"] = false
|
newObject?["chapterTrack"] = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldSchemaVersion < 17) {
|
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
|
migration.enumerateObjects(ofType: PlayerSettings.className()) { oldObject, newObject in
|
||||||
newObject?["downloadUsingCellular"] = "ALWAYS"
|
newObject?["downloadUsingCellular"] = "ALWAYS"
|
||||||
newObject?["streamingUsingCellular"] = "ALWAYS"
|
newObject?["streamingUsingCellular"] = "ALWAYS"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldSchemaVersion < 18) {
|
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
|
migration.enumerateObjects(ofType: PlayerSettings.className()) { oldObject, newObject in
|
||||||
newObject?["disableSleepTimerFadeOut"] = false
|
newObject?["disableSleepTimerFadeOut"] = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldSchemaVersion < 20) {
|
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
|
migration.enumerateObjects(ofType: ServerConnectionConfig.className()) { oldObject, newObject in
|
||||||
newObject?["version"] = ""
|
newObject?["version"] = ""
|
||||||
}
|
}
|
||||||
@@ -88,22 +86,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
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.
|
// 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.
|
// 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) {
|
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.
|
// 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) {
|
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.
|
// 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) {
|
func applicationWillTerminate(_ application: UIApplication) {
|
||||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
// 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 {
|
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
|
|||||||
CAPPluginMethod(name: "setSleepTimer", returnType: CAPPluginReturnPromise)
|
CAPPluginMethod(name: "setSleepTimer", returnType: CAPPluginReturnPromise)
|
||||||
]
|
]
|
||||||
|
|
||||||
private let logger = AppLogger(category: "AbsAudioPlayer")
|
|
||||||
|
|
||||||
private var initialPlayWhenReady = false
|
private var initialPlayWhenReady = false
|
||||||
private var monitor: NWPathMonitor?
|
private var monitor: NWPathMonitor?
|
||||||
private let queue = DispatchQueue.global(qos: .background)
|
private let queue = DispatchQueue.global(qos: .background)
|
||||||
@@ -82,7 +80,7 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
|
|||||||
let startTimeOverride = call.getDouble("startTime")
|
let startTimeOverride = call.getDouble("startTime")
|
||||||
|
|
||||||
if libraryItemId == nil {
|
if libraryItemId == nil {
|
||||||
logger.error("provide library item id")
|
AbsLogger.error(message: "No id provided")
|
||||||
return call.resolve()
|
return call.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +91,7 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
|
|||||||
let item = Database.shared.getLocalLibraryItem(localLibraryItemId: libraryItemId!)
|
let item = Database.shared.getLocalLibraryItem(localLibraryItemId: libraryItemId!)
|
||||||
let episode = item?.getPodcastEpisode(episodeId: episodeId)
|
let episode = item?.getPodcastEpisode(episodeId: episodeId)
|
||||||
guard let playbackSession = item?.getPlaybackSession(episode: episode) else {
|
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([:])
|
return call.resolve([:])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +103,7 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
|
|||||||
try self.startPlaybackSession(playbackSession, playWhenReady: playWhenReady, playbackRate: playbackRate)
|
try self.startPlaybackSession(playbackSession, playWhenReady: playWhenReady, playbackRate: playbackRate)
|
||||||
call.resolve(try playbackSession.asDictionary())
|
call.resolve(try playbackSession.asDictionary())
|
||||||
} catch(let exception) {
|
} catch(let exception) {
|
||||||
logger.error("Failed to start session")
|
AbsLogger.error(message: "Failed to start session for local item: \(exception)")
|
||||||
debugPrint(exception)
|
debugPrint(exception)
|
||||||
call.resolve([:])
|
call.resolve([:])
|
||||||
}
|
}
|
||||||
@@ -119,7 +117,7 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
|
|||||||
try self?.startPlaybackSession(session, playWhenReady: playWhenReady, playbackRate: playbackRate)
|
try self?.startPlaybackSession(session, playWhenReady: playWhenReady, playbackRate: playbackRate)
|
||||||
call.resolve(try session.asDictionary())
|
call.resolve(try session.asDictionary())
|
||||||
} catch(let exception) {
|
} catch(let exception) {
|
||||||
self?.logger.error("Failed to start session")
|
AbsLogger.error(message: "Failed to start streaming session")
|
||||||
debugPrint(exception)
|
debugPrint(exception)
|
||||||
call.resolve([:])
|
call.resolve([:])
|
||||||
}
|
}
|
||||||
@@ -127,19 +125,22 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stops playback and closes session.
|
||||||
@objc func closePlayback(_ call: CAPPluginCall) {
|
@objc func closePlayback(_ call: CAPPluginCall) {
|
||||||
logger.log("Close playback")
|
AbsLogger.info(message: "Close playback")
|
||||||
|
|
||||||
PlayerHandler.stopPlayback()
|
PlayerHandler.stopPlayback()
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@objc func getCurrentTime(_ call: CAPPluginCall) {
|
@objc func getCurrentTime(_ call: CAPPluginCall) {
|
||||||
call.resolve([
|
call.resolve([
|
||||||
"value": PlayerHandler.getCurrentTime() ?? 0,
|
"value": PlayerHandler.getCurrentTime() ?? 0,
|
||||||
"bufferedTime": PlayerHandler.getCurrentTime() ?? 0,
|
"bufferedTime": PlayerHandler.getCurrentTime() ?? 0,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func setPlaybackSpeed(_ call: CAPPluginCall) {
|
@objc func setPlaybackSpeed(_ call: CAPPluginCall) {
|
||||||
let playbackRate = call.getFloat("value", 1.0)
|
let playbackRate = call.getFloat("value", 1.0)
|
||||||
let settings = PlayerSettings.main()
|
let settings = PlayerSettings.main()
|
||||||
@@ -152,7 +153,7 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
|
|||||||
|
|
||||||
@objc func setChapterTrack(_ call: CAPPluginCall) {
|
@objc func setChapterTrack(_ call: CAPPluginCall) {
|
||||||
let chapterTrack = call.getBool("enabled", true)
|
let chapterTrack = call.getBool("enabled", true)
|
||||||
logger.log(String(chapterTrack))
|
AbsLogger.info(message: String(chapterTrack))
|
||||||
let settings = PlayerSettings.main()
|
let settings = PlayerSettings.main()
|
||||||
try? settings.update {
|
try? settings.update {
|
||||||
settings.chapterTrack = chapterTrack
|
settings.chapterTrack = chapterTrack
|
||||||
@@ -225,7 +226,7 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
|
|||||||
|
|
||||||
let seconds = time / 1000
|
let seconds = time / 1000
|
||||||
|
|
||||||
logger.log("chapter time: \(isChapterTime)")
|
AbsLogger.info(message: "chapter time: \(isChapterTime)")
|
||||||
if isChapterTime {
|
if isChapterTime {
|
||||||
PlayerHandler.setChapterSleepTime(stopAt: seconds)
|
PlayerHandler.setChapterSleepTime(stopAt: seconds)
|
||||||
return call.resolve([ "success": true ])
|
return call.resolve([ "success": true ])
|
||||||
@@ -262,7 +263,7 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
|
|||||||
guard let localMediaProgressId = PlayerHandler.getPlaybackSession()?.localMediaProgressId else { return }
|
guard let localMediaProgressId = PlayerHandler.getPlaybackSession()?.localMediaProgressId else { return }
|
||||||
guard let localMediaProgress = Database.shared.getLocalMediaProgress(localMediaProgressId: localMediaProgressId) else { return }
|
guard let localMediaProgress = Database.shared.getLocalMediaProgress(localMediaProgressId: localMediaProgressId) else { return }
|
||||||
guard let progressUpdate = try? localMediaProgress.asDictionary() 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)
|
self.notifyListeners("onLocalMediaProgressUpdate", data: progressUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +273,7 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
|
|||||||
let playWhenReady = PlayerHandler.getPlayWhenReady()
|
let playWhenReady = PlayerHandler.getPlayWhenReady()
|
||||||
let libraryItemId = session?.libraryItemId ?? ""
|
let libraryItemId = session?.libraryItemId ?? ""
|
||||||
let episodeId = session?.episodeId ?? nil
|
let episodeId = session?.episodeId ?? nil
|
||||||
logger.log("Forcing Transcode")
|
AbsLogger.info(message: "Forcing Transcode")
|
||||||
|
|
||||||
// If direct playing then fallback to transcode
|
// If direct playing then fallback to transcode
|
||||||
ApiClient.startPlaybackSession(libraryItemId: libraryItemId, episodeId: episodeId, forceTranscode: true) { [weak self] session in
|
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.sendPlaybackSession(session: try session.asDictionary())
|
||||||
self.sendMetadata()
|
self.sendMetadata()
|
||||||
} catch(let exception) {
|
} catch(let exception) {
|
||||||
self?.logger.error("Failed to start transcoded session")
|
AbsLogger.error(message: "Failed to start transcoded session")
|
||||||
debugPrint(exception)
|
debugPrint(exception)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
|
|||||||
CAPPluginMethod(name: "updateLocalEbookProgress", returnType: CAPPluginReturnPromise)
|
CAPPluginMethod(name: "updateLocalEbookProgress", returnType: CAPPluginReturnPromise)
|
||||||
]
|
]
|
||||||
|
|
||||||
private let logger = AppLogger(category: "AbsDatabase")
|
|
||||||
private let secureStorage = SecureStorage()
|
private let secureStorage = SecureStorage()
|
||||||
|
|
||||||
// Used to notify the webview frontend that the token has been refreshed
|
// Used to notify the webview frontend that the token has been refreshed
|
||||||
@@ -80,7 +79,7 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
|
|||||||
if (refreshToken != "") {
|
if (refreshToken != "") {
|
||||||
// Store refresh token securely if provided
|
// Store refresh token securely if provided
|
||||||
let hasRefreshToken = secureStorage.storeRefreshToken(serverConnectionConfigId: id ?? "", refreshToken: refreshToken)
|
let hasRefreshToken = secureStorage.storeRefreshToken(serverConnectionConfigId: id ?? "", refreshToken: refreshToken)
|
||||||
logger.log("Refresh token secured = \(hasRefreshToken)")
|
AbsLogger.info(message: "Refresh token secured = \(hasRefreshToken)")
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = ServerConnectionConfig()
|
let config = ServerConnectionConfig()
|
||||||
@@ -148,15 +147,16 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
|
|||||||
let items = Database.shared.getLocalLibraryItems()
|
let items = Database.shared.getLocalLibraryItems()
|
||||||
call.resolve([ "value": try items.asDictionaryArray()])
|
call.resolve([ "value": try items.asDictionaryArray()])
|
||||||
} catch(let exception) {
|
} catch(let exception) {
|
||||||
logger.error("error while readling local library items")
|
AbsLogger.error(message: "error reading local library items \(exception)")
|
||||||
debugPrint(exception)
|
debugPrint(exception)
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func getLocalLibraryItem(_ call: CAPPluginCall) {
|
@objc func getLocalLibraryItem(_ call: CAPPluginCall) {
|
||||||
|
let id = call.getString("id") ?? ""
|
||||||
do {
|
do {
|
||||||
let item = Database.shared.getLocalLibraryItem(localLibraryItemId: call.getString("id") ?? "")
|
let item = Database.shared.getLocalLibraryItem(localLibraryItemId: id)
|
||||||
switch item {
|
switch item {
|
||||||
case .some(let foundItem):
|
case .some(let foundItem):
|
||||||
call.resolve(try foundItem.asDictionary())
|
call.resolve(try foundItem.asDictionary())
|
||||||
@@ -164,7 +164,7 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
|
|||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
} catch(let exception) {
|
} catch(let exception) {
|
||||||
logger.error("error while readling local library items")
|
AbsLogger.error(message: "error reading local library item[\(id)] \(exception)")
|
||||||
debugPrint(exception)
|
debugPrint(exception)
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
@@ -180,8 +180,7 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
|
|||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
} catch(let exception) {
|
} catch(let exception) {
|
||||||
logger.error("error while readling local library items")
|
AbsLogger.error(message: "error while readling local library items: \(exception)", error: exception)
|
||||||
debugPrint(exception)
|
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,8 +193,7 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
|
|||||||
do {
|
do {
|
||||||
call.resolve([ "value": try Database.shared.getAllLocalMediaProgress().asDictionaryArray() ])
|
call.resolve([ "value": try Database.shared.getAllLocalMediaProgress().asDictionaryArray() ])
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Error while loading local media progress")
|
AbsLogger.error(message: "Error while loading local media progress", error: error)
|
||||||
debugPrint(error)
|
|
||||||
call.resolve(["value": []])
|
call.resolve(["value": []])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,7 +210,7 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
|
|||||||
|
|
||||||
@objc func syncLocalSessionsWithServer(_ call: CAPPluginCall) {
|
@objc func syncLocalSessionsWithServer(_ call: CAPPluginCall) {
|
||||||
let isFirstSync = call.getBool("isFirstSync", false)
|
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 {
|
guard Store.serverConfig != nil else {
|
||||||
call.reject("syncLocalSessionsWithServer not connected to server")
|
call.reject("syncLocalSessionsWithServer not connected to server")
|
||||||
return call.resolve()
|
return call.resolve()
|
||||||
@@ -244,12 +242,13 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log("syncServerMediaProgressWithLocalMediaProgress: Saving local media progress")
|
AbsLogger.info(message: "Saving local media progress \(serverMediaProgress)")
|
||||||
try localMediaProgress.updateFromServerMediaProgress(serverMediaProgress)
|
try localMediaProgress.updateFromServerMediaProgress(serverMediaProgress)
|
||||||
|
|
||||||
call.resolve(try localMediaProgress.asDictionary())
|
call.resolve(try localMediaProgress.asDictionary())
|
||||||
} catch {
|
} catch {
|
||||||
call.reject("Failed to sync media progress")
|
call.reject("Failed to sync media progress")
|
||||||
|
AbsLogger.error(message: "Failed to sync: \(error)")
|
||||||
debugPrint(error)
|
debugPrint(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -264,7 +263,7 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
|
|||||||
localMediaProgressId += "-\(localEpisodeId ?? "")"
|
localMediaProgressId += "-\(localEpisodeId ?? "")"
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log("updateLocalMediaProgressFinished \(localMediaProgressId) | Is Finished: \(isFinished)")
|
AbsLogger.info(message: "\(localMediaProgressId): isFinished=\(isFinished)")
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let localMediaProgress = try LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: localMediaProgressId, localLibraryItemId: localLibraryItemId, localEpisodeId: localEpisodeId)
|
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 ebookLocation = call.getString("ebookLocation", "")
|
||||||
let ebookProgress = call.getDouble("ebookProgress", 0.0)
|
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 {
|
do {
|
||||||
let localMediaProgress = try LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: localLibraryItemId, localLibraryItemId: localLibraryItemId, localEpisodeId: nil)
|
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]
|
static private let downloadsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||||
|
|
||||||
private let logger = AppLogger(category: "AbsDownloader")
|
|
||||||
|
|
||||||
private lazy var session: URLSession = {
|
private lazy var session: URLSession = {
|
||||||
let config = URLSessionConfiguration.background(withIdentifier: "AbsDownloader")
|
let config = URLSessionConfiguration.background(withIdentifier: "AbsDownloader")
|
||||||
let queue = OperationQueue()
|
let queue = OperationQueue()
|
||||||
@@ -104,7 +102,7 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
|
|||||||
private func handleDownloadTaskUpdate(downloadTask: URLSessionTask, progressHandler: DownloadProgressHandler) {
|
private func handleDownloadTaskUpdate(downloadTask: URLSessionTask, progressHandler: DownloadProgressHandler) {
|
||||||
do {
|
do {
|
||||||
guard let downloadItemPartId = downloadTask.taskDescription else { throw LibraryItemDownloadError.noTaskDescription }
|
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
|
// Find the download item
|
||||||
let downloadItem = Database.shared.getDownloadItem(downloadItemPartId: downloadItemPartId)
|
let downloadItem = Database.shared.getDownloadItem(downloadItemPartId: downloadItemPartId)
|
||||||
@@ -119,7 +117,7 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
|
|||||||
try progressHandler(downloadItem, part)
|
try progressHandler(downloadItem, part)
|
||||||
try? self.notifyListeners("onDownloadItemPartUpdate", data: part.asDictionary())
|
try? self.notifyListeners("onDownloadItemPartUpdate", data: part.asDictionary())
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Error while processing progress")
|
AbsLogger.error(message: "Error while processing progress")
|
||||||
debugPrint(error)
|
debugPrint(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +128,7 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
|
|||||||
}
|
}
|
||||||
self.notifyDownloadProgress()
|
self.notifyDownloadProgress()
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("DownloadItemError")
|
AbsLogger.error(message: "DownloadItemError")
|
||||||
debugPrint(error)
|
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
|
// 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() {
|
private func notifyDownloadProgress() {
|
||||||
if self.monitoringProgressTimer?.isValid ?? false {
|
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 {
|
} else {
|
||||||
DispatchQueue.runOnMainQueue {
|
DispatchQueue.runOnMainQueue {
|
||||||
self.monitoringProgressTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true, block: { [unowned self] t in
|
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
|
// Fetch active downloads in a thread-safe way
|
||||||
func fetchActiveDownloads() -> [String: DownloadItem]? {
|
func fetchActiveDownloads() -> [String: DownloadItem]? {
|
||||||
self.progressStatusQueue.sync {
|
self.progressStatusQueue.sync {
|
||||||
let activeDownloads = self.downloadItemProgress
|
let activeDownloads = self.downloadItemProgress
|
||||||
if activeDownloads.isEmpty {
|
if activeDownloads.isEmpty {
|
||||||
logger.log("Finishing monitoring download progress...")
|
AbsLogger.info(message: "Finishing monitoring download progress...")
|
||||||
t.invalidate()
|
t.invalidate()
|
||||||
}
|
}
|
||||||
return activeDownloads
|
return activeDownloads
|
||||||
@@ -184,7 +182,7 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
|
|||||||
|
|
||||||
if ( downloadItem.didDownloadSuccessfully() ) {
|
if ( downloadItem.didDownloadSuccessfully() ) {
|
||||||
ApiClient.getLibraryItemWithProgress(libraryItemId: downloadItem.libraryItemId!, episodeId: downloadItem.episodeId) { [weak self] libraryItem in
|
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
|
let localDirectory = libraryItem.id
|
||||||
var coverFile: String?
|
var coverFile: String?
|
||||||
|
|
||||||
@@ -231,12 +229,12 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
|
|||||||
var episodeId = call.getString("episodeId")
|
var episodeId = call.getString("episodeId")
|
||||||
if ( episodeId == "null" ) { episodeId = nil }
|
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"]) }
|
guard let libraryItemId = libraryItemId else { return call.resolve(["error": "libraryItemId not specified"]) }
|
||||||
|
|
||||||
ApiClient.getLibraryItemWithProgress(libraryItemId: libraryItemId, episodeId: episodeId) { [weak self] libraryItem in
|
ApiClient.getLibraryItemWithProgress(libraryItemId: libraryItemId, episodeId: episodeId) { [weak self] libraryItem in
|
||||||
if let libraryItem = libraryItem {
|
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 {
|
do {
|
||||||
if let episodeId = episodeId {
|
if let episodeId = episodeId {
|
||||||
// Download a podcast episode
|
// 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 {
|
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
|
// If we don't name metadata, then we can't proceed
|
||||||
guard let filename = track.metadata?.filename else {
|
guard let filename = track.metadata?.filename else {
|
||||||
@@ -393,10 +391,10 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
|
|||||||
|
|
||||||
private func createLibraryItemFileDirectory(item: LibraryItem) throws -> String {
|
private func createLibraryItemFileDirectory(item: LibraryItem) throws -> String {
|
||||||
let itemDirectory = item.id
|
let itemDirectory = item.id
|
||||||
logger.log("ITEM DIR \(itemDirectory)")
|
AbsLogger.info(message: "ITEM DIR \(itemDirectory)")
|
||||||
|
|
||||||
guard AbsDownloader.itemDownloadFolder(path: itemDirectory) != nil else {
|
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
|
throw LibraryItemDownloadError.failedDirectory
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,7 +416,7 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
|
|||||||
|
|
||||||
return itemFolder
|
return itemFolder
|
||||||
} catch {
|
} catch {
|
||||||
AppLogger().error("Failed to CREATE LI DIRECTORY \(error)")
|
AbsLogger.error(message: "Failed to CREATE LI DIRECTORY \(error)", error: error)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,12 +23,10 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
|
|||||||
CAPPluginMethod(name: "deleteTrackFromItem", returnType: CAPPluginReturnPromise)
|
CAPPluginMethod(name: "deleteTrackFromItem", returnType: CAPPluginReturnPromise)
|
||||||
]
|
]
|
||||||
|
|
||||||
private let logger = AppLogger(category: "AbsFileSystem")
|
|
||||||
|
|
||||||
@objc func selectFolder(_ call: CAPPluginCall) {
|
@objc func selectFolder(_ call: CAPPluginCall) {
|
||||||
let mediaType = call.getString("mediaType")
|
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")
|
call.unavailable("Not available on iOS")
|
||||||
}
|
}
|
||||||
@@ -36,7 +34,7 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
|
|||||||
@objc func checkFolderPermission(_ call: CAPPluginCall) {
|
@objc func checkFolderPermission(_ call: CAPPluginCall) {
|
||||||
let folderUrl = call.getString("folderUrl")
|
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")
|
call.unavailable("Not available on iOS")
|
||||||
}
|
}
|
||||||
@@ -45,7 +43,7 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
|
|||||||
let folderId = call.getString("folderId")
|
let folderId = call.getString("folderId")
|
||||||
let forceAudioProbe = call.getBool("forceAudioProbe", false)
|
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")
|
call.unavailable("Not available on iOS")
|
||||||
}
|
}
|
||||||
@@ -53,7 +51,7 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
|
|||||||
@objc func removeFolder(_ call: CAPPluginCall) {
|
@objc func removeFolder(_ call: CAPPluginCall) {
|
||||||
let folderId = call.getString("folderId")
|
let folderId = call.getString("folderId")
|
||||||
|
|
||||||
logger.log("removeFolder \(folderId ?? "UNSET")")
|
AbsLogger.info(message: "removeFolder \(folderId ?? "UNSET")")
|
||||||
|
|
||||||
call.unavailable("Not available on iOS")
|
call.unavailable("Not available on iOS")
|
||||||
}
|
}
|
||||||
@@ -61,7 +59,7 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
|
|||||||
@objc func removeLocalLibraryItem(_ call: CAPPluginCall) {
|
@objc func removeLocalLibraryItem(_ call: CAPPluginCall) {
|
||||||
let localLibraryItemId = call.getString("localLibraryItemId")
|
let localLibraryItemId = call.getString("localLibraryItemId")
|
||||||
|
|
||||||
logger.log("removeLocalLibraryItem \(localLibraryItemId ?? "UNSET")")
|
AbsLogger.info(message: "removeLocalLibraryItem \(localLibraryItemId ?? "UNSET")")
|
||||||
|
|
||||||
call.unavailable("Not available on iOS")
|
call.unavailable("Not available on iOS")
|
||||||
}
|
}
|
||||||
@@ -70,7 +68,7 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
|
|||||||
let localLibraryItemId = call.getString("localLibraryItemId")
|
let localLibraryItemId = call.getString("localLibraryItemId")
|
||||||
let forceAudioProbe = call.getBool("forceAudioProbe", false)
|
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")
|
call.unavailable("Not available on iOS")
|
||||||
}
|
}
|
||||||
@@ -79,7 +77,7 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
|
|||||||
let localLibraryItemId = call.getString("id")
|
let localLibraryItemId = call.getString("id")
|
||||||
let contentUrl = call.getString("contentUrl")
|
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
|
var success = false
|
||||||
do {
|
do {
|
||||||
@@ -89,7 +87,7 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
|
|||||||
success = true
|
success = true
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Failed to delete \(error)")
|
AbsLogger.error(message: "Failed to delete \(error)")
|
||||||
success = false
|
success = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +98,7 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
|
|||||||
let localLibraryItemId = call.getString("id")
|
let localLibraryItemId = call.getString("id")
|
||||||
let trackLocalFileId = call.getString("trackLocalFileId")
|
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
|
var success = false
|
||||||
if let localLibraryItemId = localLibraryItemId, let trackLocalFileId = trackLocalFileId, let item = Database.shared.getLocalLibraryItem(localLibraryItemId: localLibraryItemId) {
|
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
|
success = true
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Failed to delete \(error)")
|
AbsLogger.error(message: "Failed to delete \(error)")
|
||||||
success = false
|
success = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Failed to delete \(error)")
|
AbsLogger.error(message: "Failed to delete \(error)")
|
||||||
success = false
|
success = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,16 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Capacitor
|
import Capacitor
|
||||||
|
|
||||||
|
enum AbsLogLevel {
|
||||||
|
case info
|
||||||
|
case warn
|
||||||
|
case error
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileName(file: String = #file) -> String {
|
||||||
|
return (file as NSString).lastPathComponent
|
||||||
|
}
|
||||||
|
|
||||||
@objc(AbsLogger)
|
@objc(AbsLogger)
|
||||||
public class AbsLogger: CAPPlugin, CAPBridgedPlugin {
|
public class AbsLogger: CAPPlugin, CAPBridgedPlugin {
|
||||||
public var identifier = "AbsLoggerPlugin"
|
public var identifier = "AbsLoggerPlugin"
|
||||||
@@ -19,29 +29,96 @@ public class AbsLogger: CAPPlugin, CAPBridgedPlugin {
|
|||||||
CAPPluginMethod(name: "clearLogs", returnType: CAPPluginReturnPromise)
|
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: [
|
||||||
|
"id": entry.id,
|
||||||
|
"tag": entry.tag,
|
||||||
|
"message": entry.message,
|
||||||
|
"level": entry.level,
|
||||||
|
"timestamp": entry.timestamp
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
@objc func info(_ call: CAPPluginCall) {
|
||||||
let message = call.getString("message") ?? ""
|
let message = call.getString("message") ?? ""
|
||||||
let tag = call.getString("tag") ?? ""
|
let tag = call.getString("tag") ?? ""
|
||||||
|
|
||||||
logger.log("[\(tag)] \(message)")
|
do {
|
||||||
call.resolve()
|
try info(tag: tag, message: message)
|
||||||
|
call.resolve()
|
||||||
|
} catch {
|
||||||
|
call.reject("Failed to log \(message)", "101", error)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func error(_ call: CAPPluginCall) {
|
@objc func error(_ call: CAPPluginCall) {
|
||||||
let message = call.getString("message") ?? ""
|
let message = call.getString("message") ?? ""
|
||||||
let tag = call.getString("tag") ?? ""
|
let tag = call.getString("tag") ?? ""
|
||||||
|
|
||||||
logger.error("[\(tag)] \(message)")
|
do {
|
||||||
call.resolve()
|
try error(tag: tag, message: message)
|
||||||
|
call.resolve()
|
||||||
|
} catch {
|
||||||
|
call.reject("Failed to log \(message)", "101", error)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func getAllLogs(_ call: CAPPluginCall) {
|
@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) {
|
@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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
class AudioPlayer: NSObject {
|
||||||
internal let queue = DispatchQueue(label: "ABSAudioPlayerQueue")
|
internal let queue = DispatchQueue(label: "ABSAudioPlayerQueue")
|
||||||
internal let logger = AppLogger(category: "AudioPlayer")
|
|
||||||
|
|
||||||
private var status: PlayerStatus
|
private var status: PlayerStatus
|
||||||
internal var rateManager: AudioPlayerRateManager
|
internal var rateManager: AudioPlayerRateManager
|
||||||
@@ -81,7 +80,7 @@ class AudioPlayer: NSObject {
|
|||||||
|
|
||||||
let playbackSession = self.getPlaybackSession()
|
let playbackSession = self.getPlaybackSession()
|
||||||
guard let playbackSession = playbackSession else {
|
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)
|
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -115,10 +114,10 @@ class AudioPlayer: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.currentTrackIndex = getItemIndexForTime(time: playbackSession.currentTime)
|
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]
|
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) {
|
for item in Array(playerItems) {
|
||||||
self.audioPlayer.insert(item, after:self.audioPlayer.items().last)
|
self.audioPlayer.insert(item, after:self.audioPlayer.items().last)
|
||||||
@@ -128,7 +127,7 @@ class AudioPlayer: NSObject {
|
|||||||
setupQueueObserver()
|
setupQueueObserver()
|
||||||
setupQueueItemStatusObserver()
|
setupQueueItemStatusObserver()
|
||||||
|
|
||||||
logger.log("Audioplayer ready")
|
AbsLogger.info(message:"Audioplayer ready")
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
@@ -146,8 +145,7 @@ class AudioPlayer: NSObject {
|
|||||||
do {
|
do {
|
||||||
try AVAudioSession.sharedInstance().setActive(false)
|
try AVAudioSession.sharedInstance().setActive(false)
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Failed to set AVAudioSession inactive")
|
AbsLogger.error(message: "Failed to set AVAudioSession inactive", error: error)
|
||||||
logger.error(error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.removeAudioSessionNotifications()
|
self.removeAudioSessionNotifications()
|
||||||
@@ -253,14 +251,14 @@ class AudioPlayer: NSObject {
|
|||||||
self.audioPlayer.currentItem.map { item in
|
self.audioPlayer.currentItem.map { item in
|
||||||
self.currentTrackIndex = self.allPlayerItems.firstIndex(of:item) ?? 0
|
self.currentTrackIndex = self.allPlayerItems.firstIndex(of:item) ?? 0
|
||||||
if (self.currentTrackIndex != prevTrackIndex) {
|
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() {
|
private func setupQueueItemStatusObserver() {
|
||||||
logger.log("queueStatusObserver: Setting up")
|
AbsLogger.info(message:"queueStatusObserver: Setting up")
|
||||||
|
|
||||||
// Listen for player item updates
|
// Listen for player item updates
|
||||||
self.queueItemStatusObserver?.invalidate()
|
self.queueItemStatusObserver?.invalidate()
|
||||||
@@ -275,13 +273,13 @@ class AudioPlayer: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func handleQueueItemStatus(playerItem: AVPlayerItem) {
|
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 {
|
guard let playbackSession = self.getPlaybackSession() else {
|
||||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
|
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (playerItem.status == .readyToPlay) {
|
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
|
// Seek the player before initializing, so a currentTime of 0 does not appear in MediaProgress / session
|
||||||
let firstReady = self.status == .uninitialized
|
let firstReady = self.status == .uninitialized
|
||||||
@@ -300,7 +298,7 @@ class AudioPlayer: NSObject {
|
|||||||
self.status = .paused
|
self.status = .paused
|
||||||
}
|
}
|
||||||
} else if (playerItem.status == .failed) {
|
} 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)
|
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -332,7 +330,7 @@ class AudioPlayer: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func resumePlayback() {
|
private func resumePlayback() {
|
||||||
logger.log("PLAY: Resuming playback")
|
AbsLogger.info(message:"PLAY: Resuming playback")
|
||||||
|
|
||||||
self.markAudioSessionAs(active: true)
|
self.markAudioSessionAs(active: true)
|
||||||
DispatchQueue.runOnMainQueue {
|
DispatchQueue.runOnMainQueue {
|
||||||
@@ -351,7 +349,7 @@ class AudioPlayer: NSObject {
|
|||||||
public func pause() {
|
public func pause() {
|
||||||
guard self.isInitialized() else { return }
|
guard self.isInitialized() else { return }
|
||||||
|
|
||||||
logger.log("PAUSE: Pausing playback")
|
AbsLogger.info(message:"PAUSE: Pausing playback")
|
||||||
DispatchQueue.runOnMainQueue {
|
DispatchQueue.runOnMainQueue {
|
||||||
self.audioPlayer.pause()
|
self.audioPlayer.pause()
|
||||||
}
|
}
|
||||||
@@ -371,7 +369,7 @@ class AudioPlayer: NSObject {
|
|||||||
public func startFadeOut() {
|
public func startFadeOut() {
|
||||||
guard self.isInitialized() else { return }
|
guard self.isInitialized() else { return }
|
||||||
guard let currentTime = self.getCurrentTime() 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.
|
// Define fade parameters.
|
||||||
let fadeDuration: Float = 60.0 // total fade duration in seconds
|
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.
|
// Ensure volume is exactly zero and end fade.
|
||||||
self.audioPlayer.volume = targetVolume
|
self.audioPlayer.volume = targetVolume
|
||||||
t.invalidate()
|
t.invalidate()
|
||||||
self.logger.log("Fadeout: Fade complete, pausing playback")
|
AbsLogger.info(message:"Fadeout: Fade complete, pausing playback")
|
||||||
self.pause()
|
self.pause()
|
||||||
self.audioPlayer.volume = initialVolume
|
self.audioPlayer.volume = initialVolume
|
||||||
self.seek(currentTime, from: "fadeOut")
|
self.seek(currentTime, from: "fadeOut")
|
||||||
@@ -419,12 +417,12 @@ class AudioPlayer: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func seek(_ to: Double, from: String) {
|
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 }
|
guard let playbackSession = self.getPlaybackSession() else { return }
|
||||||
|
|
||||||
let indexOfSeek = getItemIndexForTime(time: to)
|
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 {
|
if self.audioPlayer.currentItem == nil {
|
||||||
self.currentTrackIndex = indexOfSeek
|
self.currentTrackIndex = indexOfSeek
|
||||||
@@ -488,16 +486,16 @@ class AudioPlayer: NSObject {
|
|||||||
let currentTrack = playbackSession.audioTracks[self.currentTrackIndex]
|
let currentTrack = playbackSession.audioTracks[self.currentTrackIndex]
|
||||||
let ctso = currentTrack.startOffset ?? 0.0
|
let ctso = currentTrack.startOffset ?? 0.0
|
||||||
let trackEnd = ctso + currentTrack.duration
|
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 boundedTime = min(max(to, ctso), trackEnd)
|
||||||
let seekTime = boundedTime - ctso
|
let seekTime = boundedTime - ctso
|
||||||
|
|
||||||
DispatchQueue.runOnMainQueue {
|
DispatchQueue.runOnMainQueue {
|
||||||
self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000)) { [weak self] completed in
|
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 {
|
guard completed else {
|
||||||
self?.logger.log("SEEK: WARNING: seeking not completed (to \(seekTime)")
|
AbsLogger.info(message:"SEEK: WARNING: seeking not completed (to \(seekTime)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
@@ -574,7 +572,7 @@ class AudioPlayer: NSObject {
|
|||||||
} else if (playbackSession.playMethod == PlayMethod.local.rawValue) {
|
} else if (playbackSession.playMethod == PlayMethod.local.rawValue) {
|
||||||
guard let localFile = track.getLocalFile() else {
|
guard let localFile = track.getLocalFile() else {
|
||||||
// Worst case we can stream the file
|
// 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 urlstr = "\(Store.serverConfig!.address)/api/items/\(itemId)/file/\(ino)?token=\(Store.serverConfig!.token)"
|
||||||
let url = URL(string: urlstr)!
|
let url = URL(string: urlstr)!
|
||||||
return AVURLAsset(url: url)
|
return AVURLAsset(url: url)
|
||||||
@@ -594,8 +592,7 @@ class AudioPlayer: NSObject {
|
|||||||
do {
|
do {
|
||||||
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio)
|
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio)
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Failed to set AVAudioSession category")
|
AbsLogger.error(message: "Failed to set AVAudioSession category", error: error)
|
||||||
logger.error(error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,7 +600,7 @@ class AudioPlayer: NSObject {
|
|||||||
do {
|
do {
|
||||||
try AVAudioSession.sharedInstance().setActive(active)
|
try AVAudioSession.sharedInstance().setActive(active)
|
||||||
} catch {
|
} 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 reasonValue = userInfo[AVAudioSessionInterruptionReasonKey] as? UInt ?? 0
|
||||||
let reason = AVAudioSession.InterruptionReason(rawValue: reasonValue)
|
let reason = AVAudioSession.InterruptionReason(rawValue: reasonValue)
|
||||||
if (reason == .appWasSuspended) {
|
if (reason == .appWasSuspended) {
|
||||||
logger.log("AVAudioSession was suspended")
|
AbsLogger.info(message:"AVAudioSession was suspended")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -812,14 +809,14 @@ class AudioPlayer: NSObject {
|
|||||||
if context == &playerContext {
|
if context == &playerContext {
|
||||||
if keyPath == #keyPath(AVPlayer.currentItem) {
|
if keyPath == #keyPath(AVPlayer.currentItem) {
|
||||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil)
|
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 audioPlayer.currentItem == nil {
|
||||||
// if the queue is rebuilding, we expect the current item may be nil
|
// if the queue is rebuilding, we expect the current item may be nil
|
||||||
if self.isRebuildingQueue {
|
if self.isRebuildingQueue {
|
||||||
return
|
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)
|
self.markAudioSessionAs(active: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ extension AudioPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func setSleepTimer(secondsUntilSleep: Double) {
|
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.removeSleepTimer()
|
||||||
self.sleepTimeRemaining = secondsUntilSleep
|
self.sleepTimeRemaining = secondsUntilSleep
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ extension AudioPlayer {
|
|||||||
guard let currentTime = self.getCurrentTime() else { return }
|
guard let currentTime = self.getCurrentTime() else { return }
|
||||||
guard stopAt >= currentTime 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
|
// Schedule the observation time
|
||||||
self.sleepTimeChapterStopAt = stopAt
|
self.sleepTimeChapterStopAt = stopAt
|
||||||
@@ -138,7 +138,7 @@ extension AudioPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func handleSleepEnd() {
|
private func handleSleepEnd() {
|
||||||
logger.log("SLEEP TIMER: Pausing audio")
|
AbsLogger.info(message: "SLEEP TIMER: Pausing audio")
|
||||||
self.pause()
|
self.pause()
|
||||||
self.removeSleepTimer()
|
self.removeSleepTimer()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import AVFoundation
|
|||||||
|
|
||||||
@available(iOS 16.0, *)
|
@available(iOS 16.0, *)
|
||||||
class DefaultedAudioPlayerRateManager: NSObject, AudioPlayerRateManager {
|
class DefaultedAudioPlayerRateManager: NSObject, AudioPlayerRateManager {
|
||||||
internal let logger = AppLogger(category: "DefaultedAudioPlayerRateManager")
|
|
||||||
|
|
||||||
internal var audioPlayer: AVPlayer
|
internal var audioPlayer: AVPlayer
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import Foundation
|
|||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
|
||||||
class LegacyAudioPlayerRateManager: NSObject, AudioPlayerRateManager {
|
class LegacyAudioPlayerRateManager: NSObject, AudioPlayerRateManager {
|
||||||
internal let logger = AppLogger(category: "AudioPlayer")
|
|
||||||
|
|
||||||
internal var audioPlayer: AVPlayer
|
internal var audioPlayer: AVPlayer
|
||||||
|
|
||||||
@@ -54,7 +53,7 @@ class LegacyAudioPlayerRateManager: NSObject, AudioPlayerRateManager {
|
|||||||
let playbackSpeedChanged = rate > 0.0 && rate != self.defaultRate && !(observed && rate == 1)
|
let playbackSpeedChanged = rate > 0.0 && rate != self.defaultRate && !(observed && rate == 1)
|
||||||
|
|
||||||
if self.audioPlayer.rate != rate {
|
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 {
|
DispatchQueue.runOnMainQueue {
|
||||||
self.audioPlayer.rate = rate
|
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?) {
|
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||||
if context == &managerContext {
|
if context == &managerContext {
|
||||||
if keyPath == #keyPath(AVPlayer.rate) {
|
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)
|
self.handlePlaybackRateChange(change?[.newKey] as? Float ?? 1.0, observed: true)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ class PlayerProgress {
|
|||||||
|
|
||||||
private static var TIME_BETWEEN_SESSION_SYNC_IN_SECONDS = 15.0
|
private static var TIME_BETWEEN_SESSION_SYNC_IN_SECONDS = 15.0
|
||||||
|
|
||||||
private let logger = AppLogger(category: "PlayerProgress")
|
|
||||||
|
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
|
|
||||||
@@ -30,8 +28,7 @@ class PlayerProgress {
|
|||||||
try await updateServerSessionFromLocalSession(session, rateLimitSync: !isStopping)
|
try await updateServerSessionFromLocalSession(session, rateLimitSync: !isStopping)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Failed to syncFromPlayer")
|
AbsLogger.error(message: "Failed to syncFromPlayer \(error)", error: error)
|
||||||
logger.error(error)
|
|
||||||
}
|
}
|
||||||
await UIApplication.shared.endBackgroundTask(backgroundToken)
|
await UIApplication.shared.endBackgroundTask(backgroundToken)
|
||||||
}
|
}
|
||||||
@@ -76,7 +73,7 @@ class PlayerProgress {
|
|||||||
|
|
||||||
try localMediaProgress.updateFromPlaybackSession(session)
|
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
|
// Send the local progress back to front-end
|
||||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.localProgress.rawValue), object: nil)
|
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.localProgress.rawValue), object: nil)
|
||||||
@@ -126,7 +123,7 @@ class PlayerProgress {
|
|||||||
session = session.freeze()
|
session = session.freeze()
|
||||||
|
|
||||||
guard safeToSync else { return }
|
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
|
var success = false
|
||||||
if session.isLocal {
|
if session.isLocal {
|
||||||
@@ -155,25 +152,25 @@ class PlayerProgress {
|
|||||||
|
|
||||||
// TODO: Unused for now
|
// TODO: Unused for now
|
||||||
private func updateLocalSessionFromServerMediaProgress() async throws {
|
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: {
|
guard let session = try Realm(queue: nil).objects(PlaybackSession.self).last(where: {
|
||||||
$0.isActiveSession == true && $0.serverConnectionConfigId == Store.serverConfig?.id
|
$0.isActiveSession == true && $0.serverConnectionConfigId == Store.serverConfig?.id
|
||||||
})?.freeze() else {
|
})?.freeze() else {
|
||||||
logger.log("updateLocalSessionFromServerMediaProgress: Failed to get session")
|
AbsLogger.info(message:"updateLocalSessionFromServerMediaProgress: Failed to get session")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the current progress
|
// Fetch the current progress
|
||||||
let progress = await ApiClient.getMediaProgress(libraryItemId: session.libraryItemId!, episodeId: session.episodeId)
|
let progress = await ApiClient.getMediaProgress(libraryItemId: session.libraryItemId!, episodeId: session.episodeId)
|
||||||
guard let progress = progress else {
|
guard let progress = progress else {
|
||||||
logger.log("updateLocalSessionFromServerMediaProgress: No progress object")
|
AbsLogger.info(message:"updateLocalSessionFromServerMediaProgress: No progress object")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine which session is newer
|
// Determine which session is newer
|
||||||
let serverLastUpdate = progress.lastUpdate
|
let serverLastUpdate = progress.lastUpdate
|
||||||
guard let localLastUpdate = session.updatedAt else {
|
guard let localLastUpdate = session.updatedAt else {
|
||||||
logger.log("updateLocalSessionFromServerMediaProgress: No local session updatedAt")
|
AbsLogger.info(message:"updateLocalSessionFromServerMediaProgress: No local session updatedAt")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let serverCurrentTime = progress.currentTime
|
let serverCurrentTime = progress.currentTime
|
||||||
@@ -184,16 +181,16 @@ class PlayerProgress {
|
|||||||
|
|
||||||
// Update the session, if needed
|
// Update the session, if needed
|
||||||
if serverIsNewerThanLocal && currentTimeIsDifferent {
|
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 }
|
guard let session = session.thaw() else { return }
|
||||||
try session.update {
|
try session.update {
|
||||||
session.currentTime = serverCurrentTime
|
session.currentTime = serverCurrentTime
|
||||||
session.updatedAt = serverLastUpdate
|
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)
|
PlayerHandler.seek(amount: session.currentTime)
|
||||||
} else {
|
} 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
|
import Alamofire
|
||||||
|
|
||||||
class ApiClient {
|
class ApiClient {
|
||||||
private static let logger = AppLogger(category: "ApiClient")
|
|
||||||
private static let secureStorage = SecureStorage()
|
private static let secureStorage = SecureStorage()
|
||||||
|
|
||||||
public static func getData(from url: URL, completion: @escaping (UIImage?) -> Void) {
|
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)?) {
|
public static func postResource<T: Decodable>(endpoint: String, parameters: [String: Any], decodable: T.Type = T.self, callback: ((_ param: T) -> Void)?) {
|
||||||
if (Store.serverConfig == nil) {
|
if (Store.serverConfig == nil) {
|
||||||
logger.error("Server config not set")
|
AbsLogger.error(message: "Server config not set")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +34,7 @@ class ApiClient {
|
|||||||
case .success(let obj):
|
case .success(let obj):
|
||||||
callback?(obj)
|
callback?(obj)
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
logger.error("api request to \(endpoint) failed")
|
AbsLogger.error(message: "api request to \(endpoint) failed")
|
||||||
print(error)
|
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)?) {
|
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) {
|
if (Store.serverConfig == nil) {
|
||||||
logger.error("Server config not set")
|
AbsLogger.error(message: "Server config not set")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +55,7 @@ class ApiClient {
|
|||||||
case .success(let obj):
|
case .success(let obj):
|
||||||
callback?(obj)
|
callback?(obj)
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
logger.error("api request to \(endpoint) failed")
|
AbsLogger.error(message: "api request to \(endpoint) failed")
|
||||||
print(error)
|
print(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,7 +71,7 @@ class ApiClient {
|
|||||||
|
|
||||||
public static func postResource<T:Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) {
|
public static func postResource<T:Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) {
|
||||||
if (Store.serverConfig == nil) {
|
if (Store.serverConfig == nil) {
|
||||||
logger.error("Server config not set")
|
AbsLogger.error(message: "Server config not set")
|
||||||
callback?(false)
|
callback?(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -86,7 +85,7 @@ class ApiClient {
|
|||||||
case .success(_):
|
case .success(_):
|
||||||
callback?(true)
|
callback?(true)
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
logger.error("api request to \(endpoint) failed")
|
AbsLogger.error(message: "api request to \(endpoint) failed")
|
||||||
print(error)
|
print(error)
|
||||||
|
|
||||||
callback?(false)
|
callback?(false)
|
||||||
@@ -96,7 +95,7 @@ class ApiClient {
|
|||||||
|
|
||||||
public static func patchResource<T: Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) {
|
public static func patchResource<T: Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) {
|
||||||
if (Store.serverConfig == nil) {
|
if (Store.serverConfig == nil) {
|
||||||
logger.error("Server config not set")
|
AbsLogger.error(message: "Server config not set")
|
||||||
callback?(false)
|
callback?(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -110,7 +109,7 @@ class ApiClient {
|
|||||||
case .success(_):
|
case .success(_):
|
||||||
callback?(true)
|
callback?(true)
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
logger.error("api request to \(endpoint) failed")
|
AbsLogger.error(message: "api request to \(endpoint) failed")
|
||||||
print(error)
|
print(error)
|
||||||
callback?(false)
|
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)?) {
|
public static func getResource<T: Decodable>(endpoint: String, decodable: T.Type = T.self, callback: ((_ param: T?) -> Void)?) {
|
||||||
if (Store.serverConfig == nil) {
|
if (Store.serverConfig == nil) {
|
||||||
logger.error("Server config not set")
|
AbsLogger.error(message: "Server config not set")
|
||||||
callback?(nil)
|
callback?(nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -141,7 +140,7 @@ class ApiClient {
|
|||||||
case .success(let obj):
|
case .success(let obj):
|
||||||
callback?(obj)
|
callback?(obj)
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
logger.error("api request to \(endpoint) failed")
|
AbsLogger.error(message: "api request to \(endpoint) failed")
|
||||||
print(error)
|
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)?) {
|
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 {
|
guard let serverConfig = Store.serverConfig else {
|
||||||
logger.error("handleTokenRefresh: No server config available")
|
AbsLogger.error(message: "handleTokenRefresh: No server config available")
|
||||||
callback?(nil)
|
callback?(nil)
|
||||||
return
|
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
|
// Get refresh token from secure storage
|
||||||
guard let refreshToken = secureStorage.getRefreshToken(serverConnectionConfigId: serverConfig.id) else {
|
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()
|
handleRefreshFailure()
|
||||||
callback?(nil)
|
callback?(nil)
|
||||||
return
|
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
|
// Create refresh token request
|
||||||
let refreshHeaders: HTTPHeaders = [
|
let refreshHeaders: HTTPHeaders = [
|
||||||
@@ -190,23 +189,23 @@ class ApiClient {
|
|||||||
case .success(let refreshResponse):
|
case .success(let refreshResponse):
|
||||||
guard let user = refreshResponse.user,
|
guard let user = refreshResponse.user,
|
||||||
!user.accessToken.isEmpty else {
|
!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()
|
handleRefreshFailure()
|
||||||
callback?(nil)
|
callback?(nil)
|
||||||
return
|
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
|
// Update tokens in secure storage and store
|
||||||
updateTokens(newAccessToken: user.accessToken, newRefreshToken: user.refreshToken ?? refreshToken, serverConnectionConfigId: serverConfig.id)
|
updateTokens(newAccessToken: user.accessToken, newRefreshToken: user.refreshToken ?? refreshToken, serverConnectionConfigId: serverConfig.id)
|
||||||
|
|
||||||
// Retry the original request with the new access token
|
// 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)
|
retryOriginalRequest(endpoint: endpoint, method: method, parameters: parameters, decodable: decodable, newAccessToken: user.accessToken, callback: callback)
|
||||||
|
|
||||||
case .failure(let error):
|
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()
|
handleRefreshFailure()
|
||||||
callback?(nil)
|
callback?(nil)
|
||||||
}
|
}
|
||||||
@@ -220,14 +219,14 @@ class ApiClient {
|
|||||||
// Update the refresh token in secure storage if it's new
|
// Update the refresh token in secure storage if it's new
|
||||||
if newRefreshToken != secureStorage.getRefreshToken(serverConnectionConfigId: serverConnectionConfigId) {
|
if newRefreshToken != secureStorage.getRefreshToken(serverConnectionConfigId: serverConnectionConfigId) {
|
||||||
let hasStored = secureStorage.storeRefreshToken(serverConnectionConfigId: serverConnectionConfigId, refreshToken: newRefreshToken)
|
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
|
// Update access token on server connection config
|
||||||
Database.shared.updateServerConnectionConfigToken(newToken: newAccessToken)
|
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
|
// Notify webview frontend about token refresh
|
||||||
if let callback = AbsDatabase.tokenRefreshCallback {
|
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)?) {
|
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 {
|
guard let serverConfig = Store.serverConfig else {
|
||||||
logger.error("retryOriginalRequest: No server config available")
|
AbsLogger.error(message: "retryOriginalRequest: No server config available")
|
||||||
callback?(nil)
|
callback?(nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -270,7 +269,7 @@ class ApiClient {
|
|||||||
retryRequest = AF.request("\(serverConfig.address)/\(endpoint)", method: .patch, headers: headers)
|
retryRequest = AF.request("\(serverConfig.address)/\(endpoint)", method: .patch, headers: headers)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
logger.error("retryOriginalRequest: Unsupported method \(method)")
|
AbsLogger.error(message: "retryOriginalRequest: Unsupported method \(method)")
|
||||||
callback?(nil)
|
callback?(nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -282,7 +281,7 @@ class ApiClient {
|
|||||||
if let data = response.data, !data.isEmpty {
|
if let data = response.data, !data.isEmpty {
|
||||||
// If it is a string return nil (e.g. express returns OK for 200 status codes)
|
// If it is a string return nil (e.g. express returns OK for 200 status codes)
|
||||||
if let responseString = String(data: data, encoding: .utf8) {
|
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)
|
callback?(nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -292,16 +291,16 @@ class ApiClient {
|
|||||||
let decodedObject = try JSONDecoder().decode(decodable, from: data)
|
let decodedObject = try JSONDecoder().decode(decodable, from: data)
|
||||||
callback?(decodedObject)
|
callback?(decodedObject)
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("retryOriginalRequest: JSON decode failed: \(error)")
|
AbsLogger.error(message: "retryOriginalRequest: JSON decode failed: \(error)", error: error)
|
||||||
callback?(nil)
|
callback?(nil)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Empty response
|
// Empty response
|
||||||
logger.log("retryOriginalRequest: Empty response with success status \(statusCode)")
|
AbsLogger.info(message: "retryOriginalRequest: Empty response with success status \(statusCode)")
|
||||||
callback?(nil)
|
callback?(nil)
|
||||||
}
|
}
|
||||||
} else {
|
} 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)
|
callback?(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -312,7 +311,7 @@ class ApiClient {
|
|||||||
* This will clear the current server connection and notify webview
|
* This will clear the current server connection and notify webview
|
||||||
*/
|
*/
|
||||||
private static func handleRefreshFailure() {
|
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
|
// Clear the current server connection
|
||||||
Store.serverConfig = nil
|
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)?) {
|
public static func getResourceWithTokenRefresh<T: Decodable>(endpoint: String, decodable: T.Type = T.self, callback: ((_ param: T?) -> Void)?) {
|
||||||
if (Store.serverConfig == nil) {
|
if (Store.serverConfig == nil) {
|
||||||
logger.error("Server config not set")
|
AbsLogger.error(message: "Server config not set")
|
||||||
callback?(nil)
|
callback?(nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -345,14 +344,14 @@ class ApiClient {
|
|||||||
|
|
||||||
request.responseDecodable(of: decodable) { response in
|
request.responseDecodable(of: decodable) { response in
|
||||||
if let statusCode = response.response?.statusCode, statusCode == 401 {
|
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)
|
handleTokenRefresh(originalRequest: request, endpoint: endpoint, method: .get, parameters: nil, decodable: decodable, callback: callback)
|
||||||
} else {
|
} else {
|
||||||
switch response.result {
|
switch response.result {
|
||||||
case .success(let obj):
|
case .success(let obj):
|
||||||
callback?(obj)
|
callback?(obj)
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
logger.error("api request to \(endpoint) failed")
|
AbsLogger.error(message: "api request to \(endpoint) failed")
|
||||||
print(error)
|
print(error)
|
||||||
callback?(nil)
|
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)?) {
|
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) {
|
if (Store.serverConfig == nil) {
|
||||||
logger.error("Server config not set")
|
AbsLogger.error(message: "Server config not set")
|
||||||
callback?(nil)
|
callback?(nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -375,14 +374,14 @@ class ApiClient {
|
|||||||
|
|
||||||
request.responseDecodable(of: decodable) { response in
|
request.responseDecodable(of: decodable) { response in
|
||||||
if let statusCode = response.response?.statusCode, statusCode == 401 {
|
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)
|
handleTokenRefresh(originalRequest: request, endpoint: endpoint, method: .post, parameters: parameters, decodable: decodable, callback: callback)
|
||||||
} else {
|
} else {
|
||||||
switch response.result {
|
switch response.result {
|
||||||
case .success(let obj):
|
case .success(let obj):
|
||||||
callback?(obj)
|
callback?(obj)
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
logger.error("api request to \(endpoint) failed")
|
AbsLogger.error(message: "api request to \(endpoint) failed")
|
||||||
print(error)
|
print(error)
|
||||||
callback?(nil)
|
callback?(nil)
|
||||||
}
|
}
|
||||||
@@ -395,7 +394,7 @@ class ApiClient {
|
|||||||
*/
|
*/
|
||||||
public static func postResourceWithTokenRefresh<T: Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) {
|
public static func postResourceWithTokenRefresh<T: Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) {
|
||||||
if (Store.serverConfig == nil) {
|
if (Store.serverConfig == nil) {
|
||||||
logger.error("Server config not set")
|
AbsLogger.error(message: "Server config not set")
|
||||||
callback?(false)
|
callback?(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -408,7 +407,7 @@ class ApiClient {
|
|||||||
|
|
||||||
request.response { response in
|
request.response { response in
|
||||||
if let statusCode = response.response?.statusCode, statusCode == 401 {
|
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
|
handleTokenRefresh(originalRequest: request, endpoint: endpoint, method: .post, parameters: parameters, decodable: EmptyResponse.self) { result in
|
||||||
callback?(result != nil)
|
callback?(result != nil)
|
||||||
}
|
}
|
||||||
@@ -417,7 +416,7 @@ class ApiClient {
|
|||||||
case .success(_):
|
case .success(_):
|
||||||
callback?(true)
|
callback?(true)
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
logger.error("api request to \(endpoint) failed")
|
AbsLogger.error(message: "api request to \(endpoint) failed")
|
||||||
print(error)
|
print(error)
|
||||||
callback?(false)
|
callback?(false)
|
||||||
}
|
}
|
||||||
@@ -427,7 +426,7 @@ class ApiClient {
|
|||||||
|
|
||||||
public static func patchResourceWithTokenRefresh<T: Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) {
|
public static func patchResourceWithTokenRefresh<T: Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) {
|
||||||
if (Store.serverConfig == nil) {
|
if (Store.serverConfig == nil) {
|
||||||
logger.error("Server config not set")
|
AbsLogger.error(message: "Server config not set")
|
||||||
callback?(false)
|
callback?(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -440,7 +439,7 @@ class ApiClient {
|
|||||||
|
|
||||||
request.response { response in
|
request.response { response in
|
||||||
if let statusCode = response.response?.statusCode, statusCode == 401 {
|
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
|
handleTokenRefresh(originalRequest: request, endpoint: endpoint, method: .patch, parameters: parameters, decodable: EmptyResponse.self) { result in
|
||||||
callback?(result != nil)
|
callback?(result != nil)
|
||||||
}
|
}
|
||||||
@@ -449,7 +448,7 @@ class ApiClient {
|
|||||||
case .success(_):
|
case .success(_):
|
||||||
callback?(true)
|
callback?(true)
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
logger.error("api request to \(endpoint) failed")
|
AbsLogger.error(message: "api request to \(endpoint) failed")
|
||||||
print(error)
|
print(error)
|
||||||
callback?(false)
|
callback?(false)
|
||||||
}
|
}
|
||||||
@@ -489,7 +488,7 @@ class ApiClient {
|
|||||||
// Use the new token refresh-enabled method
|
// Use the new token refresh-enabled method
|
||||||
postResourceWithTokenRefresh(endpoint: endpoint, parameters: parameters, decodable: PlaybackSession.self) { session in
|
postResourceWithTokenRefresh(endpoint: endpoint, parameters: parameters, decodable: PlaybackSession.self) { session in
|
||||||
guard let session = session else {
|
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
|
callback(PlaybackSession()) // Return empty session on failure
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -533,14 +532,14 @@ class ApiClient {
|
|||||||
let localMediaProgressList = Database.shared.getAllLocalMediaProgress().filter {
|
let localMediaProgressList = Database.shared.getAllLocalMediaProgress().filter {
|
||||||
$0.serverConnectionConfigId == Store.serverConfig?.id
|
$0.serverConnectionConfigId == Store.serverConfig?.id
|
||||||
}.map { $0.freeze() }
|
}.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) {
|
if (localMediaProgressList.isEmpty) {
|
||||||
logger.log("syncLocalSessionsWithServer: No local progress to sync")
|
AbsLogger.info(message: "syncLocalSessionsWithServer: No local progress to sync")
|
||||||
} else {
|
} else {
|
||||||
let currentUser = await ApiClient.getCurrentUser()
|
let currentUser = await ApiClient.getCurrentUser()
|
||||||
guard let currentUser = currentUser else {
|
guard let currentUser = currentUser else {
|
||||||
logger.log("syncLocalSessionsWithServer: No User")
|
AbsLogger.info(message: "syncLocalSessionsWithServer: No User")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try currentUser.mediaProgress.forEach { mediaProgress in
|
try currentUser.mediaProgress.forEach { mediaProgress in
|
||||||
@@ -552,12 +551,12 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (localMediaProgress != nil && mediaProgress.lastUpdate > localMediaProgress!.lastUpdate) {
|
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() {
|
if let localMediaProgress = localMediaProgress?.thaw() {
|
||||||
try localMediaProgress.updateFromServerMediaProgress(mediaProgress)
|
try localMediaProgress.updateFromServerMediaProgress(mediaProgress)
|
||||||
}
|
}
|
||||||
} else if (localMediaProgress != nil) {
|
} 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 {
|
let playbackSessions = Database.shared.getAllPlaybackSessions().filter {
|
||||||
$0.serverConnectionConfigId == Store.serverConfig?.id
|
$0.serverConnectionConfigId == Store.serverConfig?.id
|
||||||
}.map { $0.freeze() }
|
}.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) {
|
if (!playbackSessions.isEmpty) {
|
||||||
let success = await ApiClient.reportAllLocalPlaybackSessions(playbackSessions)
|
let success = await ApiClient.reportAllLocalPlaybackSessions(playbackSessions)
|
||||||
if (success) {
|
if (success) {
|
||||||
// Remove sessions from db
|
// Remove sessions from db
|
||||||
try playbackSessions.forEach { session in
|
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
|
// On first sync then remove all sessions
|
||||||
if (!session.isActiveSession || isFirstSync) {
|
if (!session.isActiveSession || isFirstSync) {
|
||||||
if let session = session.thaw() {
|
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) {
|
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 ?? "")"
|
let endpoint = episodeId?.isEmpty ?? true ? "api/me/progress/\(libraryItemId)" : "api/me/progress/\(libraryItemId)/\(episodeId ?? "")"
|
||||||
patchResourceWithTokenRefresh(endpoint: endpoint, parameters: payload) { _ in
|
patchResourceWithTokenRefresh(endpoint: endpoint, parameters: payload) { _ in
|
||||||
callback()
|
callback()
|
||||||
@@ -597,7 +596,7 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static func getMediaProgress(libraryItemId: String, episodeId: String?) async -> MediaProgress? {
|
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 ?? "")"
|
let endpoint = episodeId?.isEmpty ?? true ? "api/me/progress/\(libraryItemId)" : "api/me/progress/\(libraryItemId)/\(episodeId ?? "")"
|
||||||
return await withCheckedContinuation { continuation in
|
return await withCheckedContinuation { continuation in
|
||||||
getResourceWithTokenRefresh(endpoint: endpoint, decodable: MediaProgress.self) { result in
|
getResourceWithTokenRefresh(endpoint: endpoint, decodable: MediaProgress.self) { result in
|
||||||
@@ -607,7 +606,7 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static func getCurrentUser() async -> User? {
|
public static func getCurrentUser() async -> User? {
|
||||||
logger.log("getCurrentUser")
|
AbsLogger.info(message: "getCurrentUser")
|
||||||
return await withCheckedContinuation { continuation in
|
return await withCheckedContinuation { continuation in
|
||||||
getResourceWithTokenRefresh(endpoint: "api/me", decodable: User.self) { result in
|
getResourceWithTokenRefresh(endpoint: "api/me", decodable: User.self) { result in
|
||||||
continuation.resume(returning: result)
|
continuation.resume(returning: result)
|
||||||
|
|||||||
@@ -13,9 +13,13 @@ class Database {
|
|||||||
return Database()
|
return Database()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private let logger = AppLogger(category: "Database")
|
private init() {
|
||||||
|
do {
|
||||||
private init() {}
|
try cleanExpiredLogs()
|
||||||
|
} catch {
|
||||||
|
debugPrint(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func setServerConnectionConfig(config: ServerConnectionConfig) {
|
public func setServerConnectionConfig(config: ServerConnectionConfig) {
|
||||||
let config = config
|
let config = config
|
||||||
@@ -33,7 +37,7 @@ class Database {
|
|||||||
existing.token = config.token
|
existing.token = config.token
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("failed to update server config")
|
AbsLogger.error("setServerConn", message: "failed to update server config")
|
||||||
debugPrint(error)
|
debugPrint(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +58,7 @@ class Database {
|
|||||||
realm.add(config)
|
realm.add(config)
|
||||||
}
|
}
|
||||||
} catch(let exception) {
|
} catch(let exception) {
|
||||||
logger.error("failed to save server config")
|
AbsLogger.error(message: "failed to save server config")
|
||||||
debugPrint(exception)
|
debugPrint(exception)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +90,7 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch(let exception) {
|
} catch(let exception) {
|
||||||
logger.error("failed to delete server config")
|
AbsLogger.error(message: "failed to delete server config")
|
||||||
debugPrint(exception)
|
debugPrint(exception)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,7 +121,7 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch(let exception) {
|
} catch(let exception) {
|
||||||
logger.error("failed to save server config active index")
|
AbsLogger.error(message: "failed to save server config active index")
|
||||||
debugPrint(exception)
|
debugPrint(exception)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,7 +141,7 @@ class Database {
|
|||||||
realm.add(deviceSettings)
|
realm.add(deviceSettings)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("failed to save device settings")
|
AbsLogger.error(message: "failed to save device settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,4 +279,54 @@ class Database {
|
|||||||
return nil
|
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)
|
let json = try JSONSerialization.data(withJSONObject: value)
|
||||||
return try JSONDecoder().decode(type, from: json)
|
return try JSONDecoder().decode(type, from: json)
|
||||||
} catch {
|
} catch {
|
||||||
AppLogger().error("Failed to get json for \(key)")
|
AbsLogger.error(message: "Failed to get json for \(key)", error: error)
|
||||||
debugPrint(error)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,3 +105,80 @@ extension URL {
|
|||||||
return attributes?[.creationDate] as? Date
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@
|
|||||||
<div class="flex-grow"></div>
|
<div class="flex-grow"></div>
|
||||||
<div class="text-xs text-gray-400">{{ log.tag }}</div>
|
<div class="text-xs text-gray-400">{{ log.tag }}</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -144,6 +144,12 @@ npx cap open android
|
|||||||
|
|
||||||
Start coding!
|
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
|
### Mac Environment Setup for iOS
|
||||||
|
|
||||||
Required Software:
|
Required Software:
|
||||||
@@ -213,3 +219,9 @@ npx cap open ios
|
|||||||
<br>
|
<br>
|
||||||
|
|
||||||
Start coding!
|
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