mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-26 05:14:09 +02:00
Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
guard let localLibraryItem = Database.shared.getLocalLibraryItem(localLibraryItemId: localLibraryItemId) else { return nil }
|
||||||
|
let episode = localLibraryItem.getPodcastEpisode(episodeId: localEpisodeId)
|
||||||
|
let progress = LocalMediaProgress(localLibraryItem: localLibraryItem, episode: episode)
|
||||||
|
realm.add(progress)
|
||||||
|
return progress
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let localLibraryItemId = localLibraryItemId {
|
|
||||||
guard let localLibraryItem = Database.shared.getLocalLibraryItem(localLibraryItemId: localLibraryItemId) else { return nil }
|
|
||||||
let episode = localLibraryItem.getPodcastEpisode(episodeId: localEpisodeId)
|
|
||||||
return LocalMediaProgress(localLibraryItem: localLibraryItem, episode: episode)
|
|
||||||
} 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
private static func cleanupOldSessions(currentSessionId: String?) {
|
||||||
|
do {
|
||||||
if remainingSleepTime != nil {
|
let realm = try Realm()
|
||||||
if sleepTimerChapterStopTime != nil {
|
let oldSessions = realm.objects(PlaybackSession.self) .where({
|
||||||
let timeUntilChapterEnd = Double(sleepTimerChapterStopTime ?? 0) - (getCurrentTime() ?? 0)
|
$0.isActiveSession == true && $0.serverConnectionConfigId == Store.serverConfig?.id
|
||||||
if timeUntilChapterEnd <= 0 {
|
})
|
||||||
paused = true
|
try realm.write {
|
||||||
remainingSleepTime = nil
|
for s in oldSessions {
|
||||||
} else {
|
if s.id != currentSessionId {
|
||||||
remainingSleepTime = Int(timeUntilChapterEnd.rounded())
|
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() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
setLastActiveConfigIndex(index: existing.index)
|
||||||
debugPrint(exception)
|
} 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
self.nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = rate
|
||||||
|
self.nowPlayingInfo[MPNowPlayingInfoPropertyDefaultPlaybackRate] = 1.0
|
||||||
|
|
||||||
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = self.nowPlayingInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func reset() {
|
public func reset() {
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user