mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-08-03 17:44:51 +02:00
Sync offline playback progress
This commit is contained in:
parent
7c5ee940d3
commit
6aa0f2253b
4 changed files with 73 additions and 26 deletions
|
@ -11,7 +11,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
// Override point for customization after application launch.
|
||||
|
||||
let configuration = Realm.Configuration(
|
||||
schemaVersion: 2,
|
||||
schemaVersion: 3,
|
||||
migrationBlock: { migration, oldSchemaVersion in
|
||||
if (oldSchemaVersion < 1) {
|
||||
NSLog("Realm schema version was \(oldSchemaVersion)")
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import Foundation
|
||||
import RealmSwift
|
||||
|
||||
class PlaybackSession: Object, Codable {
|
||||
class PlaybackSession: Object, Codable, Deletable {
|
||||
@Persisted(primaryKey: true) var id: String = ""
|
||||
var userId: String?
|
||||
@Persisted var libraryItemId: String?
|
||||
|
@ -20,7 +20,7 @@ class PlaybackSession: Object, Codable {
|
|||
@Persisted var displayAuthor: String?
|
||||
@Persisted var coverPath: String?
|
||||
@Persisted var duration: Double = 0
|
||||
@Persisted var playMethod: Int = 1
|
||||
@Persisted var playMethod: Int = PlayMethod.directplay.rawValue
|
||||
@Persisted var startedAt: Double?
|
||||
@Persisted var updatedAt: Double?
|
||||
@Persisted var timeListening: Double = 0
|
||||
|
@ -30,8 +30,24 @@ class PlaybackSession: Object, Codable {
|
|||
@Persisted var localLibraryItem: LocalLibraryItem?
|
||||
@Persisted var serverConnectionConfigId: String?
|
||||
@Persisted var serverAddress: String?
|
||||
@Persisted var isActiveSession = true
|
||||
|
||||
var isLocal: Bool { self.localLibraryItem != nil }
|
||||
var mediaPlayer: String { "AVPlayer" }
|
||||
var deviceInfo: [String: String?]? {
|
||||
var systemInfo = utsname()
|
||||
uname(&systemInfo)
|
||||
let modelCode = withUnsafePointer(to: &systemInfo.machine) {
|
||||
$0.withMemoryRebound(to: CChar.self, capacity: 1) {
|
||||
ptr in String.init(validatingUTF8: ptr)
|
||||
}
|
||||
}
|
||||
return [
|
||||
"manufacturer": "Apple",
|
||||
"model": modelCode,
|
||||
"clientVersion": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
|
||||
]
|
||||
}
|
||||
|
||||
var localMediaProgressId: String? {
|
||||
if let localLibraryItem = localLibraryItem, let episodeId = episodeId {
|
||||
|
@ -72,10 +88,11 @@ class PlaybackSession: Object, Codable {
|
|||
self.localLibraryItem = localLibraryItem
|
||||
self.serverConnectionConfigId = serverConnectionConfigId
|
||||
self.serverAddress = serverAddress
|
||||
self.isActiveSession = true
|
||||
}
|
||||
|
||||
private enum CodingKeys : String, CodingKey {
|
||||
case id, userId, libraryItemId, episodeId, mediaType, chapters, displayTitle, displayAuthor, coverPath, duration, playMethod, startedAt, updatedAt, timeListening, audioTracks, currentTime, libraryItem, localLibraryItem, serverConnectionConfigId, serverAddress, isLocal, localMediaProgressId
|
||||
case id, userId, libraryItemId, episodeId, mediaType, chapters, displayTitle, displayAuthor, coverPath, duration, playMethod, mediaPlayer, deviceInfo, startedAt, updatedAt, timeListening, audioTracks, currentTime, libraryItem, localLibraryItem, serverConnectionConfigId, serverAddress, isLocal, localMediaProgressId
|
||||
}
|
||||
|
||||
override init() {}
|
||||
|
@ -107,6 +124,7 @@ class PlaybackSession: Object, Codable {
|
|||
localLibraryItem = try values.decodeIfPresent(LocalLibraryItem.self, forKey: .localLibraryItem)
|
||||
serverConnectionConfigId = try values.decodeIfPresent(String.self, forKey: .serverConnectionConfigId)
|
||||
serverAddress = try values.decodeIfPresent(String.self, forKey: .serverAddress)
|
||||
isActiveSession = true
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
|
@ -122,6 +140,8 @@ class PlaybackSession: Object, Codable {
|
|||
try container.encode(coverPath, forKey: .coverPath)
|
||||
try container.encode(duration, forKey: .duration)
|
||||
try container.encode(playMethod, forKey: .playMethod)
|
||||
try container.encode(mediaPlayer, forKey: .mediaPlayer)
|
||||
try container.encode(deviceInfo, forKey: .deviceInfo)
|
||||
try container.encode(startedAt, forKey: .startedAt)
|
||||
try container.encode(updatedAt, forKey: .updatedAt)
|
||||
try container.encode(timeListening, forKey: .timeListening)
|
||||
|
|
|
@ -34,7 +34,6 @@ class PlayerHandler {
|
|||
}
|
||||
}
|
||||
private static var listeningTimePassedSinceLastSync: Double = 0.0
|
||||
private static var lastSyncReport: PlaybackReport?
|
||||
|
||||
public static var paused: Bool {
|
||||
get {
|
||||
|
@ -69,15 +68,35 @@ class PlayerHandler {
|
|||
timer = nil
|
||||
}
|
||||
|
||||
private static func cleanupOldSessions(currentSessionId: String?) {
|
||||
if let currentSessionId = currentSessionId {
|
||||
let realm = try! Realm()
|
||||
let oldSessions = realm.objects(PlaybackSession.self)
|
||||
.where({ $0.isActiveSession == true && $0.id != currentSessionId })
|
||||
try! realm.write {
|
||||
for s in oldSessions {
|
||||
s.isActiveSession = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func startPlayback(sessionId: String, playWhenReady: Bool, playbackRate: Float) {
|
||||
guard let session = Database.shared.getPlaybackSession(id: sessionId) else { return }
|
||||
|
||||
// Clean up the existing player
|
||||
if player != nil {
|
||||
player?.destroy()
|
||||
player = nil
|
||||
}
|
||||
|
||||
// Cleanup old sessions
|
||||
cleanupOldSessions(currentSessionId: sessionId)
|
||||
|
||||
// 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))
|
||||
|
||||
// Create the audio player
|
||||
player = AudioPlayer(sessionId: sessionId, playWhenReady: playWhenReady, playbackRate: playbackRate)
|
||||
|
||||
startTickTimer()
|
||||
|
@ -93,6 +112,8 @@ class PlayerHandler {
|
|||
player?.destroy()
|
||||
player = nil
|
||||
|
||||
cleanupOldSessions(currentSessionId: nil)
|
||||
|
||||
NowPlayingInfo.shared.reset()
|
||||
}
|
||||
|
||||
|
@ -180,10 +201,8 @@ class PlayerHandler {
|
|||
guard let player = player else { return }
|
||||
guard let session = getPlaybackSession() else { return }
|
||||
|
||||
// Prevent a sync at the current time
|
||||
// Get current time
|
||||
let playerCurrentTime = player.getCurrentTime()
|
||||
let hasSyncAtCurrentTime = lastSyncReport?.currentTime.isEqual(to: playerCurrentTime) ?? false
|
||||
if hasSyncAtCurrentTime { return }
|
||||
|
||||
// Prevent multiple sync requests
|
||||
let timeSinceLastSync = Date().timeIntervalSince1970 - lastSyncTime
|
||||
|
@ -196,29 +215,18 @@ class PlayerHandler {
|
|||
guard !playerCurrentTime.isNaN else { return }
|
||||
|
||||
lastSyncTime = Date().timeIntervalSince1970 // seconds
|
||||
let report = PlaybackReport(currentTime: playerCurrentTime, duration: player.getDuration(), timeListened: listeningTimePassedSinceLastSync)
|
||||
|
||||
session.update {
|
||||
session.currentTime = playerCurrentTime
|
||||
session.timeListening += listeningTimePassedSinceLastSync
|
||||
}
|
||||
listeningTimePassedSinceLastSync = 0
|
||||
lastSyncReport = report
|
||||
|
||||
let sessionIsLocal = session.isLocal
|
||||
if !sessionIsLocal {
|
||||
if Connectivity.isConnectedToInternet {
|
||||
NSLog("sending playback report")
|
||||
ApiClient.reportPlaybackProgress(report: report, sessionId: session.id)
|
||||
}
|
||||
} else {
|
||||
if let localMediaProgress = syncLocalProgress() {
|
||||
if Connectivity.isConnectedToInternet {
|
||||
ApiClient.reportLocalMediaProgress(localMediaProgress) { success in
|
||||
NSLog("Synced local media progress: \(success)")
|
||||
}
|
||||
}
|
||||
}
|
||||
// Persist items in the database and sync to the server
|
||||
if session.isLocal {
|
||||
_ = syncLocalProgress()
|
||||
}
|
||||
syncPlaybackSessionsToServer()
|
||||
}
|
||||
|
||||
private static func syncLocalProgress() -> LocalMediaProgress? {
|
||||
|
@ -241,4 +249,23 @@ class PlayerHandler {
|
|||
|
||||
return localMediaProgress
|
||||
}
|
||||
|
||||
private static func syncPlaybackSessionsToServer() {
|
||||
guard Connectivity.isConnectedToInternet else { return }
|
||||
DispatchQueue.global(qos: .utility).async {
|
||||
let realm = try! Realm()
|
||||
for session in realm.objects(PlaybackSession.self) {
|
||||
NSLog("Sending sessionId(\(session.id)) to server")
|
||||
let sessionRef = ThreadSafeReference(to: session)
|
||||
ApiClient.reportLocalPlaybackProgress(session.freeze()) { success in
|
||||
// Remove old sessions after they synced with the server
|
||||
let session = try! Realm().resolve(sessionRef)
|
||||
if success && !(session?.isActiveSession ?? false) {
|
||||
NSLog("Deleting sessionId(\(session!.id)) as is no longer active")
|
||||
session?.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -166,8 +166,8 @@ class ApiClient {
|
|||
try? postResource(endpoint: "api/session/\(sessionId)/sync", parameters: report.asDictionary().mapValues({ value in "\(value)" }), callback: nil)
|
||||
}
|
||||
|
||||
public static func reportLocalMediaProgress(_ localMediaProgress: LocalMediaProgress, callback: @escaping (_ success: Bool) -> Void) {
|
||||
let progress = localMediaProgress.freeze()
|
||||
public static func reportLocalPlaybackProgress(_ session: PlaybackSession, callback: @escaping (_ success: Bool) -> Void) {
|
||||
let progress = session.freeze()
|
||||
postResource(endpoint: "api/session/local", parameters: progress, callback: callback)
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue