diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt b/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt index 7962ad69..1bf80475 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt @@ -19,130 +19,6 @@ data class DeviceData( var localLibraryItemIdPlaying:String? ) -@JsonIgnoreProperties(ignoreUnknown = true) -data class LocalLibraryItem( - var id:String, - var serverAddress:String?, - var libraryItemId:String?, - var folderId:String, - var basePath:String, - var absolutePath:String, - var contentUrl:String, - var isInvalid:Boolean, - var mediaType:String, - var media:MediaType, - var localFiles:MutableList, - var coverContentUrl:String?, - var coverAbsolutePath:String?, - var isLocal:Boolean -) { - @JsonIgnore - fun getDuration():Double { - var total = 0.0 - var audioTracks = media.getAudioTracks() - audioTracks.forEach{ total += it.duration } - return total - } - - @JsonIgnore - fun updateFromScan(audioTracks:MutableList, _localFiles:MutableList) { - media.setAudioTracks(audioTracks) - localFiles = _localFiles - - if (coverContentUrl != null) { - if (localFiles.find { it.contentUrl == coverContentUrl } == null) { - // Cover was removed - coverContentUrl = null - coverAbsolutePath = null - media.coverPath = null - } - } - } - - @JsonIgnore - fun getPlaybackSession():PlaybackSession { - var sessionId = "play-${UUID.randomUUID()}" - - var mediaMetadata = media.metadata - var chapters = if (mediaType == "book") (media as Book).chapters else mutableListOf() - var authorName = "Unknown" - if (mediaType == "book") { - var bookMetadata = mediaMetadata as BookMetadata - authorName = bookMetadata?.authorName ?: "Unknown" - } - return PlaybackSession(sessionId,null,null,null, mediaType, mediaMetadata, chapters, mediaMetadata.title, authorName,null,getDuration(),PLAYMETHOD_LOCAL, media.getAudioTracks() as MutableList,0.0,null,this,null,null) - } - - @JsonIgnore - fun removeLocalFile(localFileId:String) { - localFiles.removeIf { it.id == localFileId } - } -} - -@JsonIgnoreProperties(ignoreUnknown = true) -data class LocalMediaItem( - var id:String, - var serverAddress:String?, - var name: String, - var mediaType:String, - var folderId:String, - var contentUrl:String, - var simplePath: String, - var basePath:String, - var absolutePath:String, - var audioTracks:MutableList, - var localFiles:MutableList, - var coverContentUrl:String?, - var coverAbsolutePath:String? -) { - - @JsonIgnore - fun getDuration():Double { - var total = 0.0 - audioTracks.forEach{ total += it.duration } - return total - } - - @JsonIgnore - fun getTotalSize():Long { - var total = 0L - localFiles.forEach { total += it.size } - return total - } - - @JsonIgnore - fun getMediaMetadata():MediaTypeMetadata { - return if (mediaType == "book") { - BookMetadata(name,null, mutableListOf(), mutableListOf(), mutableListOf(),null,null,null,null,null,null,null,false,null,null,null,null) - } else { - PodcastMetadata(name,null,null, mutableListOf()) - } - } - - @JsonIgnore - fun getAudiobookChapters():List { - if (mediaType != "book" || audioTracks.isEmpty()) return mutableListOf() - if (audioTracks.size == 1) { // Single track audiobook look for chapters from ffprobe - return audioTracks[0].audioProbeResult?.getBookChapters() ?: mutableListOf() - } - // Multi-track make chapters from tracks - return audioTracks.map { it.getBookChapter() } - } - - @JsonIgnore - fun getLocalLibraryItem():LocalLibraryItem { - var mediaMetadata = getMediaMetadata() - if (mediaType == "book") { - var chapters = getAudiobookChapters() - var book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,getTotalSize(),getDuration()) - return LocalLibraryItem(id,serverAddress, null, folderId, basePath,absolutePath, contentUrl, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true) - } else { - var podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false) - return LocalLibraryItem(id,serverAddress, null, folderId, basePath,absolutePath, contentUrl, false, mediaType, podcast,localFiles,coverContentUrl, coverAbsolutePath, true) - } - } -} - @JsonIgnoreProperties(ignoreUnknown = true) data class LocalFile( var id:String, diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt b/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt new file mode 100644 index 00000000..1eb8602e --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt @@ -0,0 +1,65 @@ +package com.audiobookshelf.app.data + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import java.util.* + +@JsonIgnoreProperties(ignoreUnknown = true) +data class LocalLibraryItem( + var id:String, + var serverAddress:String?, + var libraryItemId:String?, + var folderId:String, + var basePath:String, + var absolutePath:String, + var contentUrl:String, + var isInvalid:Boolean, + var mediaType:String, + var media:MediaType, + var localFiles:MutableList, + var coverContentUrl:String?, + var coverAbsolutePath:String?, + var isLocal:Boolean +) { + @JsonIgnore + fun getDuration():Double { + var total = 0.0 + var audioTracks = media.getAudioTracks() + audioTracks.forEach{ total += it.duration } + return total + } + + @JsonIgnore + fun updateFromScan(audioTracks:MutableList, _localFiles:MutableList) { + media.setAudioTracks(audioTracks) + localFiles = _localFiles + + if (coverContentUrl != null) { + if (localFiles.find { it.contentUrl == coverContentUrl } == null) { + // Cover was removed + coverContentUrl = null + coverAbsolutePath = null + media.coverPath = null + } + } + } + + @JsonIgnore + fun getPlaybackSession():PlaybackSession { + var sessionId = "play-${UUID.randomUUID()}" + + var mediaMetadata = media.metadata + var chapters = if (mediaType == "book") (media as Book).chapters else mutableListOf() + var authorName = "Unknown" + if (mediaType == "book") { + var bookMetadata = mediaMetadata as BookMetadata + authorName = bookMetadata?.authorName ?: "Unknown" + } + return PlaybackSession(sessionId,null,null,null, mediaType, mediaMetadata, chapters, mediaMetadata.title, authorName,null,getDuration(),PLAYMETHOD_LOCAL, media.getAudioTracks() as MutableList,0.0,null,this,null,null) + } + + @JsonIgnore + fun removeLocalFile(localFileId:String) { + localFiles.removeIf { it.id == localFileId } + } +} diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaItem.kt b/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaItem.kt new file mode 100644 index 00000000..10639cbc --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaItem.kt @@ -0,0 +1,72 @@ +package com.audiobookshelf.app.data + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonIgnoreProperties + +/* + Used as a helper class to generate LocalLibraryItem from scan results + */ + +@JsonIgnoreProperties(ignoreUnknown = true) +data class LocalMediaItem( + var id:String, + var serverAddress:String?, + var name: String, + var mediaType:String, + var folderId:String, + var contentUrl:String, + var simplePath: String, + var basePath:String, + var absolutePath:String, + var audioTracks:MutableList, + var localFiles:MutableList, + var coverContentUrl:String?, + var coverAbsolutePath:String? +) { + + @JsonIgnore + fun getDuration():Double { + var total = 0.0 + audioTracks.forEach{ total += it.duration } + return total + } + + @JsonIgnore + fun getTotalSize():Long { + var total = 0L + localFiles.forEach { total += it.size } + return total + } + + @JsonIgnore + fun getMediaMetadata():MediaTypeMetadata { + return if (mediaType == "book") { + BookMetadata(name,null, mutableListOf(), mutableListOf(), mutableListOf(),null,null,null,null,null,null,null,false,null,null,null,null) + } else { + PodcastMetadata(name,null,null, mutableListOf()) + } + } + + @JsonIgnore + fun getAudiobookChapters():List { + if (mediaType != "book" || audioTracks.isEmpty()) return mutableListOf() + if (audioTracks.size == 1) { // Single track audiobook look for chapters from ffprobe + return audioTracks[0].audioProbeResult?.getBookChapters() ?: mutableListOf() + } + // Multi-track make chapters from tracks + return audioTracks.map { it.getBookChapter() } + } + + @JsonIgnore + fun getLocalLibraryItem():LocalLibraryItem { + var mediaMetadata = getMediaMetadata() + if (mediaType == "book") { + var chapters = getAudiobookChapters() + var book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,getTotalSize(),getDuration()) + return LocalLibraryItem(id,serverAddress, null, folderId, basePath,absolutePath, contentUrl, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true) + } else { + var podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false) + return LocalLibraryItem(id,serverAddress, null, folderId, basePath,absolutePath, contentUrl, false, mediaType, podcast,localFiles,coverContentUrl, coverAbsolutePath, true) + } + } +} diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/MediaProgress.kt b/android/app/src/main/java/com/audiobookshelf/app/data/MediaProgress.kt new file mode 100644 index 00000000..37185a4b --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/data/MediaProgress.kt @@ -0,0 +1,18 @@ +package com.audiobookshelf.app.data + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties + +@JsonIgnoreProperties(ignoreUnknown = true) +data class MediaProgress( + val id:String, + val libraryItemId:String, + val episodeId:String, + val duration:Double, + val progress:Double, // 0 to 1 + val currentTime:Int, + val isFinished:Boolean, + val lastUpdate:Long, + val startedAt:Long, + val finishedAt:Long, + val isLocal:Boolean? +) diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt b/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt index c3116867..3978b591 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt @@ -127,4 +127,9 @@ class PlaybackSession( } return mediaItems } + + @JsonIgnore + fun clone():PlaybackSession { + return PlaybackSession(id,userId,libraryItemId,episodeId,mediaType,mediaMetadata,chapters,displayTitle,displayAuthor,coverPath,duration,playMethod,audioTracks,currentTime,libraryItem,localLibraryItem,serverUrl,token) + } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/AudiobookProgressSyncer.kt b/android/app/src/main/java/com/audiobookshelf/app/player/AudiobookProgressSyncer.kt deleted file mode 100644 index cff536f3..00000000 --- a/android/app/src/main/java/com/audiobookshelf/app/player/AudiobookProgressSyncer.kt +++ /dev/null @@ -1,184 +0,0 @@ -package com.audiobookshelf.app.player - -import android.os.Handler -import android.os.Looper -import android.util.Log -import com.audiobookshelf.app.device.DeviceManager -import com.getcapacitor.JSObject -import okhttp3.* -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.RequestBody.Companion.toRequestBody -import java.io.IOException -import java.util.* -import kotlin.concurrent.schedule - -/* - * Normal progress sync is handled in webview, but when using android auto webview may not be open. - * If webview is not open sync progress every 5s. Webview can be closed at any time so interval is always set. - */ -class AudiobookProgressSyncer constructor(playerNotificationService:PlayerNotificationService, client: OkHttpClient) { - private val tag = "AudiobookProgressSync" - private val playerNotificationService:PlayerNotificationService = playerNotificationService - private val client:OkHttpClient = client - - private var listeningTimerTask: TimerTask? = null - var listeningTimerRunning:Boolean = false - - private var webviewOpenOnStart:Boolean = false - private var webviewClosedMidSession:Boolean = false - private var listeningBookTitle:String? = "" - private var listeningBookIsLocal:Boolean = false - private var listeningBookId:String? = "" - private var listeningStreamId:String? = "" - - private var lastPlaybackTime:Long = 0 - private var lastUpdateTime:Long = 0 - - fun start() { - if (listeningTimerRunning) { - Log.d(tag, "start: Timer already running for $listeningBookTitle") - if (playerNotificationService.getCurrentBookTitle() != listeningBookTitle) { - Log.d(tag, "start: Changed audiobook stream - resetting timer") - listeningTimerTask?.cancel() - } - } - listeningTimerRunning = true - - webviewOpenOnStart = playerNotificationService.getIsWebviewOpen() - listeningBookTitle = playerNotificationService.getCurrentBookTitle() - listeningBookIsLocal = false - listeningBookId = "empty" - listeningStreamId = "empty" - - lastPlaybackTime = playerNotificationService.getCurrentTime() - lastUpdateTime = System.currentTimeMillis() / 1000L - - listeningTimerTask = Timer("ListeningTimer", false).schedule(0L, 5000L) { - Handler(Looper.getMainLooper()).post() { - // Webview was closed while android auto is open - switch to native sync - var isWebviewOpen = playerNotificationService.getIsWebviewOpen() - if (!isWebviewOpen && webviewOpenOnStart) { - Log.d(tag, "Listening Timer: webview closed Switching to native sync tracking") - webviewOpenOnStart = false - webviewClosedMidSession = true - lastUpdateTime = System.currentTimeMillis() / 1000L - } else if (isWebviewOpen && webviewClosedMidSession) { - Log.d(tag, "Listening Timer: webview re-opened Switching back to webview sync tracking") - webviewClosedMidSession = false - webviewOpenOnStart = true - lastUpdateTime = System.currentTimeMillis() / 1000L - } - if (!webviewOpenOnStart && playerNotificationService.currentPlayer.isPlaying) { - sync() - } - } - } - } - - fun stop() { - if (!listeningTimerRunning) return - Log.d(tag, "stop: Stopping listening for $listeningBookTitle") - - if (!webviewOpenOnStart) { - sync() - } - reset() - } - - fun reset() { - listeningTimerTask?.cancel() - listeningTimerTask = null - listeningTimerRunning = false - listeningBookTitle = "" - listeningBookId = "" - listeningBookIsLocal = false - listeningStreamId = "" - } - - fun sync() { - var currTime = System.currentTimeMillis() / 1000L - var elapsed = currTime - lastUpdateTime - lastUpdateTime = currTime - - if (!listeningBookIsLocal) { - Log.d(tag, "ListeningTimer: Sending sync data to server: elapsed $elapsed | $listeningStreamId | $listeningBookId") - - // Send sync data only for streaming books - var syncData: JSObject = JSObject() - syncData.put("timeListened", elapsed) - syncData.put("currentTime", playerNotificationService.getCurrentTime() / 1000) - syncData.put("streamId", listeningStreamId) - syncData.put("audiobookId", listeningBookId) - sendStreamSyncData(syncData) { - Log.d(tag, "Stream sync done") - } - } else if (listeningStreamId == "download") { - // TODO: Save downloaded audiobook progress & send to server if connected - Log.d(tag, "ListeningTimer: Is listening download") - - // Send sync data only for local books - var syncData: JSObject = JSObject() -// var duration = playerNotificationService.getAudiobookDuration() / 1000 - var duration = 1000 - var currentTime = playerNotificationService.getCurrentTime() / 1000 - syncData.put("totalDuration", duration) - syncData.put("currentTime", currentTime) - syncData.put("progress", if (duration > 0) (currentTime / duration) else 0) - syncData.put("isRead", false) - syncData.put("lastUpdate", System.currentTimeMillis()) - syncData.put("audiobookId", listeningBookId) - sendLocalSyncData(syncData) { - Log.d(tag, "Local sync done") - } - } - } - - fun sendLocalSyncData(payload:JSObject, cb: (() -> Unit)) { - var serverUrl = DeviceManager.serverAddress - var token = DeviceManager.token - - if (serverUrl == "" || token == "") { - return - } - - Log.d(tag, "Sync Local $serverUrl | $token") - var url = "$serverUrl/api/syncLocal" - sendServerRequest(url, token, payload, cb) - } - - fun sendStreamSyncData(payload:JSObject, cb: (() -> Unit)) { - var serverUrl = DeviceManager.serverAddress - var token = DeviceManager.token - - if (serverUrl == "" || token == "") { - return - } - - Log.d(tag, "Sync Stream $serverUrl | $token") - var url = "$serverUrl/api/syncStream" - sendServerRequest(url, token, payload, cb) - } - - fun sendServerRequest(url:String, token:String, payload:JSObject, cb: () -> Unit) { - val mediaType = "application/json; charset=utf-8".toMediaType() - val requestBody = payload.toString().toRequestBody(mediaType) - val request = Request.Builder().post(requestBody) - .url(url).addHeader("Authorization", "Bearer $token") - .build() - - client.newCall(request).enqueue(object : Callback { - override fun onFailure(call: Call, e: IOException) { - Log.d(tag, "FAILURE TO CONNECT") - e.printStackTrace() - cb() - } - - override fun onResponse(call: Call, response: Response) { - response.use { - if (!response.isSuccessful) throw IOException("Unexpected code $response") - cb() - } - } - }) - } -} diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/MediaProgressSyncer.kt b/android/app/src/main/java/com/audiobookshelf/app/player/MediaProgressSyncer.kt new file mode 100644 index 00000000..5aa4016d --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/player/MediaProgressSyncer.kt @@ -0,0 +1,96 @@ +package com.audiobookshelf.app.player + +import android.os.Handler +import android.os.Looper +import android.util.Log +import com.audiobookshelf.app.data.MediaProgress +import com.audiobookshelf.app.data.PlaybackSession +import com.audiobookshelf.app.server.ApiHandler +import java.util.* +import kotlin.concurrent.schedule + +data class MediaProgressSyncData( + var timeListened:Long, // seconds + var duration:Double, // seconds + var currentTime:Double // seconds +) + +class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, apiHandler: ApiHandler) { + private val tag = "MediaProgressSync" + private val playerNotificationService:PlayerNotificationService = playerNotificationService + private val apiHandler = apiHandler + + private var listeningTimerTask: TimerTask? = null + var listeningTimerRunning:Boolean = false + + private var lastSyncTime:Long = 0 + + var currentPlaybackSession: PlaybackSession? = null // copy of pb session currently syncing +// var currentMediaProgress: MediaProgress? = null + + val currentDisplayTitle get() = currentPlaybackSession?.displayTitle ?: "Unset" + val currentIsLocal get() = currentPlaybackSession?.isLocal == true + val currentSessionId get() = currentPlaybackSession?.id ?: "" + val currentPlaybackDuration get() = currentPlaybackSession?.duration ?: 0.0 + + fun start() { + if (listeningTimerRunning) { + Log.d(tag, "start: Timer already running for $currentDisplayTitle") + if (playerNotificationService.getCurrentPlaybackSessionId() != currentSessionId) { + Log.d(tag, "Playback session changed, reset timer") + listeningTimerTask?.cancel() + lastSyncTime = 0L + } else { + return + } + } + listeningTimerRunning = true + lastSyncTime = System.currentTimeMillis() + currentPlaybackSession = playerNotificationService.getCurrentPlaybackSessionCopy() + + listeningTimerTask = Timer("ListeningTimer", false).schedule(0L, 5000L) { + Handler(Looper.getMainLooper()).post() { + if (playerNotificationService.currentPlayer.isPlaying) { + var currentTime = playerNotificationService.getCurrentTimeSeconds() + sync(currentTime) + } + } + } + } + + fun stop() { + if (!listeningTimerRunning) return + Log.d(tag, "stop: Stopping listening for $currentDisplayTitle") + + var currentTime = playerNotificationService.getCurrentTimeSeconds() + sync(currentTime) + reset() + } + + fun sync(currentTime:Double) { + var diffSinceLastSync = System.currentTimeMillis() - lastSyncTime + if (diffSinceLastSync < 1000L) { + return + } + var listeningTimeToAdd = diffSinceLastSync / 1000L + lastSyncTime = System.currentTimeMillis() + + var syncData = MediaProgressSyncData(listeningTimeToAdd,currentPlaybackDuration,currentTime) + + if (currentIsLocal) { + // TODO: Save local progress sync + } else { + apiHandler.sendProgressSync(currentSessionId,syncData) { + Log.d(tag, "Progress sync data sent to server $currentDisplayTitle for time $currentTime") + } + } + } + + fun reset() { + listeningTimerTask?.cancel() + listeningTimerTask = null + listeningTimerRunning = false + currentPlaybackSession = null + lastSyncTime = 0L + } +} diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt index 6be2a841..9c40125d 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt @@ -24,6 +24,7 @@ import androidx.media.utils.MediaConstants import com.audiobookshelf.app.Audiobook import com.audiobookshelf.app.AudiobookManager import com.audiobookshelf.app.data.PlaybackSession +import com.audiobookshelf.app.server.ApiHandler import com.getcapacitor.Bridge import com.getcapacitor.JSObject import com.google.android.exoplayer2.* @@ -69,6 +70,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { private lateinit var mediaSession: MediaSessionCompat private lateinit var transportControls:MediaControllerCompat.TransportControls private lateinit var audiobookManager: AudiobookManager + lateinit var apiHandler: ApiHandler lateinit var mPlayer: SimpleExoPlayer lateinit var currentPlayer:Player @@ -76,7 +78,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { lateinit var sleepTimerManager:SleepTimerManager lateinit var castManager:CastManager - lateinit var audiobookProgressSyncer:AudiobookProgressSyncer + lateinit var mediaProgressSyncer:MediaProgressSyncer private var notificationId = 10; private var channelId = "audiobookshelf_channel" @@ -195,7 +197,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { playerNotificationManager.setPlayer(null) mPlayer.release() mediaSession.release() - audiobookProgressSyncer.reset() + mediaProgressSyncer.reset() Log.d(tag, "onDestroy") isStarted = false @@ -236,14 +238,17 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { var client: OkHttpClient = OkHttpClient() + // Initialize API + apiHandler = ApiHandler(ctx) + // Initialize sleep timer sleepTimerManager = SleepTimerManager(this) // Initialize Cast Manager castManager = CastManager(this) - // Initialize Audiobook Progress Syncer (Only used for android auto when webview is not open) - audiobookProgressSyncer = AudiobookProgressSyncer(this, client) + // Initialize Media Progress Syncer + mediaProgressSyncer = MediaProgressSyncer(this, apiHandler) // Initialize shake sensor Log.d(tag, "onCreate Register sensor listener ${mAccelerometer?.isWakeUpSensor}") @@ -601,13 +606,12 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } } else lastPauseTime = System.currentTimeMillis() - // If app is only running in android auto then webview will not be open - // so progress needs to be synced natively + // Start/stop progress sync interval Log.d(tag, "Playing ${getCurrentBookTitle()} | ${currentPlayer.mediaMetadata.title} | ${currentPlayer.mediaMetadata.displayTitle}") if (player.isPlaying) { - audiobookProgressSyncer.start() + mediaProgressSyncer.start() } else { - audiobookProgressSyncer.stop() + mediaProgressSyncer.stop() } clientEventEmitter?.onPlayingUpdate(player.isPlaying) @@ -763,6 +767,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } } + fun getCurrentTimeSeconds() : Double { + return getCurrentTime() / 1000.0 + } + fun getBufferedTime() : Long { if (currentPlayer.mediaItemCount > 1) { var windowIndex = currentPlayer.currentWindowIndex @@ -785,6 +793,14 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { return currentPlaybackSession?.displayTitle } + fun getCurrentPlaybackSessionCopy() :PlaybackSession? { + return currentPlaybackSession?.clone() + } + + fun getCurrentPlaybackSessionId() :String? { + return currentPlaybackSession?.id + } + fun calcPauseSeekBackTime() : Long { if (lastPauseTime <= 0) return 0 var time: Long = System.currentTimeMillis() - lastPauseTime diff --git a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt index 2d8ff56e..7902aa4b 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt @@ -7,6 +7,7 @@ import com.audiobookshelf.app.data.Library import com.audiobookshelf.app.data.LibraryItem import com.audiobookshelf.app.data.PlaybackSession import com.audiobookshelf.app.device.DeviceManager +import com.audiobookshelf.app.player.MediaProgressSyncData import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import com.getcapacitor.JSArray @@ -119,4 +120,12 @@ class ApiHandler { cb(playbackSession) } } + + fun sendProgressSync(sessionId:String, syncData: MediaProgressSyncData, cb: () -> Unit) { + var payload = JSObject(jacksonObjectMapper().writeValueAsString(syncData)) + + postRequest("/api/session/$sessionId/sync", payload) { + cb() + } + } } diff --git a/store/user.js b/store/user.js index 160ff847..8be139e8 100644 --- a/store/user.js +++ b/store/user.js @@ -26,7 +26,7 @@ export const getters = { if (!state.user || !state.user.mediaProgress) return null return state.user.mediaProgress.find(li => { if (episodeId && li.episodeId !== episodeId) return false - return li.id == libraryItemId + return li.libraryItemId == libraryItemId }) }, getUserBookmarksForItem: (state) => (libraryItemId) => {