This commit is contained in:
advplyr
2022-08-27 15:54:19 -05:00
18 changed files with 704 additions and 375 deletions
+9 -1
View File
@@ -11,7 +11,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// Override point for customization after application launch. // Override point for customization after application launch.
let configuration = Realm.Configuration( let configuration = Realm.Configuration(
schemaVersion: 2, schemaVersion: 4,
migrationBlock: { migration, oldSchemaVersion in migrationBlock: { migration, oldSchemaVersion in
if (oldSchemaVersion < 1) { if (oldSchemaVersion < 1) {
NSLog("Realm schema version was \(oldSchemaVersion)") NSLog("Realm schema version was \(oldSchemaVersion)")
@@ -19,6 +19,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
newObject?["enableAltView"] = false newObject?["enableAltView"] = false
} }
} }
if (oldSchemaVersion < 4) {
NSLog("Realm schema version was \(oldSchemaVersion)... Reindexing server configs")
var indexCounter = 1
migration.enumerateObjects(ofType: ServerConnectionConfig.className()) { oldObject, newObject in
newObject?["index"] = indexCounter
indexCounter += 1
}
}
} }
) )
Realm.Configuration.defaultConfiguration = configuration Realm.Configuration.defaultConfiguration = configuration
+30 -28
View File
@@ -38,9 +38,11 @@ public class AbsAudioPlayer: CAPPlugin {
do { do {
// Fetch the most recent active session // Fetch the most recent active session
let activeSession = try await Realm().objects(PlaybackSession.self).where({ $0.isActiveSession == true }).last?.freeze() let activeSession = try await Realm().objects(PlaybackSession.self).where({
$0.isActiveSession == true && $0.serverConnectionConfigId == Store.serverConfig?.id
}).last?.freeze()
if let activeSession = activeSession { if let activeSession = activeSession {
await PlayerProgress.syncFromServer() await PlayerProgress.shared.syncFromServer()
try self.startPlaybackSession(activeSession, playWhenReady: false, playbackRate: PlayerSettings.main().playbackRate) try self.startPlaybackSession(activeSession, playWhenReady: false, playbackRate: PlayerSettings.main().playbackRate)
} }
} catch { } catch {
@@ -79,9 +81,9 @@ public class AbsAudioPlayer: CAPPlugin {
NSLog("Failed to get local playback session") NSLog("Failed to get local playback session")
return call.resolve([:]) return call.resolve([:])
} }
playbackSession.save()
do { do {
try playbackSession.save()
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) {
@@ -91,8 +93,8 @@ public class AbsAudioPlayer: CAPPlugin {
} }
} else { // Playing from the server } else { // Playing from the server
ApiClient.startPlaybackSession(libraryItemId: libraryItemId!, episodeId: episodeId, forceTranscode: false) { session in ApiClient.startPlaybackSession(libraryItemId: libraryItemId!, episodeId: episodeId, forceTranscode: false) { session in
session.save()
do { do {
try session.save()
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) {
@@ -120,7 +122,7 @@ public class AbsAudioPlayer: CAPPlugin {
@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()
settings.update { try? settings.update {
settings.playbackRate = playbackRate settings.playbackRate = playbackRate
} }
PlayerHandler.setPlaybackSpeed(speed: settings.playbackRate) PlayerHandler.setPlaybackSpeed(speed: settings.playbackRate)
@@ -166,45 +168,47 @@ public class AbsAudioPlayer: CAPPlugin {
@objc func decreaseSleepTime(_ call: CAPPluginCall) { @objc func decreaseSleepTime(_ call: CAPPluginCall) {
guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) } guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) }
guard let time = Int(timeString) else { return call.resolve([ "success": false ]) } guard let time = Double(timeString) else { return call.resolve([ "success": false ]) }
guard let currentSleepTime = PlayerHandler.remainingSleepTime else { return call.resolve([ "success": false ]) } guard let _ = PlayerHandler.remainingSleepTime else { return call.resolve([ "success": false ]) }
PlayerHandler.remainingSleepTime = currentSleepTime - (time / 1000) let seconds = time/1000
PlayerHandler.decreaseSleepTime(decreaseSeconds: seconds)
call.resolve() call.resolve()
} }
@objc func increaseSleepTime(_ call: CAPPluginCall) { @objc func increaseSleepTime(_ call: CAPPluginCall) {
guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) } guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) }
guard let time = Int(timeString) else { return call.resolve([ "success": false ]) } guard let time = Double(timeString) else { return call.resolve([ "success": false ]) }
guard let currentSleepTime = PlayerHandler.remainingSleepTime else { return call.resolve([ "success": false ]) } guard let _ = PlayerHandler.remainingSleepTime else { return call.resolve([ "success": false ]) }
PlayerHandler.remainingSleepTime = currentSleepTime + (time / 1000) let seconds = time/1000
PlayerHandler.increaseSleepTime(increaseSeconds: seconds)
call.resolve() call.resolve()
} }
@objc func setSleepTimer(_ call: CAPPluginCall) { @objc func setSleepTimer(_ call: CAPPluginCall) {
guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) } guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) }
guard let time = Int(timeString) else { return call.resolve([ "success": false ]) } guard let time = Int(timeString) else { return call.resolve([ "success": false ]) }
let timeSeconds = time / 1000 let isChapterTime = call.getBool("isChapterTime", false)
NSLog("chapter time: \(call.getBool("isChapterTime", false))") let seconds = time / 1000
if call.getBool("isChapterTime", false) { NSLog("chapter time: \(isChapterTime)")
let timeToPause = timeSeconds - Int(PlayerHandler.getCurrentTime() ?? 0) if isChapterTime {
if timeToPause < 0 { return call.resolve([ "success": false ]) } PlayerHandler.setChapterSleepTime(stopAt: Double(seconds))
PlayerHandler.sleepTimerChapterStopTime = timeSeconds
PlayerHandler.remainingSleepTime = timeToPause
return call.resolve([ "success": true ]) return call.resolve([ "success": true ])
} }
PlayerHandler.sleepTimerChapterStopTime = nil PlayerHandler.setSleepTime(secondsUntilSleep: Double(seconds))
PlayerHandler.remainingSleepTime = timeSeconds
call.resolve([ "success": true ]) call.resolve([ "success": true ])
} }
@objc func cancelSleepTimer(_ call: CAPPluginCall) { @objc func cancelSleepTimer(_ call: CAPPluginCall) {
PlayerHandler.remainingSleepTime = nil PlayerHandler.cancelSleepTime()
PlayerHandler.sleepTimerChapterStopTime = nil PlayerHandler.sleepTimerChapterStopTime = nil
call.resolve() call.resolve()
} }
@objc func getSleepTimerTime(_ call: CAPPluginCall) { @objc func getSleepTimerTime(_ call: CAPPluginCall) {
call.resolve([ call.resolve([
"value": PlayerHandler.remainingSleepTime "value": PlayerHandler.remainingSleepTime
@@ -240,17 +244,15 @@ public class AbsAudioPlayer: CAPPlugin {
// If direct playing then fallback to transcode // If direct playing then fallback to transcode
ApiClient.startPlaybackSession(libraryItemId: libraryItemId, episodeId: episodeId, forceTranscode: true) { session in ApiClient.startPlaybackSession(libraryItemId: libraryItemId, episodeId: episodeId, forceTranscode: true) { session in
session.save()
PlayerHandler.startPlayback(sessionId: session.id, playWhenReady: self.initialPlayWhenReady, playbackRate: PlayerSettings.main().playbackRate)
do { do {
try session.save()
PlayerHandler.startPlayback(sessionId: session.id, playWhenReady: self.initialPlayWhenReady, playbackRate: PlayerSettings.main().playbackRate)
self.sendPlaybackSession(session: try session.asDictionary()) self.sendPlaybackSession(session: try session.asDictionary())
self.sendMetadata()
} catch(let exception) { } catch(let exception) {
NSLog("failed to convert session to json") NSLog("Failed to start transcoded session")
debugPrint(exception) debugPrint(exception)
} }
self.sendMetadata()
} }
} else { } else {
self.notifyListeners("onPlaybackFailed", data: [ self.notifyListeners("onPlaybackFailed", data: [
+32 -26
View File
@@ -43,7 +43,7 @@ public class AbsDatabase: CAPPlugin {
let config = ServerConnectionConfig() let config = ServerConnectionConfig()
config.id = id ?? "" config.id = id ?? ""
config.index = 1 config.index = 0
config.name = name config.name = name
config.address = address config.address = address
config.userId = userId config.userId = userId
@@ -51,7 +51,8 @@ public class AbsDatabase: CAPPlugin {
config.token = token config.token = token
Store.serverConfig = config Store.serverConfig = config
call.resolve(convertServerConnectionConfigToJSON(config: config)) let savedConfig = Store.serverConfig // Fetch the latest value
call.resolve(convertServerConnectionConfigToJSON(config: savedConfig!))
} }
@objc func removeServerConnectionConfig(_ call: CAPPluginCall) { @objc func removeServerConnectionConfig(_ call: CAPPluginCall) {
let id = call.getString("serverConnectionConfigId", "") let id = call.getString("serverConnectionConfigId", "")
@@ -139,7 +140,7 @@ public class AbsDatabase: CAPPlugin {
call.reject("localMediaProgressId not specificed") call.reject("localMediaProgressId not specificed")
return return
} }
Database.shared.removeLocalMediaProgress(localMediaProgressId: localMediaProgressId) try? Database.shared.removeLocalMediaProgress(localMediaProgressId: localMediaProgressId)
call.resolve() call.resolve()
} }
@@ -171,15 +172,15 @@ public class AbsDatabase: CAPPlugin {
return call.reject("localLibraryItemId or localMediaProgressId must be specified") return call.reject("localLibraryItemId or localMediaProgressId must be specified")
} }
let localMediaProgress = LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: localMediaProgressId, localLibraryItemId: localLibraryItemId, localEpisodeId: localEpisodeId) let localMediaProgress = try LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: localMediaProgressId, localLibraryItemId: localLibraryItemId, localEpisodeId: localEpisodeId)
guard let localMediaProgress = localMediaProgress else { guard let localMediaProgress = localMediaProgress else {
call.reject("Local media progress not found or created") call.reject("Local media progress not found or created")
return return
} }
localMediaProgress.updateFromServerMediaProgress(serverMediaProgress)
NSLog("syncServerMediaProgressWithLocalMediaProgress: Saving local media progress") NSLog("syncServerMediaProgressWithLocalMediaProgress: Saving local media progress")
Database.shared.saveLocalMediaProgress(localMediaProgress) 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")
@@ -195,31 +196,36 @@ public class AbsDatabase: CAPPlugin {
NSLog("updateLocalMediaProgressFinished \(localMediaProgressId ?? "Unknown") | Is Finished: \(isFinished)") NSLog("updateLocalMediaProgressFinished \(localMediaProgressId ?? "Unknown") | Is Finished: \(isFinished)")
let localMediaProgress = LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: localMediaProgressId, localLibraryItemId: localLibraryItemId, localEpisodeId: localEpisodeId) do {
guard let localMediaProgress = localMediaProgress else { let localMediaProgress = try LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: localMediaProgressId, localLibraryItemId: localLibraryItemId, localEpisodeId: localEpisodeId)
call.resolve(["error": "Library Item not found"]) guard let localMediaProgress = localMediaProgress else {
return call.resolve(["error": "Library Item not found"])
} return
}
// Update finished status // Update finished status
localMediaProgress.updateIsFinished(isFinished) try localMediaProgress.updateIsFinished(isFinished)
Database.shared.saveLocalMediaProgress(localMediaProgress)
// Build API response // Build API response
let progressDictionary = try? localMediaProgress.asDictionary() let progressDictionary = try? localMediaProgress.asDictionary()
var response: [String: Any] = ["local": true, "server": false, "localMediaProgress": progressDictionary ?? ""] var response: [String: Any] = ["local": true, "server": false, "localMediaProgress": progressDictionary ?? ""]
// Send update to the server if logged in // Send update to the server if logged in
let hasLinkedServer = localMediaProgress.serverConnectionConfigId != nil let hasLinkedServer = localMediaProgress.serverConnectionConfigId != nil
let loggedIntoServer = Store.serverConfig?.id == localMediaProgress.serverConnectionConfigId let loggedIntoServer = Store.serverConfig?.id == localMediaProgress.serverConnectionConfigId
if hasLinkedServer && loggedIntoServer { if hasLinkedServer && loggedIntoServer {
response["server"] = true response["server"] = true
let payload = ["isFinished": isFinished] let payload = ["isFinished": isFinished]
ApiClient.updateMediaProgress(libraryItemId: localMediaProgress.libraryItemId!, episodeId: localEpisodeId, payload: payload) { ApiClient.updateMediaProgress(libraryItemId: localMediaProgress.libraryItemId!, episodeId: localEpisodeId, payload: payload) {
call.resolve(response)
}
} else {
call.resolve(response) call.resolve(response)
} }
} else { } catch {
call.resolve(response) debugPrint(error)
call.resolve(["error": "Failed to mark as complete"])
return
} }
} }
+5 -5
View File
@@ -28,7 +28,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
handleDownloadTaskUpdate(downloadTask: downloadTask) { downloadItem, downloadItemPart in handleDownloadTaskUpdate(downloadTask: downloadTask) { downloadItem, downloadItemPart in
let realm = try! Realm() let realm = try Realm()
try realm.write { try realm.write {
downloadItemPart.progress = 100 downloadItemPart.progress = 100
downloadItemPart.completed = true downloadItemPart.completed = true
@@ -139,7 +139,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
} }
self.handleDownloadTaskCompleteFromDownloadItem(item) self.handleDownloadTaskCompleteFromDownloadItem(item)
if let item = Database.shared.getDownloadItem(downloadItemId: item.id!) { if let item = Database.shared.getDownloadItem(downloadItemId: item.id!) {
item.delete() try? item.delete()
} }
} }
@@ -181,7 +181,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
} }
} else { } else {
localLibraryItem = LocalLibraryItem(libraryItem, localUrl: localDirectory, server: Store.serverConfig!, files: files, coverPath: coverFile) localLibraryItem = LocalLibraryItem(libraryItem, localUrl: localDirectory, server: Store.serverConfig!, files: files, coverPath: coverFile)
Database.shared.saveLocalLibraryItem(localLibraryItem: localLibraryItem!) try? Database.shared.saveLocalLibraryItem(localLibraryItem: localLibraryItem!)
} }
statusNotification["localLibraryItem"] = try? localLibraryItem.asDictionary() statusNotification["localLibraryItem"] = try? localLibraryItem.asDictionary()
@@ -189,7 +189,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
if let progress = libraryItem.userMediaProgress { if let progress = libraryItem.userMediaProgress {
let episode = downloadItem.media?.episodes.first(where: { $0.id == downloadItem.episodeId }) let episode = downloadItem.media?.episodes.first(where: { $0.id == downloadItem.episodeId })
let localMediaProgress = LocalMediaProgress(localLibraryItem: localLibraryItem!, episode: episode, progress: progress) let localMediaProgress = LocalMediaProgress(localLibraryItem: localLibraryItem!, episode: episode, progress: progress)
Database.shared.saveLocalMediaProgress(localMediaProgress) try? localMediaProgress.save()
statusNotification["localMediaProgress"] = try? localMediaProgress.asDictionary() statusNotification["localMediaProgress"] = try? localMediaProgress.asDictionary()
} }
@@ -276,7 +276,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
} }
// Persist in the database before status start coming in // Persist in the database before status start coming in
Database.shared.saveDownloadItem(downloadItem) try Database.shared.saveDownloadItem(downloadItem)
// Start all the downloads // Start all the downloads
for task in tasks { for task in tasks {
+20 -15
View File
@@ -70,7 +70,7 @@ public class AbsFileSystem: CAPPlugin {
do { do {
if let localLibraryItemId = localLibraryItemId, let item = Database.shared.getLocalLibraryItem(localLibraryItemId: localLibraryItemId) { if let localLibraryItemId = localLibraryItemId, let item = Database.shared.getLocalLibraryItem(localLibraryItemId: localLibraryItemId) {
try FileManager.default.removeItem(at: item.contentDirectory!) try FileManager.default.removeItem(at: item.contentDirectory!)
item.delete() try item.delete()
success = true success = true
} }
} catch { } catch {
@@ -89,24 +89,29 @@ public class AbsFileSystem: CAPPlugin {
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) {
item.update { do {
do { try item.update {
if let fileIndex = item.localFiles.firstIndex(where: { $0.id == trackLocalFileId }) { do {
try FileManager.default.removeItem(at: item.localFiles[fileIndex].contentPath) if let fileIndex = item.localFiles.firstIndex(where: { $0.id == trackLocalFileId }) {
item.realm?.delete(item.localFiles[fileIndex]) try FileManager.default.removeItem(at: item.localFiles[fileIndex].contentPath)
if item.isPodcast, let media = item.media { item.realm?.delete(item.localFiles[fileIndex])
if let episodeIndex = media.episodes.firstIndex(where: { $0.audioTrack?.localFileId == trackLocalFileId }) { if item.isPodcast, let media = item.media {
media.episodes.remove(at: episodeIndex) if let episodeIndex = media.episodes.firstIndex(where: { $0.audioTrack?.localFileId == trackLocalFileId }) {
media.episodes.remove(at: episodeIndex)
}
item.media = media
} }
item.media = media call.resolve(try item.asDictionary())
success = true
} }
call.resolve(try item.asDictionary()) } catch {
success = true NSLog("Failed to delete \(error)")
success = false
} }
} catch {
NSLog("Failed to delete \(error)")
success = false
} }
} catch {
NSLog("Failed to delete \(error)")
success = false
} }
} }
@@ -31,6 +31,7 @@ class PlaybackSession: Object, Codable, Deletable {
@Persisted var serverConnectionConfigId: String? @Persisted var serverConnectionConfigId: String?
@Persisted var serverAddress: String? @Persisted var serverAddress: String?
@Persisted var isActiveSession = true @Persisted var isActiveSession = true
@Persisted var serverUpdatedAt: Double = 0
var isLocal: Bool { self.localLibraryItem != nil } var isLocal: Bool { self.localLibraryItem != nil }
var mediaPlayer: String { "AVPlayer" } var mediaPlayer: String { "AVPlayer" }
@@ -88,8 +88,8 @@ extension DownloadItem {
self.downloadItemParts.allSatisfy({ $0.failed == false }) self.downloadItemParts.allSatisfy({ $0.failed == false })
} }
func delete() { func delete() throws {
try! self.realm?.write { try self.realm?.write {
self.realm?.delete(self.downloadItemParts) self.realm?.delete(self.downloadItemParts)
self.realm?.delete(self) self.realm?.delete(self)
} }
@@ -198,8 +198,8 @@ extension LocalLibraryItem {
) )
} }
func delete() { func delete() throws {
try! self.realm?.write { try self.realm?.write {
self.realm?.delete(self.localFiles) self.realm?.delete(self.localFiles)
self.realm?.delete(self) self.realm?.delete(self)
} }
@@ -63,8 +63,12 @@ class LocalMediaProgress: Object, Codable {
try container.encode(localLibraryItemId, forKey: .localLibraryItemId) try container.encode(localLibraryItemId, forKey: .localLibraryItemId)
try container.encode(localEpisodeId, forKey: .localEpisodeId) try container.encode(localEpisodeId, forKey: .localEpisodeId)
try container.encode(duration, forKey: .duration) try container.encode(duration, forKey: .duration)
try container.encode(progress, forKey: .progress) if progress.isNaN == false {
try container.encode(currentTime, forKey: .currentTime) try container.encode(progress, forKey: .progress)
}
if currentTime.isNaN == false {
try container.encode(currentTime, forKey: .currentTime)
}
try container.encode(isFinished, forKey: .isFinished) try container.encode(isFinished, forKey: .isFinished)
try container.encode(lastUpdate, forKey: .lastUpdate) try container.encode(lastUpdate, forKey: .lastUpdate)
try container.encode(startedAt, forKey: .startedAt) try container.encode(startedAt, forKey: .startedAt)
@@ -115,8 +119,8 @@ extension LocalMediaProgress {
self.finishedAt = progress.finishedAt self.finishedAt = progress.finishedAt
} }
func updateIsFinished(_ finished: Bool) { func updateIsFinished(_ finished: Bool) throws {
try! Realm().write { try self.realm?.write {
if self.isFinished != finished { if self.isFinished != finished {
self.progress = finished ? 1.0 : 0.0 self.progress = finished ? 1.0 : 0.0
} }
@@ -131,8 +135,8 @@ extension LocalMediaProgress {
} }
} }
func updateFromPlaybackSession(_ playbackSession: PlaybackSession) { func updateFromPlaybackSession(_ playbackSession: PlaybackSession) throws {
try! Realm().write { try self.realm?.write {
self.currentTime = playbackSession.currentTime self.currentTime = playbackSession.currentTime
self.progress = playbackSession.progress self.progress = playbackSession.progress
self.lastUpdate = Date().timeIntervalSince1970 * 1000 self.lastUpdate = Date().timeIntervalSince1970 * 1000
@@ -141,8 +145,8 @@ extension LocalMediaProgress {
} }
} }
func updateFromServerMediaProgress(_ serverMediaProgress: MediaProgress) { func updateFromServerMediaProgress(_ serverMediaProgress: MediaProgress) throws {
try! Realm().write { try self.realm?.write {
self.isFinished = serverMediaProgress.isFinished self.isFinished = serverMediaProgress.isFinished
self.progress = serverMediaProgress.progress self.progress = serverMediaProgress.progress
self.currentTime = serverMediaProgress.currentTime self.currentTime = serverMediaProgress.currentTime
@@ -153,20 +157,25 @@ extension LocalMediaProgress {
} }
} }
static func fetchOrCreateLocalMediaProgress(localMediaProgressId: String?, localLibraryItemId: String?, localEpisodeId: String?) -> LocalMediaProgress? { static func fetchOrCreateLocalMediaProgress(localMediaProgressId: String?, localLibraryItemId: String?, localEpisodeId: String?) throws -> LocalMediaProgress? {
if let localMediaProgressId = localMediaProgressId { let realm = try Realm()
// Check if it existing in the database, if not, we need to create it return try realm.write { () -> LocalMediaProgress? in
if let progress = Database.shared.getLocalMediaProgress(localMediaProgressId: localMediaProgressId) { if let localMediaProgressId = localMediaProgressId {
return progress // Check if it existing in the database, if not, we need to create it
if let progress = Database.shared.getLocalMediaProgress(localMediaProgressId: localMediaProgressId) {
return progress
}
} }
}
if let localLibraryItemId = localLibraryItemId { if let localLibraryItemId = localLibraryItemId {
guard let localLibraryItem = Database.shared.getLocalLibraryItem(localLibraryItemId: localLibraryItemId) else { return nil } guard let localLibraryItem = Database.shared.getLocalLibraryItem(localLibraryItemId: localLibraryItemId) else { return nil }
let episode = localLibraryItem.getPodcastEpisode(episodeId: localEpisodeId) let episode = localLibraryItem.getPodcastEpisode(episodeId: localEpisodeId)
return LocalMediaProgress(localLibraryItem: localLibraryItem, episode: episode) let progress = LocalMediaProgress(localLibraryItem: localLibraryItem, episode: episode)
} else { realm.add(progress)
return nil return progress
} else {
return nil
}
} }
} }
} }
@@ -37,7 +37,7 @@ class AudioTrack: EmbeddedObject, Codable {
contentUrl = try? values.decode(String.self, forKey: .contentUrl) contentUrl = try? values.decode(String.self, forKey: .contentUrl)
mimeType = try values.decode(String.self, forKey: .mimeType) mimeType = try values.decode(String.self, forKey: .mimeType)
metadata = try? values.decode(FileMetadata.self, forKey: .metadata) metadata = try? values.decode(FileMetadata.self, forKey: .metadata)
localFileId = try! values.decodeIfPresent(String.self, forKey: .localFileId) localFileId = try? values.decodeIfPresent(String.self, forKey: .localFileId)
serverIndex = try? values.decode(Int.self, forKey: .serverIndex) serverIndex = try? values.decode(Int.self, forKey: .serverIndex)
} }
+224 -21
View File
@@ -18,12 +18,13 @@ enum PlayMethod:Int {
} }
class AudioPlayer: NSObject { class AudioPlayer: NSObject {
private let queue = DispatchQueue(label: "ABSAudioPlayerQueue")
// enums and @objc are not compatible // enums and @objc are not compatible
@objc dynamic var status: Int @objc dynamic var status: Int
@objc dynamic var rate: Float @objc dynamic var rate: Float
private var tmpRate: Float = 1.0 private var tmpRate: Float = 1.0
private var lastPlayTime: Double = 0.0
private var playerContext = 0 private var playerContext = 0
private var playerItemContext = 0 private var playerItemContext = 0
@@ -34,12 +35,18 @@ class AudioPlayer: NSObject {
private var audioPlayer: AVQueuePlayer private var audioPlayer: AVQueuePlayer
private var sessionId: String private var sessionId: String
private var timeObserverToken: Any?
private var queueObserver:NSKeyValueObservation? private var queueObserver:NSKeyValueObservation?
private var queueItemStatusObserver:NSKeyValueObservation? private var queueItemStatusObserver:NSKeyValueObservation?
private var sleepTimeStopAt: Double?
private var sleepTimeToken: Any?
private var currentTrackIndex = 0 private var currentTrackIndex = 0
private var allPlayerItems:[AVPlayerItem] = [] private var allPlayerItems:[AVPlayerItem] = []
private var pausedTimer: Timer?
// MARK: - Constructor // MARK: - Constructor
init(sessionId: String, playWhenReady: Bool = false, playbackRate: Float = 1) { init(sessionId: String, playWhenReady: Bool = false, playbackRate: Float = 1) {
self.playWhenReady = playWhenReady self.playWhenReady = playWhenReady
@@ -77,12 +84,16 @@ class AudioPlayer: NSObject {
self.audioPlayer.insert(item, after:self.audioPlayer.items().last) self.audioPlayer.insert(item, after:self.audioPlayer.items().last)
} }
setupTimeObserver()
setupQueueObserver() setupQueueObserver()
setupQueueItemStatusObserver() setupQueueItemStatusObserver()
NSLog("Audioplayer ready") NSLog("Audioplayer ready")
} }
deinit { deinit {
self.stopPausedTimer()
self.removeSleepTimer()
self.removeTimeObserver()
self.queueObserver?.invalidate() self.queueObserver?.invalidate()
self.queueItemStatusObserver?.invalidate() self.queueItemStatusObserver?.invalidate()
destroy() destroy()
@@ -124,6 +135,36 @@ class AudioPlayer: NSObject {
return 0 return 0
} }
private func setupTimeObserver() {
// Time observer should be configured on the main queue
DispatchQueue.runOnMainQueue {
self.removeTimeObserver()
let timeScale = CMTimeScale(NSEC_PER_SEC)
// Rate will be different depending on playback speed, aim for 2 observations/sec
let seconds = 0.5 * (self.rate > 0 ? self.rate : 1.0)
let time = CMTime(seconds: Double(seconds), preferredTimescale: timeScale)
self.timeObserverToken = self.audioPlayer.addPeriodicTimeObserver(forInterval: time, queue: self.queue) { [weak self] time in
Task {
// Let the player update the current playback positions
await PlayerProgress.shared.syncFromPlayer(currentTime: time.seconds, includesPlayProgress: true, isStopping: false)
}
// Update the sleep time, if set
if self?.sleepTimeStopAt != nil {
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil)
}
}
}
}
private func removeTimeObserver() {
if let timeObserverToken = timeObserverToken {
self.audioPlayer.removeTimeObserver(timeObserverToken)
self.timeObserverToken = nil
}
}
func setupQueueObserver() { func setupQueueObserver() {
self.queueObserver = self.audioPlayer.observe(\.currentItem, options: [.new]) {_,_ in self.queueObserver = self.audioPlayer.observe(\.currentItem, options: [.new]) {_,_ in
let prevTrackIndex = self.currentTrackIndex let prevTrackIndex = self.currentTrackIndex
@@ -165,25 +206,44 @@ class AudioPlayer: NSObject {
}) })
} }
private func startPausedTimer() {
guard self.pausedTimer == nil else { return }
self.queue.async {
self.pausedTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { timer in
NSLog("PAUSE TIMER: Syncing from server")
Task { await PlayerProgress.shared.syncFromServer() }
}
}
}
private func stopPausedTimer() {
self.pausedTimer?.invalidate()
self.pausedTimer = nil
}
// MARK: - Methods // MARK: - Methods
public func play(allowSeekBack: Bool = false) { public func play(allowSeekBack: Bool = false) {
guard self.isInitialized() else { return } guard self.isInitialized() else { return }
if allowSeekBack { // Capture remaining sleep time before changing the track position
let diffrence = Date.timeIntervalSinceReferenceDate - lastPlayTime let sleepSecondsRemaining = PlayerHandler.remainingSleepTime
if allowSeekBack, let session = Database.shared.getPlaybackSession(id: self.sessionId) {
let lastPlayed = (session.updatedAt ?? 0)/1000
let difference = Date.timeIntervalSinceReferenceDate - lastPlayed
var time: Int? var time: Int?
if lastPlayTime == 0 { if lastPlayed == 0 {
time = 5 time = 5
} else if diffrence < 6 { } else if difference < 6 {
time = 2 time = 2
} else if diffrence < 12 { } else if difference < 12 {
time = 10 time = 10
} else if diffrence < 30 { } else if difference < 30 {
time = 15 time = 15
} else if diffrence < 180 { } else if difference < 180 {
time = 20 time = 20
} else if diffrence < 3600 { } else if difference < 3600 {
time = 25 time = 25
} else { } else {
time = 29 time = 29
@@ -193,13 +253,22 @@ class AudioPlayer: NSObject {
seek(getCurrentTime() - Double(time!), from: "play") seek(getCurrentTime() - Double(time!), from: "play")
} }
} }
lastPlayTime = Date.timeIntervalSinceReferenceDate
self.stopPausedTimer()
Task {
let isPlaying = self.status > 0
await PlayerProgress.shared.syncFromPlayer(currentTime: self.getCurrentTime(), includesPlayProgress: isPlaying, isStopping: false)
}
self.audioPlayer.play() self.audioPlayer.play()
self.status = 1 self.status = 1
self.rate = self.tmpRate self.rate = self.tmpRate
self.audioPlayer.rate = self.tmpRate self.audioPlayer.rate = self.tmpRate
// If we have an active sleep timer, reschedule based on rate
self.rescheduleSleepTimerAtTime(time: self.getCurrentTime(), secondsRemaining: sleepSecondsRemaining)
updateNowPlaying() updateNowPlaying()
} }
@@ -207,11 +276,18 @@ class AudioPlayer: NSObject {
guard self.isInitialized() else { return } guard self.isInitialized() else { return }
self.audioPlayer.pause() self.audioPlayer.pause()
Task {
let wasPlaying = self.status > 0
await PlayerProgress.shared.syncFromPlayer(currentTime: self.getCurrentTime(), includesPlayProgress: wasPlaying, isStopping: true)
}
self.status = 0 self.status = 0
self.rate = 0.0 self.rate = 0.0
updateNowPlaying() updateNowPlaying()
lastPlayTime = Date.timeIntervalSinceReferenceDate
self.startPausedTimer()
} }
public func seek(_ to: Double, from: String) { public func seek(_ to: Double, from: String) {
@@ -228,6 +304,8 @@ class AudioPlayer: NSObject {
let trackEnd = ctso + currentTrack.duration let trackEnd = ctso + currentTrack.duration
NSLog("Seek current track END = \(trackEnd)") NSLog("Seek current track END = \(trackEnd)")
// Capture remaining sleep time before changing the track position
let sleepSecondsRemaining = PlayerHandler.remainingSleepTime
let indexOfSeek = getItemIndexForTime(time: to) let indexOfSeek = getItemIndexForTime(time: to)
NSLog("Seek to index \(indexOfSeek) | Current index \(self.currentTrackIndex)") NSLog("Seek to index \(indexOfSeek) | Current index \(self.currentTrackIndex)")
@@ -236,7 +314,7 @@ class AudioPlayer: NSObject {
if (self.currentTrackIndex != indexOfSeek) { if (self.currentTrackIndex != indexOfSeek) {
self.currentTrackIndex = indexOfSeek self.currentTrackIndex = indexOfSeek
playbackSession.update { try? playbackSession.update {
playbackSession.currentTime = to playbackSession.currentTime = to
} }
@@ -255,30 +333,154 @@ class AudioPlayer: NSObject {
let currentTrackStartOffset = playbackSession.audioTracks[self.currentTrackIndex].startOffset ?? 0.0 let currentTrackStartOffset = playbackSession.audioTracks[self.currentTrackIndex].startOffset ?? 0.0
let seekTime = to - currentTrackStartOffset let seekTime = to - currentTrackStartOffset
self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000)) { completed in self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000)) { [weak self] completed in
if !completed { if !completed {
NSLog("WARNING: seeking not completed (to \(seekTime)") NSLog("WARNING: seeking not completed (to \(seekTime)")
} }
if continuePlaying { if continuePlaying {
self.play() self?.play()
}
self?.updateNowPlaying()
// If we have an active sleep timer, reschedule based on seek, since seek is fuzzy
// This needs to occur after play() to capture the correct playback rate
if let currentTime = self?.getCurrentTime() {
self?.rescheduleSleepTimerAtTime(time: currentTime, secondsRemaining: sleepSecondsRemaining)
} }
self.updateNowPlaying()
} }
} }
} }
public func setPlaybackRate(_ rate: Float, observed: Bool = false) { public func setPlaybackRate(_ rate: Float, observed: Bool = false) {
// Capture remaining sleep time before changing the rate
let sleepSecondsRemaining = PlayerHandler.remainingSleepTime
let playbackSpeedChanged = rate > 0.0 && rate != self.tmpRate && !(observed && rate == 1)
if self.audioPlayer.rate != rate { if self.audioPlayer.rate != rate {
NSLog("setPlaybakRate rate changed from \(self.audioPlayer.rate) to \(rate)") NSLog("setPlaybakRate rate changed from \(self.audioPlayer.rate) to \(rate)")
self.audioPlayer.rate = rate self.audioPlayer.rate = rate
} }
if rate > 0.0 && !(observed && rate == 1) {
self.tmpRate = rate
}
self.rate = rate self.rate = rate
self.updateNowPlaying() self.updateNowPlaying()
if playbackSpeedChanged {
self.tmpRate = rate
// If we have an active sleep timer, reschedule based on rate
self.rescheduleSleepTimerAtTime(time: self.getCurrentTime(), secondsRemaining: sleepSecondsRemaining)
// Setup the time observer again at the new rate
self.setupTimeObserver()
}
}
public func getSleepStopAt() -> Double? {
return self.sleepTimeStopAt
}
// Let iOS handle the sleep timer logic by letting us know when it's time to stop
public func setSleepTime(stopAt: Double, scaleBasedOnSpeed: Bool = false) {
NSLog("SLEEP TIMER: Scheduling for \(stopAt)")
// Reset any previous sleep timer
self.removeSleepTimer()
let currentTime = getCurrentTime()
// Mark the time to stop playing
if scaleBasedOnSpeed {
// Consider paused as playing at 1x
let rate = Double(self.rate > 0 ? self.rate : 1)
// Calculate the scaled time to stop at
let timeUntilSleep = (stopAt - currentTime) * rate
self.sleepTimeStopAt = currentTime + timeUntilSleep
NSLog("SLEEP TIMER: Adjusted based on playback speed of \(rate) to \(self.sleepTimeStopAt!)")
} else {
self.sleepTimeStopAt = stopAt
}
guard let sleepTimeStopAt = self.sleepTimeStopAt else { return }
let sleepTime = CMTime(seconds: sleepTimeStopAt, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
// Schedule the observation time
var times = [NSValue]()
times.append(NSValue(time: sleepTime))
sleepTimeToken = self.audioPlayer.addBoundaryTimeObserver(forTimes: times, queue: queue) { [weak self] in
NSLog("SLEEP TIMER: Pausing audio")
self?.pause()
PlayerHandler.sleepTimerChapterStopTime = nil
self?.removeSleepTimer()
}
// Update the UI
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil)
}
private func rescheduleSleepTimerAtTime(time: Double, secondsRemaining: Int?) {
// Not a chapter sleep timer
let hadToCancelChapterSleepTimer = decideIfChapterSleepTimerNeedsToBeCanceled(time: time)
guard !hadToCancelChapterSleepTimer else { return }
guard PlayerHandler.sleepTimerChapterStopTime == nil else { return }
// Verify sleep timer is set
guard self.sleepTimeToken != nil else { return }
// Update the sleep timer
if let secondsRemaining = secondsRemaining {
let newSleepTimerPosition = time + Double(secondsRemaining)
self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true)
}
}
private func decideIfChapterSleepTimerNeedsToBeCanceled(time: Double) -> Bool {
if let chapterSleepTime = PlayerHandler.sleepTimerChapterStopTime {
let sleepIsBeforeCurrentTime = Double(chapterSleepTime) <= time
if sleepIsBeforeCurrentTime {
PlayerHandler.sleepTimerChapterStopTime = nil
self.removeSleepTimer()
return true
}
}
return false
}
public func increaseSleepTime(extraTimeInSeconds: Double) {
if let sleepTime = PlayerHandler.remainingSleepTime {
let currentTime = getCurrentTime()
let newSleepTimerPosition = currentTime + Double(sleepTime) + extraTimeInSeconds
if newSleepTimerPosition > currentTime {
self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true)
}
}
}
public func decreaseSleepTime(removeTimeInSeconds: Double) {
if let sleepTime = PlayerHandler.remainingSleepTime {
let currentTime = getCurrentTime()
let newSleepTimerPosition = currentTime + Double(sleepTime) - removeTimeInSeconds
guard newSleepTimerPosition > currentTime else { return }
if newSleepTimerPosition > currentTime {
self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true)
}
}
}
public func removeSleepTimer() {
self.sleepTimeStopAt = nil
if let token = sleepTimeToken {
self.audioPlayer.removeTimeObserver(token)
sleepTimeToken = nil
}
// Update the UI
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepEnded.rawValue), object: self)
} }
public func getCurrentTime() -> Double { public func getCurrentTime() -> Double {
@@ -332,7 +534,7 @@ class AudioPlayer: NSObject {
private func initAudioSession() { private func initAudioSession() {
do { do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio, options: [.allowAirPlay]) try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio)
try AVAudioSession.sharedInstance().setActive(true) try AVAudioSession.sharedInstance().setActive(true)
} catch { } catch {
NSLog("Failed to set AVAudioSession category") NSLog("Failed to set AVAudioSession category")
@@ -346,6 +548,7 @@ class AudioPlayer: NSObject {
UIApplication.shared.beginReceivingRemoteControlEvents() UIApplication.shared.beginReceivingRemoteControlEvents()
} }
let commandCenter = MPRemoteCommandCenter.shared() let commandCenter = MPRemoteCommandCenter.shared()
let deviceSettings = Database.shared.getDeviceSettings()
commandCenter.playCommand.isEnabled = true commandCenter.playCommand.isEnabled = true
commandCenter.playCommand.addTarget { [unowned self] event in commandCenter.playCommand.addTarget { [unowned self] event in
@@ -359,7 +562,7 @@ class AudioPlayer: NSObject {
} }
commandCenter.skipForwardCommand.isEnabled = true commandCenter.skipForwardCommand.isEnabled = true
commandCenter.skipForwardCommand.preferredIntervals = [30] commandCenter.skipForwardCommand.preferredIntervals = [NSNumber(value: deviceSettings.jumpForwardTime)]
commandCenter.skipForwardCommand.addTarget { [unowned self] event in commandCenter.skipForwardCommand.addTarget { [unowned self] event in
guard let command = event.command as? MPSkipIntervalCommand else { guard let command = event.command as? MPSkipIntervalCommand else {
return .noSuchContent return .noSuchContent
@@ -369,7 +572,7 @@ class AudioPlayer: NSObject {
return .success return .success
} }
commandCenter.skipBackwardCommand.isEnabled = true commandCenter.skipBackwardCommand.isEnabled = true
commandCenter.skipBackwardCommand.preferredIntervals = [30] commandCenter.skipBackwardCommand.preferredIntervals = [NSNumber(value: deviceSettings.jumpBackwardsTime)]
commandCenter.skipBackwardCommand.addTarget { [unowned self] event in commandCenter.skipBackwardCommand.addTarget { [unowned self] event in
guard let command = event.command as? MPSkipIntervalCommand else { guard let command = event.command as? MPSkipIntervalCommand else {
return .noSuchContent return .noSuchContent
+89 -154
View File
@@ -10,85 +10,8 @@ import RealmSwift
class PlayerHandler { class PlayerHandler {
private static var player: AudioPlayer? private static var player: AudioPlayer?
private static var playingTimer: Timer?
private static var pausedTimer: Timer?
private static var lastSyncTime: Double = 0.0
public static var sleepTimerChapterStopTime: Int? = nil public static var sleepTimerChapterStopTime: Int? = nil
private static var _remainingSleepTime: Int? = nil
public static var remainingSleepTime: Int? {
get {
return _remainingSleepTime
}
set(time) {
if time != nil && time! < 0 {
_remainingSleepTime = nil
} else {
_remainingSleepTime = time
}
if _remainingSleepTime == nil {
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepEnded.rawValue), object: _remainingSleepTime)
} else {
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: _remainingSleepTime)
}
}
}
private static var listeningTimePassedSinceLastSync: Double = 0.0
public static var paused: Bool {
get {
guard let player = player else {
return true
}
return player.rate == 0.0
}
set(paused) {
if paused {
self.player?.pause()
} else {
self.player?.play()
self.pausedTimer?.invalidate()
}
}
}
public static func startTickTimer() {
DispatchQueue.runOnMainQueue {
NSLog("Starting the tick timer")
playingTimer?.invalidate()
pausedTimer?.invalidate()
playingTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
self.tick()
}
}
}
public static func stopTickTimer() {
NSLog("Stopping the tick timer")
playingTimer?.invalidate()
pausedTimer?.invalidate()
playingTimer = nil
}
private static func startPausedTimer() {
guard self.paused else { return }
self.pausedTimer?.invalidate()
self.pausedTimer = Timer.scheduledTimer(timeInterval: 30, target: self, selector: #selector(syncServerProgressDuringPause), userInfo: nil, repeats: true)
}
private static func cleanupOldSessions(currentSessionId: String?) {
let realm = try! Realm()
let oldSessions = realm.objects(PlaybackSession.self) .where({ $0.isActiveSession == true })
try! realm.write {
for s in oldSessions {
if s.id != currentSessionId {
s.isActiveSession = false
}
}
}
}
public static func startPlayback(sessionId: String, playWhenReady: Bool, playbackRate: Float) { public static func startPlayback(sessionId: String, playWhenReady: Bool, playbackRate: Float) {
guard let session = Database.shared.getPlaybackSession(id: sessionId) else { return } guard let session = Database.shared.getPlaybackSession(id: sessionId) else { return }
@@ -99,26 +22,21 @@ class PlayerHandler {
player = nil player = nil
} }
// Cleanup old sessions // Cleanup and sync old sessions
cleanupOldSessions(currentSessionId: sessionId) cleanupOldSessions(currentSessionId: sessionId)
Task { await PlayerProgress.shared.syncToServer() }
// Set now playing info // Set now playing info
NowPlayingInfo.shared.setSessionMetadata(metadata: NowPlayingMetadata(id: session.id, itemId: session.libraryItemId!, artworkUrl: session.coverPath, title: session.displayTitle ?? "Unknown title", author: session.displayAuthor, series: nil)) NowPlayingInfo.shared.setSessionMetadata(metadata: NowPlayingMetadata(id: session.id, itemId: session.libraryItemId!, artworkUrl: session.coverPath, title: session.displayTitle ?? "Unknown title", author: session.displayAuthor, series: nil))
// Create the audio player // Create the audio player
player = AudioPlayer(sessionId: sessionId, playWhenReady: playWhenReady, playbackRate: playbackRate) player = AudioPlayer(sessionId: sessionId, playWhenReady: playWhenReady, playbackRate: playbackRate)
startTickTimer()
startPausedTimer()
} }
public static func stopPlayback() { public static func stopPlayback() {
// Pause playback first, so we can sync our current progress // Pause playback first, so we can sync our current progress
player?.pause() player?.pause()
// Stop updating progress before we destory the player, so we don't receive bad data
stopTickTimer()
player?.destroy() player?.destroy()
player = nil player = nil
@@ -127,6 +45,47 @@ class PlayerHandler {
NowPlayingInfo.shared.reset() NowPlayingInfo.shared.reset()
} }
public static var paused: Bool {
get {
guard let player = player else { return true }
return player.rate == 0.0
}
set(paused) {
if paused {
self.player?.pause()
} else {
self.player?.play()
}
}
}
public static var remainingSleepTime: Int? {
get {
guard let player = player else { return nil }
// Return the player time until sleep
var timeUntilSleep: Double? = nil
if let sleepTimerChapterStopTime = sleepTimerChapterStopTime {
timeUntilSleep = Double(sleepTimerChapterStopTime) - player.getCurrentTime()
} else if let stopAt = player.getSleepStopAt() {
timeUntilSleep = stopAt - player.getCurrentTime()
}
// Scale the time until sleep based on the playback rate
if let timeUntilSleep = timeUntilSleep {
// Consider paused as playing at 1x
let rate = Double(player.rate > 0 ? player.rate : 1)
let timeUntilSleepScaled = timeUntilSleep / rate
guard timeUntilSleepScaled.isNaN == false else { return nil }
return Int(timeUntilSleepScaled.rounded())
} else {
return nil
}
}
}
public static func getCurrentTime() -> Double? { public static func getCurrentTime() -> Double? {
self.player?.getCurrentTime() self.player?.getCurrentTime()
} }
@@ -135,29 +94,53 @@ class PlayerHandler {
self.player?.setPlaybackRate(speed) self.player?.setPlaybackRate(speed)
} }
public static func setSleepTime(secondsUntilSleep: Double) {
guard let player = player else { return }
let stopAt = secondsUntilSleep + player.getCurrentTime()
player.setSleepTime(stopAt: stopAt, scaleBasedOnSpeed: true)
}
public static func setChapterSleepTime(stopAt: Double) {
guard let player = player else { return }
self.sleepTimerChapterStopTime = Int(stopAt)
player.setSleepTime(stopAt: stopAt, scaleBasedOnSpeed: false)
}
public static func increaseSleepTime(increaseSeconds: Double) {
self.sleepTimerChapterStopTime = nil
self.player?.increaseSleepTime(extraTimeInSeconds: increaseSeconds)
}
public static func decreaseSleepTime(decreaseSeconds: Double) {
self.sleepTimerChapterStopTime = nil
self.player?.decreaseSleepTime(removeTimeInSeconds: decreaseSeconds)
}
public static func cancelSleepTime() {
PlayerHandler.sleepTimerChapterStopTime = nil
self.player?.removeSleepTimer()
}
public static func getPlayMethod() -> Int? { public static func getPlayMethod() -> Int? {
self.player?.getPlayMethod() self.player?.getPlayMethod()
} }
public static func getPlaybackSession() -> PlaybackSession? { public static func getPlaybackSession() -> PlaybackSession? {
guard let player = player else { return nil } guard let player = player else { return nil }
guard let session = Database.shared.getPlaybackSession(id: player.getPlaybackSessionId()) else { return nil } guard player.isInitialized() else { return nil }
return session
return Database.shared.getPlaybackSession(id: player.getPlaybackSessionId())
} }
public static func seekForward(amount: Double) { public static func seekForward(amount: Double) {
guard let player = player else { guard let player = player else { return }
return
}
let destinationTime = player.getCurrentTime() + amount let destinationTime = player.getCurrentTime() + amount
player.seek(destinationTime, from: "handler") player.seek(destinationTime, from: "handler")
} }
public static func seekBackward(amount: Double) { public static func seekBackward(amount: Double) {
guard let player = player else { guard let player = player else { return }
return
}
let destinationTime = player.getCurrentTime() - amount let destinationTime = player.getCurrentTime() - amount
player.seek(destinationTime, from: "handler") player.seek(destinationTime, from: "handler")
@@ -171,10 +154,6 @@ class PlayerHandler {
guard let player = player else { return nil } guard let player = player else { return nil }
guard player.isInitialized() else { return nil } guard player.isInitialized() else { return nil }
DispatchQueue.main.async {
syncPlayerProgress()
}
return [ return [
"duration": player.getDuration(), "duration": player.getDuration(),
"currentTime": player.getCurrentTime(), "currentTime": player.getCurrentTime(),
@@ -183,68 +162,24 @@ class PlayerHandler {
] ]
} }
private static func tick() { // MARK: - Helper logic
if !paused {
listeningTimePassedSinceLastSync += 1
if remainingSleepTime != nil { private static func cleanupOldSessions(currentSessionId: String?) {
if sleepTimerChapterStopTime != nil { do {
let timeUntilChapterEnd = Double(sleepTimerChapterStopTime ?? 0) - (getCurrentTime() ?? 0) let realm = try Realm()
if timeUntilChapterEnd <= 0 { let oldSessions = realm.objects(PlaybackSession.self) .where({
paused = true $0.isActiveSession == true && $0.serverConnectionConfigId == Store.serverConfig?.id
remainingSleepTime = nil })
} else { try realm.write {
remainingSleepTime = Int(timeUntilChapterEnd.rounded()) for s in oldSessions {
if s.id != currentSessionId {
s.isActiveSession = false
} }
} else {
if remainingSleepTime! <= 0 {
paused = true
}
remainingSleepTime! -= 1
} }
} }
} catch {
debugPrint("Failed to cleanup sessions")
debugPrint(error)
} }
if listeningTimePassedSinceLastSync >= 5 {
syncPlayerProgress()
}
}
public static func syncPlayerProgress() {
guard let player = player else { return }
guard player.isInitialized() else { return }
guard let session = getPlaybackSession() else { return }
NSLog("Syncing player progress")
// Get current time
let playerCurrentTime = player.getCurrentTime()
// Prevent multiple sync requests
let timeSinceLastSync = Date().timeIntervalSince1970 - lastSyncTime
if (lastSyncTime > 0 && timeSinceLastSync < 1) {
NSLog("syncProgress last sync time was < 1 second so not syncing")
return
}
// Prevent a sync if we got junk data from the player (occurs when exiting out of memory
guard !playerCurrentTime.isNaN else { return }
lastSyncTime = Date().timeIntervalSince1970 // seconds
session.update {
session.currentTime = playerCurrentTime
session.timeListening += listeningTimePassedSinceLastSync
session.updatedAt = Date().timeIntervalSince1970 * 1000
}
listeningTimePassedSinceLastSync = 0
// Persist items in the database and sync to the server
if session.isLocal { PlayerProgress.syncFromPlayer() }
Task { await PlayerProgress.syncToServer() }
}
@objc public static func syncServerProgressDuringPause() {
Task { await PlayerProgress.syncFromServer() }
} }
} }
+109 -22
View File
@@ -10,38 +10,91 @@ import UIKit
import RealmSwift import RealmSwift
class PlayerProgress { class PlayerProgress {
public static let shared = PlayerProgress()
private static let TIME_BETWEEN_SESSION_SYNC_IN_SECONDS = 10.0
private init() {} private init() {}
public static func syncFromPlayer() {
updateLocalMediaProgressFromLocalSession() // MARK: - SYNC HOOKS
public func syncFromPlayer(currentTime: Double, includesPlayProgress: Bool, isStopping: Bool) async {
let backgroundToken = await UIApplication.shared.beginBackgroundTask(withName: "ABS:syncFromPlayer")
do {
let session = try updateLocalSessionFromPlayer(currentTime: currentTime, includesPlayProgress: includesPlayProgress)
try updateLocalMediaProgressFromLocalSession()
if let session = session {
try await updateServerSessionFromLocalSession(session, rateLimitSync: !isStopping)
}
} catch {
debugPrint("Failed to syncFromPlayer")
debugPrint(error)
}
await UIApplication.shared.endBackgroundTask(backgroundToken)
} }
public static func syncToServer() async { public func syncToServer() async {
let backgroundToken = await UIApplication.shared.beginBackgroundTask(withName: "ABS:syncToServer") let backgroundToken = await UIApplication.shared.beginBackgroundTask(withName: "ABS:syncToServer")
updateAllServerSessionFromLocalSession() do {
try await updateAllServerSessionFromLocalSession()
} catch {
debugPrint("Failed to syncToServer")
debugPrint(error)
}
await UIApplication.shared.endBackgroundTask(backgroundToken) await UIApplication.shared.endBackgroundTask(backgroundToken)
} }
public static func syncFromServer() async { public func syncFromServer() async {
let backgroundToken = await UIApplication.shared.beginBackgroundTask(withName: "ABS:syncFromServer") let backgroundToken = await UIApplication.shared.beginBackgroundTask(withName: "ABS:syncFromServer")
await updateLocalSessionFromServerMediaProgress() do {
try await updateLocalSessionFromServerMediaProgress()
} catch {
debugPrint("Failed to syncFromServer")
debugPrint(error)
}
await UIApplication.shared.endBackgroundTask(backgroundToken) await UIApplication.shared.endBackgroundTask(backgroundToken)
} }
private static func updateLocalMediaProgressFromLocalSession() {
// MARK: - SYNC LOGIC
private func updateLocalSessionFromPlayer(currentTime: Double, includesPlayProgress: Bool) throws -> PlaybackSession? {
guard let session = PlayerHandler.getPlaybackSession() else { return nil }
guard !currentTime.isNaN else { return nil } // Prevent bad data on player stop
try session.update {
session.realm?.refresh()
let nowInSeconds = Date().timeIntervalSince1970
let nowInMilliseconds = nowInSeconds * 1000
let lastUpdateInMilliseconds = session.updatedAt ?? nowInMilliseconds
let lastUpdateInSeconds = lastUpdateInMilliseconds / 1000
let secondsSinceLastUpdate = nowInSeconds - lastUpdateInSeconds
session.currentTime = currentTime
session.updatedAt = nowInMilliseconds
if includesPlayProgress {
session.timeListening += secondsSinceLastUpdate
}
}
return session.freeze()
}
private func updateLocalMediaProgressFromLocalSession() throws {
guard let session = PlayerHandler.getPlaybackSession() else { return } guard let session = PlayerHandler.getPlaybackSession() else { return }
guard session.isLocal else { return } guard session.isLocal else { return }
let localMediaProgress = LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: session.localMediaProgressId, localLibraryItemId: session.localLibraryItem?.id, localEpisodeId: session.episodeId) let localMediaProgress = try LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: session.localMediaProgressId, localLibraryItemId: session.localLibraryItem?.id, localEpisodeId: session.episodeId)
guard let localMediaProgress = localMediaProgress else { guard let localMediaProgress = localMediaProgress else {
// Local media progress should have been created // Local media progress should have been created
// If we're here, it means a library id is invalid // If we're here, it means a library id is invalid
return return
} }
localMediaProgress.updateFromPlaybackSession(session) try localMediaProgress.updateFromPlaybackSession(session)
Database.shared.saveLocalMediaProgress(localMediaProgress)
NSLog("Local progress saved to the database") NSLog("Local progress saved to the database")
@@ -49,16 +102,46 @@ class PlayerProgress {
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.localProgress.rawValue), object: nil) NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.localProgress.rawValue), object: nil)
} }
private static func updateAllServerSessionFromLocalSession() { private func updateAllServerSessionFromLocalSession() async throws {
let sessions = try! Realm().objects(PlaybackSession.self).where({ $0.serverConnectionConfigId == Store.serverConfig?.id }) try await withThrowingTaskGroup(of: Void.self) { [self] group in
for session in sessions { for session in try await Realm().objects(PlaybackSession.self).where({ $0.serverConnectionConfigId == Store.serverConfig?.id }) {
let session = session.freeze() let session = session.freeze()
Task { await updateServerSessionFromLocalSession(session) } group.addTask {
try await self.updateServerSessionFromLocalSession(session)
}
}
try await group.waitForAll()
} }
} }
private static func updateServerSessionFromLocalSession(_ session: PlaybackSession) async { private func updateServerSessionFromLocalSession(_ session: PlaybackSession, rateLimitSync: Bool = false) async throws {
NSLog("Sending sessionId(\(session.id)) to server") var safeToSync = true
guard var session = session.thaw() else { return }
// We need to update and check the server time in a transaction for thread-safety
try session.update {
session.realm?.refresh()
let nowInMilliseconds = Date().timeIntervalSince1970 * 1000
let lastUpdateInMilliseconds = session.serverUpdatedAt
// If required, rate limit requests based on session last update
if rateLimitSync {
let timeSinceLastSync = nowInMilliseconds - lastUpdateInMilliseconds
let timeBetweenSessionSync = PlayerProgress.TIME_BETWEEN_SESSION_SYNC_IN_SECONDS * 1000
safeToSync = timeSinceLastSync > timeBetweenSessionSync
if !safeToSync {
return // This only exits the update block
}
}
session.serverUpdatedAt = nowInMilliseconds
}
session = session.freeze()
guard safeToSync else { return }
NSLog("Sending sessionId(\(session.id)) to server with currentTime(\(session.currentTime))")
var success = false var success = false
if session.isLocal { if session.isLocal {
@@ -68,16 +151,20 @@ class PlayerProgress {
success = await ApiClient.reportPlaybackProgress(report: playbackReport, sessionId: session.id) success = await ApiClient.reportPlaybackProgress(report: playbackReport, sessionId: session.id)
} }
// Remove old sessions after they synced with the server // Remove old sessions after they synced with the server
if success && !session.isActiveSession { if success && !session.isActiveSession {
NSLog("Deleting sessionId(\(session.id)) as is no longer active") if let session = session.thaw() {
session.thaw()?.delete() try session.delete()
}
} }
} }
private static func updateLocalSessionFromServerMediaProgress() async { private func updateLocalSessionFromServerMediaProgress() async throws {
NSLog("updateLocalSessionFromServerMediaProgress: Checking if local media progress was updated on server") NSLog("updateLocalSessionFromServerMediaProgress: Checking if local media progress was updated on server")
guard let session = try! await Realm().objects(PlaybackSession.self).last(where: { $0.isActiveSession == true })?.freeze() else { guard let session = try await Realm().objects(PlaybackSession.self).last(where: {
$0.isActiveSession == true && $0.serverConnectionConfigId == Store.serverConfig?.id
})?.freeze() else {
NSLog("updateLocalSessionFromServerMediaProgress: Failed to get session") NSLog("updateLocalSessionFromServerMediaProgress: Failed to get session")
return return
} }
@@ -105,7 +192,7 @@ class PlayerProgress {
if serverIsNewerThanLocal && currentTimeIsDifferent { if serverIsNewerThanLocal && currentTimeIsDifferent {
NSLog("updateLocalSessionFromServerMediaProgress: Server has newer time than local serverLastUpdate=\(serverLastUpdate) localLastUpdate=\(localLastUpdate)") NSLog("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 }
session.update { try session.update {
session.currentTime = serverCurrentTime session.currentTime = serverCurrentTime
session.updatedAt = serverLastUpdate session.updatedAt = serverLastUpdate
} }
+6 -1
View File
@@ -200,7 +200,12 @@ class ApiClient {
if let updates = response.localProgressUpdates { if let updates = response.localProgressUpdates {
for update in updates { for update in updates {
Database.shared.saveLocalMediaProgress(update) do {
try update.save()
} catch {
debugPrint("Failed to update local media progress")
debugPrint(error)
}
} }
} }
+8 -8
View File
@@ -9,15 +9,15 @@ import Foundation
import RealmSwift import RealmSwift
extension Object { extension Object {
func save() { func save() throws {
let realm = try! Realm() let realm = try Realm()
try! realm.write { try realm.write {
realm.add(self, update: .modified) realm.add(self, update: .modified)
} }
} }
func update(handler: () -> Void) { func update(handler: () -> Void) throws {
try! self.realm?.write { try self.realm?.write {
handler() handler()
} }
} }
@@ -33,12 +33,12 @@ extension EmbeddedObject {
} }
protocol Deletable { protocol Deletable {
func delete() func delete() throws
} }
extension Deletable where Self: Object { extension Deletable where Self: Object {
func delete() { func delete() throws {
try! self.realm?.write { try self.realm?.write {
self.realm?.delete(self) self.realm?.delete(self)
} }
} }
+113 -54
View File
@@ -20,29 +20,43 @@ class Database {
let realm = try! Realm() let realm = try! Realm()
let existing: ServerConnectionConfig? = realm.object(ofType: ServerConnectionConfig.self, forPrimaryKey: config.id) let existing: ServerConnectionConfig? = realm.object(ofType: ServerConnectionConfig.self, forPrimaryKey: config.id)
if config.index == 0 { if let existing = existing {
let lastConfig: ServerConnectionConfig? = realm.objects(ServerConnectionConfig.self).last do {
try existing.update {
if lastConfig != nil { existing.name = config.name
config.index = lastConfig!.index + 1 existing.address = config.address
} else { existing.userId = config.userId
config.index = 1 existing.username = config.username
} existing.token = config.token
}
do {
try realm.write {
if existing != nil {
realm.delete(existing!)
} }
realm.add(config) } catch {
NSLog("failed to update server config")
debugPrint(error)
} }
} catch(let exception) {
NSLog("failed to save server config")
debugPrint(exception)
}
setLastActiveConfigIndex(index: config.index) setLastActiveConfigIndex(index: existing.index)
} else {
if config.index == 0 {
let lastConfig: ServerConnectionConfig? = realm.objects(ServerConnectionConfig.self).last
if lastConfig != nil {
config.index = lastConfig!.index + 1
} else {
config.index = 1
}
}
do {
try realm.write {
realm.add(config)
}
} catch(let exception) {
NSLog("failed to save server config")
debugPrint(exception)
}
setLastActiveConfigIndex(index: config.index)
}
} }
public func deleteServerConnectionConfig(id: String) { public func deleteServerConnectionConfig(id: String) {
@@ -112,48 +126,83 @@ class Database {
} }
public func getLocalLibraryItems(mediaType: MediaType? = nil) -> [LocalLibraryItem] { public func getLocalLibraryItems(mediaType: MediaType? = nil) -> [LocalLibraryItem] {
let realm = try! Realm() do {
return Array(realm.objects(LocalLibraryItem.self)) let realm = try Realm()
return Array(realm.objects(LocalLibraryItem.self))
} catch {
debugPrint(error)
return []
}
} }
public func getLocalLibraryItem(byServerLibraryItemId: String) -> LocalLibraryItem? { public func getLocalLibraryItem(byServerLibraryItemId: String) -> LocalLibraryItem? {
let realm = try! Realm() do {
return realm.objects(LocalLibraryItem.self).first(where: { $0.libraryItemId == byServerLibraryItemId }) let realm = try Realm()
return realm.objects(LocalLibraryItem.self).first(where: { $0.libraryItemId == byServerLibraryItemId })
} catch {
debugPrint(error)
return nil
}
} }
public func getLocalLibraryItem(localLibraryItemId: String) -> LocalLibraryItem? { public func getLocalLibraryItem(localLibraryItemId: String) -> LocalLibraryItem? {
let realm = try! Realm() do {
return realm.object(ofType: LocalLibraryItem.self, forPrimaryKey: localLibraryItemId) let realm = try Realm()
return realm.object(ofType: LocalLibraryItem.self, forPrimaryKey: localLibraryItemId)
} catch {
debugPrint(error)
return nil
}
} }
public func saveLocalLibraryItem(localLibraryItem: LocalLibraryItem) { public func saveLocalLibraryItem(localLibraryItem: LocalLibraryItem) throws {
let realm = try! Realm() let realm = try Realm()
try! realm.write { realm.add(localLibraryItem, update: .modified) } try realm.write { realm.add(localLibraryItem, update: .modified) }
} }
public func getLocalFile(localFileId: String) -> LocalFile? { public func getLocalFile(localFileId: String) -> LocalFile? {
let realm = try! Realm() do {
return realm.object(ofType: LocalFile.self, forPrimaryKey: localFileId) let realm = try Realm()
return realm.object(ofType: LocalFile.self, forPrimaryKey: localFileId)
} catch {
debugPrint(error)
return nil
}
} }
public func getDownloadItem(downloadItemId: String) -> DownloadItem? { public func getDownloadItem(downloadItemId: String) -> DownloadItem? {
let realm = try! Realm() do {
return realm.object(ofType: DownloadItem.self, forPrimaryKey: downloadItemId) let realm = try Realm()
return realm.object(ofType: DownloadItem.self, forPrimaryKey: downloadItemId)
} catch {
debugPrint(error)
return nil
}
} }
public func getDownloadItem(libraryItemId: String) -> DownloadItem? { public func getDownloadItem(libraryItemId: String) -> DownloadItem? {
let realm = try! Realm() do {
return realm.objects(DownloadItem.self).filter("libraryItemId == %@", libraryItemId).first let realm = try Realm()
return realm.objects(DownloadItem.self).filter("libraryItemId == %@", libraryItemId).first
} catch {
debugPrint(error)
return nil
}
} }
public func getDownloadItem(downloadItemPartId: String) -> DownloadItem? { public func getDownloadItem(downloadItemPartId: String) -> DownloadItem? {
let realm = try! Realm() do {
return realm.objects(DownloadItem.self).filter("SUBQUERY(downloadItemParts, $part, $part.id == %@) .@count > 0", downloadItemPartId).first let realm = try Realm()
return realm.objects(DownloadItem.self).filter("SUBQUERY(downloadItemParts, $part, $part.id == %@) .@count > 0", downloadItemPartId).first
} catch {
debugPrint(error)
return nil
}
} }
public func saveDownloadItem(_ downloadItem: DownloadItem) { public func saveDownloadItem(_ downloadItem: DownloadItem) throws {
let realm = try! Realm() let realm = try Realm()
return try! realm.write { realm.add(downloadItem, update: .modified) } return try realm.write { realm.add(downloadItem, update: .modified) }
} }
public func getDeviceSettings() -> DeviceSettings { public func getDeviceSettings() -> DeviceSettings {
@@ -162,31 +211,41 @@ class Database {
} }
public func getAllLocalMediaProgress() -> [LocalMediaProgress] { public func getAllLocalMediaProgress() -> [LocalMediaProgress] {
let realm = try! Realm() do {
return Array(realm.objects(LocalMediaProgress.self)) let realm = try Realm()
} return Array(realm.objects(LocalMediaProgress.self))
} catch {
public func saveLocalMediaProgress(_ mediaProgress: LocalMediaProgress) { debugPrint(error)
let realm = try! Realm() return []
try! realm.write { realm.add(mediaProgress, update: .modified) } }
} }
// For books this will just be the localLibraryItemId for podcast episodes this will be "{localLibraryItemId}-{episodeId}" // For books this will just be the localLibraryItemId for podcast episodes this will be "{localLibraryItemId}-{episodeId}"
public func getLocalMediaProgress(localMediaProgressId: String) -> LocalMediaProgress? { public func getLocalMediaProgress(localMediaProgressId: String) -> LocalMediaProgress? {
let realm = try! Realm() do {
return realm.object(ofType: LocalMediaProgress.self, forPrimaryKey: localMediaProgressId) let realm = try Realm()
return realm.object(ofType: LocalMediaProgress.self, forPrimaryKey: localMediaProgressId)
} catch {
debugPrint(error)
return nil
}
} }
public func removeLocalMediaProgress(localMediaProgressId: String) { public func removeLocalMediaProgress(localMediaProgressId: String) throws {
let realm = try! Realm() let realm = try Realm()
try! realm.write { try realm.write {
let progress = realm.object(ofType: LocalMediaProgress.self, forPrimaryKey: localMediaProgressId) let progress = realm.object(ofType: LocalMediaProgress.self, forPrimaryKey: localMediaProgressId)
realm.delete(progress!) realm.delete(progress!)
} }
} }
public func getPlaybackSession(id: String) -> PlaybackSession? { public func getPlaybackSession(id: String) -> PlaybackSession? {
let realm = try! Realm() do {
return realm.object(ofType: PlaybackSession.self, forPrimaryKey: id) let realm = try Realm()
return realm.object(ofType: PlaybackSession.self, forPrimaryKey: id)
} catch {
debugPrint(error)
return nil
}
} }
} }
+10 -5
View File
@@ -59,12 +59,17 @@ class NowPlayingInfo {
} }
} }
public func update(duration: Double, currentTime: Double, rate: Float) { public func update(duration: Double, currentTime: Double, rate: Float) {
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration // Update on the main to prevent access collisions
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime DispatchQueue.main.async { [weak self] in
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = rate if let self = self {
nowPlayingInfo[MPNowPlayingInfoPropertyDefaultPlaybackRate] = 1.0 self.nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration
self.nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime
self.nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = rate
self.nowPlayingInfo[MPNowPlayingInfoPropertyDefaultPlaybackRate] = 1.0
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo MPNowPlayingInfoCenter.default().nowPlayingInfo = self.nowPlayingInfo
}
}
} }
public func reset() { public func reset() {
+9 -5
View File
@@ -9,10 +9,17 @@ import Foundation
import RealmSwift import RealmSwift
class Store { class Store {
private static var _serverConfig: ServerConnectionConfig?
public static var serverConfig: ServerConnectionConfig? { public static var serverConfig: ServerConnectionConfig? {
get { get {
return _serverConfig do {
// Fetch each time, as holding onto a live or frozen realm object is bad
let index = Database.shared.getLastActiveConfigIndex()
let realm = try Realm()
return realm.objects(ServerConnectionConfig.self).first(where: { $0.index == index })
} catch {
debugPrint(error)
return nil
}
} }
set(updated) { set(updated) {
if updated != nil { if updated != nil {
@@ -20,9 +27,6 @@ class Store {
} else { } else {
Database.shared.setLastActiveConfigIndexToNil() Database.shared.setLastActiveConfigIndexToNil()
} }
// Make safe for accessing on all threads
_serverConfig = updated?.freeze()
} }
} }
} }