diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/DbManager.kt b/android/app/src/main/java/com/audiobookshelf/app/data/DbManager.kt index b41f5d0c..cc23d8e3 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/DbManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/DbManager.kt @@ -76,10 +76,9 @@ class DbManager { fun getAllLocalFolders():List { var localFolders:MutableList = mutableListOf() - Paper.book("localFolders").allKeys.forEach { - var localFolder:LocalFolder? = Paper.book("localFolders").read(it) - if (localFolder != null) { - localFolders.add(localFolder) + Paper.book("localFolders").allKeys.forEach { localFolderId -> + Paper.book("localFolders").read(localFolderId)?.let { + localFolders.add(it) } } return localFolders @@ -103,15 +102,41 @@ class DbManager { fun getDownloadItems():List { var downloadItems:MutableList = mutableListOf() - Paper.book("downloadItems").allKeys.forEach { - var downloadItem:AbsDownloader.DownloadItem? = Paper.book("downloadItems").read(it) - if (downloadItem != null) { - downloadItems.add(downloadItem) + Paper.book("downloadItems").allKeys.forEach { downloadItemId -> + Paper.book("downloadItems").read(downloadItemId)?.let { + downloadItems.add(it) } } return downloadItems } + fun saveLocalMediaProgress(mediaProgress:LocalMediaProgress) { + Paper.book("localMediaProgress").write(mediaProgress.id,mediaProgress) + } + // For books this will just be the localLibraryItemId for podcast episodes this will be "{localLibraryItemId}-{episodeId}" + fun getLocalMediaProgress(localMediaProgressId:String):LocalMediaProgress? { + return Paper.book("localMediaProgress").read(localMediaProgressId) + } + fun getAllLocalMediaProgress():List { + var mediaProgress:MutableList = mutableListOf() + Paper.book("localMediaProgress").allKeys.forEach { localMediaProgressId -> + Paper.book("localMediaProgress").read(localMediaProgressId)?.let { + mediaProgress.add(it) + } + } + return mediaProgress + } + fun removeLocalMediaProgress(localMediaProgressId:String) { + Paper.book("localMediaProgress").delete(localMediaProgressId) + } + + fun saveLocalPlaybackSession(playbackSession:PlaybackSession) { + Paper.book("localPlaybackSession").write(playbackSession.id,playbackSession) + } + fun getLocalPlaybackSession(playbackSessionId:String):PlaybackSession? { + return Paper.book("localPlaybackSession").read(playbackSessionId) + } + fun saveObject(db:String, key:String, value:JSONObject) { Log.d(tag, "Saving Object $key ${value.toString()}") Paper.book(db).write(key, value) 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 1bf80475..6de16cd1 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 @@ -9,6 +9,7 @@ data class ServerConnectionConfig( var index:Int, var name:String, var address:String, + var userId:String, var username:String, var token:String ) @@ -16,7 +17,7 @@ data class ServerConnectionConfig( data class DeviceData( var serverConnectionConfigs:MutableList, var lastServerConnectionConfigId:String?, - var localLibraryItemIdPlaying:String? + var currentLocalPlaybackSession:PlaybackSession? // Stored to open up where left off for local media ) @JsonIgnoreProperties(ignoreUnknown = true) 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 index 1eb8602e..8ba1f334 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt @@ -1,5 +1,6 @@ package com.audiobookshelf.app.data +import com.audiobookshelf.app.device.DeviceManager import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnoreProperties import java.util.* @@ -7,8 +8,6 @@ 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, @@ -19,8 +18,14 @@ data class LocalLibraryItem( var localFiles:MutableList, var coverContentUrl:String?, var coverAbsolutePath:String?, - var isLocal:Boolean -) { + var isLocal:Boolean, + // If local library item is linked to a server item + var serverConnectionConfigId:String?, + var serverAddress:String?, + var serverUserId:String?, + var libraryItemId:String? + ) { + @JsonIgnore fun getDuration():Double { var total = 0.0 @@ -45,9 +50,14 @@ data class LocalLibraryItem( } @JsonIgnore - fun getPlaybackSession():PlaybackSession { + fun getPlaybackSession(episodeId:String):PlaybackSession { var sessionId = "play-${UUID.randomUUID()}" + val mediaProgressId = if (episodeId.isNullOrEmpty()) id else "$id-$episodeId" + var mediaProgress = DeviceManager.dbManager.getLocalMediaProgress(mediaProgressId) + var currentTime = mediaProgress?.currentTime ?: 0.0 + + // TODO: Clean up add mediaType methods for displayTitle and displayAuthor var mediaMetadata = media.metadata var chapters = if (mediaType == "book") (media as Book).chapters else mutableListOf() var authorName = "Unknown" @@ -55,7 +65,10 @@ data class LocalLibraryItem( 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) + + var episodeIdNullable = if (episodeId.isNullOrEmpty()) null else episodeId + var dateNow = System.currentTimeMillis() + return PlaybackSession(sessionId,serverUserId,libraryItemId,episodeIdNullable, mediaType, mediaMetadata, chapters, mediaMetadata.title, authorName,null,getDuration(),PLAYMETHOD_LOCAL,dateNow,0L,0L, media.getAudioTracks() as MutableList,currentTime,null,this,serverConnectionConfigId, serverAddress) } @JsonIgnore 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 index 10639cbc..2a14c847 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaItem.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaItem.kt @@ -10,7 +10,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties @JsonIgnoreProperties(ignoreUnknown = true) data class LocalMediaItem( var id:String, - var serverAddress:String?, var name: String, var mediaType:String, var folderId:String, @@ -63,10 +62,10 @@ data class LocalMediaItem( 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) + return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true,null,null,null,null) } 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) + return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false, mediaType, podcast,localFiles,coverContentUrl, coverAbsolutePath, true, null,null,null,null) } } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaProgress.kt b/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaProgress.kt new file mode 100644 index 00000000..8640ba7f --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaProgress.kt @@ -0,0 +1,22 @@ +package com.audiobookshelf.app.data + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties + +@JsonIgnoreProperties(ignoreUnknown = true) +data class LocalMediaProgress( + var id:String, + var localLibraryItemId:String, + var episodeId:String?, + var duration:Double, + var progress:Double, // 0 to 1 + var currentTime:Double, + var isFinished:Boolean, + var lastUpdate:Long, + var startedAt:Long, + var finishedAt:Long?, + // For local lib items from server to support server sync + var serverConnectionConfigId:String?, + var serverAddress:String?, + var serverUserId:String?, + var libraryItemId:String? +) 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 deleted file mode 100644 index 37185a4b..00000000 --- a/android/app/src/main/java/com/audiobookshelf/app/data/MediaProgress.kt +++ /dev/null @@ -1,18 +0,0 @@ -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 3978b591..c9b07091 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 @@ -2,8 +2,9 @@ package com.audiobookshelf.app.data import android.net.Uri import android.support.v4.media.MediaMetadataCompat -import android.util.Log import com.audiobookshelf.app.R +import com.audiobookshelf.app.device.DeviceManager +import com.audiobookshelf.app.player.MediaProgressSyncData import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.google.android.exoplayer2.MediaItem @@ -29,17 +30,29 @@ class PlaybackSession( var coverPath:String?, var duration:Double, var playMethod:Int, + var startedAt:Long, + var updatedAt:Long, + var timeListening:Long, var audioTracks:MutableList, var currentTime:Double, var libraryItem:LibraryItem?, var localLibraryItem:LocalLibraryItem?, - var serverUrl:String?, - var token:String? + var serverConnectionConfigId:String?, + var serverAddress:String? ) { + @get:JsonIgnore val isHLS get() = playMethod == PLAYMETHOD_TRANSCODE + @get:JsonIgnore val isLocal get() = playMethod == PLAYMETHOD_LOCAL + @get:JsonIgnore val currentTimeMs get() = (currentTime * 1000L).toLong() + @get:JsonIgnore + val localLibraryItemId get() = localLibraryItem?.id ?: "" + @get:JsonIgnore + val localMediaProgressId get() = if (episodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$episodeId" + @get:JsonIgnore + val progress get() = currentTime / getTotalDuration() @JsonIgnore fun getCurrentTrackIndex():Int { @@ -77,13 +90,13 @@ class PlaybackSession( if (localLibraryItem?.coverContentUrl != null) return Uri.parse(localLibraryItem?.coverContentUrl) ?: Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon) if (coverPath == null) return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon) - return Uri.parse("$serverUrl/api/items/$libraryItemId/cover?token=$token") + return Uri.parse("$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}") } @JsonIgnore fun getContentUri(audioTrack:AudioTrack): Uri { if (isLocal) return Uri.parse(audioTrack.contentUrl) // Local content url - return Uri.parse("$serverUrl${audioTrack.contentUrl}?token=$token") + return Uri.parse("$serverAddress${audioTrack.contentUrl}?token=${DeviceManager.token}") } @JsonIgnore @@ -130,6 +143,19 @@ class PlaybackSession( @JsonIgnore fun clone():PlaybackSession { - return PlaybackSession(id,userId,libraryItemId,episodeId,mediaType,mediaMetadata,chapters,displayTitle,displayAuthor,coverPath,duration,playMethod,audioTracks,currentTime,libraryItem,localLibraryItem,serverUrl,token) + return PlaybackSession(id,userId,libraryItemId,episodeId,mediaType,mediaMetadata,chapters,displayTitle,displayAuthor,coverPath,duration,playMethod,startedAt,updatedAt,timeListening,audioTracks,currentTime,libraryItem,localLibraryItem,serverConnectionConfigId,serverAddress) + } + + @JsonIgnore + fun syncData(syncData:MediaProgressSyncData) { + timeListening += syncData.timeListened + updatedAt = System.currentTimeMillis() + currentTime = syncData.currentTime + } + + @JsonIgnore + fun getNewLocalMediaProgress():LocalMediaProgress { + var dateNow = System.currentTimeMillis() + return LocalMediaProgress(localMediaProgressId,localLibraryItemId,episodeId,getTotalDuration(),progress,currentTime,false,dateNow,dateNow,null,serverConnectionConfigId,serverAddress,userId,libraryItemId) } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt b/android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt index 8955df16..6e67e3e9 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt @@ -12,6 +12,7 @@ object DeviceManager { var serverConnectionConfig: ServerConnectionConfig? = null val serverAddress get() = serverConnectionConfig?.address ?: "" + val serverUserId get() = serverConnectionConfig?.userId ?: "" val token get() = serverConnectionConfig?.token ?: "" init { diff --git a/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt b/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt index 09394b96..f056d32c 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt @@ -186,7 +186,7 @@ class FolderScanner(var ctx: Context) { Log.d(tag, "Found local media item named $itemFolderName with ${audioTracks.size} tracks and ${localFiles.size} local files") mediaItemsAdded++ - var localMediaItem = LocalMediaItem(itemId,null, itemFolderName, localFolder.mediaType, localFolder.id, itemFolder.uri.toString(), itemFolder.getSimplePath(ctx), itemFolder.getBasePath(ctx), itemFolder.getAbsolutePath(ctx),audioTracks,localFiles,coverContentUrl,coverAbsolutePath) + var localMediaItem = LocalMediaItem(itemId, itemFolderName, localFolder.mediaType, localFolder.id, itemFolder.uri.toString(), itemFolder.getSimplePath(ctx), itemFolder.getBasePath(ctx), itemFolder.getAbsolutePath(ctx),audioTracks,localFiles,coverContentUrl,coverAbsolutePath) var localLibraryItem = localMediaItem.getLocalLibraryItem() localLibraryItems.add(localLibraryItem) } @@ -236,7 +236,7 @@ class FolderScanner(var ctx: Context) { var filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*")) Log.d(tag, "scanDownloadItem ${filesFound.size} files found in ${downloadItem.itemFolderPath}") - var localLibraryItem = LocalLibraryItem("local_${downloadItem.id}", downloadItem.serverAddress, downloadItem.id, downloadItem.localFolder.id, itemFolderBasePath, itemFolderAbsolutePath, itemFolderUrl, false, downloadItem.mediaType, downloadItem.media, mutableListOf(), null, null, true) + var localLibraryItem = LocalLibraryItem("local_${downloadItem.id}", downloadItem.localFolder.id, itemFolderBasePath, itemFolderAbsolutePath, itemFolderUrl, false, downloadItem.mediaType, downloadItem.media, mutableListOf(), null, null, true,downloadItem.serverConnectionConfigId,downloadItem.serverAddress,downloadItem.serverUserId,downloadItem.id) var localFiles:MutableList = mutableListOf() var audioTracks:MutableList = mutableListOf() 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 index 5aa4016d..0f7b41c8 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/MediaProgressSyncer.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/MediaProgressSyncer.kt @@ -3,11 +3,13 @@ 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.LocalMediaProgress import com.audiobookshelf.app.data.PlaybackSession +import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.server.ApiHandler import java.util.* import kotlin.concurrent.schedule +import kotlin.math.roundToInt data class MediaProgressSyncData( var timeListened:Long, // seconds @@ -26,7 +28,7 @@ class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, a private var lastSyncTime:Long = 0 var currentPlaybackSession: PlaybackSession? = null // copy of pb session currently syncing -// var currentMediaProgress: MediaProgress? = null + var currentLocalMediaProgress: LocalMediaProgress? = null val currentDisplayTitle get() = currentPlaybackSession?.displayTitle ?: "Unset" val currentIsLocal get() = currentPlaybackSession?.isLocal == true @@ -77,8 +79,13 @@ class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, a var syncData = MediaProgressSyncData(listeningTimeToAdd,currentPlaybackDuration,currentTime) + currentPlaybackSession?.syncData(syncData) if (currentIsLocal) { - // TODO: Save local progress sync + // Save local progress sync + currentPlaybackSession?.let { + DeviceManager.dbManager.saveLocalPlaybackSession(it) + saveLocalProgress(it) + } } else { apiHandler.sendProgressSync(currentSessionId,syncData) { Log.d(tag, "Progress sync data sent to server $currentDisplayTitle for time $currentTime") @@ -86,6 +93,26 @@ class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, a } } + private fun saveLocalProgress(playbackSession:PlaybackSession) { + if (currentLocalMediaProgress == null) { + var mediaProgress = DeviceManager.dbManager.getLocalMediaProgress(playbackSession.localMediaProgressId) + if (mediaProgress == null) { + currentLocalMediaProgress = playbackSession.getNewLocalMediaProgress() + } else { + currentLocalMediaProgress = mediaProgress + } + } else { + currentLocalMediaProgress?.currentTime = playbackSession.currentTime + currentLocalMediaProgress?.lastUpdate = System.currentTimeMillis() + currentLocalMediaProgress?.progress = playbackSession.progress + } + currentLocalMediaProgress?.let { + DeviceManager.dbManager.saveLocalMediaProgress(it) + playerNotificationService.clientEventEmitter?.onLocalMediaProgressUpdate(it) + Log.d(tag, "Saved Local Progress Current Time: ${it.currentTime} | Duration ${it.duration} | Progress ${(it.progress * 100).roundToInt()}%") + } + } + fun reset() { listeningTimerTask?.cancel() listeningTimerTask = null 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 9c40125d..73b5c4c2 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 @@ -23,7 +23,9 @@ import androidx.media.MediaBrowserServiceCompat import androidx.media.utils.MediaConstants import com.audiobookshelf.app.Audiobook import com.audiobookshelf.app.AudiobookManager +import com.audiobookshelf.app.data.LocalMediaProgress import com.audiobookshelf.app.data.PlaybackSession +import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.server.ApiHandler import com.getcapacitor.Bridge import com.getcapacitor.JSObject @@ -57,6 +59,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { fun onPrepare(audiobookId: String, playWhenReady: Boolean) fun onSleepTimerEnded(currentPosition: Long) fun onSleepTimerSet(sleepTimeRemaining: Int) + fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress) } private val tag = "PlayerService" @@ -648,7 +651,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { Log.d(tag, "Playing HLS Item") var dataSourceFactory = DefaultHttpDataSource.Factory() dataSourceFactory.setUserAgent(channelId) - dataSourceFactory.setDefaultRequestProperties(hashMapOf("Authorization" to "Bearer ${playbackSession.token}")) + dataSourceFactory.setDefaultRequestProperties(hashMapOf("Authorization" to "Bearer ${DeviceManager.token}")) mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItems[0]) } mPlayer.setMediaSource(mediaSource) diff --git a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt index 464347d7..76962cda 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt @@ -6,6 +6,7 @@ import android.os.Looper import android.util.Log import androidx.core.content.ContextCompat import com.audiobookshelf.app.MainActivity +import com.audiobookshelf.app.data.LocalMediaProgress import com.audiobookshelf.app.data.PlaybackSession import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.player.CastManager @@ -64,6 +65,10 @@ class AbsAudioPlayer : Plugin() { override fun onSleepTimerSet(sleepTimeRemaining: Int) { emit("onSleepTimerSet", sleepTimeRemaining) } + + override fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress) { + notifyListeners("onLocalMediaProgressUpdate", JSObject(jacksonObjectMapper().writeValueAsString(localMediaProgress))) + } }) } mainActivity.pluginCallback = foregroundServiceReady @@ -86,6 +91,7 @@ class AbsAudioPlayer : Plugin() { } var libraryItemId = call.getString("libraryItemId", "").toString() + var episodeId = call.getString("episodeId", "").toString() var playWhenReady = call.getBoolean("playWhenReady") == true if (libraryItemId.isEmpty()) { @@ -97,13 +103,13 @@ class AbsAudioPlayer : Plugin() { DeviceManager.dbManager.getLocalLibraryItem(libraryItemId)?.let { Handler(Looper.getMainLooper()).post() { Log.d(tag, "Preparing Local Media item ${jacksonObjectMapper().writeValueAsString(it)}") - var playbackSession = it.getPlaybackSession() + var playbackSession = it.getPlaybackSession(episodeId) playerNotificationService.preparePlayer(playbackSession, playWhenReady) } return call.resolve(JSObject()) } } else { // Play library item from server - apiHandler.playLibraryItem(libraryItemId, false) { + apiHandler.playLibraryItem(libraryItemId, episodeId, false) { Handler(Looper.getMainLooper()).post() { Log.d(tag, "Preparing Player TEST ${jacksonObjectMapper().writeValueAsString(it)}") diff --git a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt index 5bbb7192..63b7130f 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt @@ -17,6 +17,10 @@ import org.json.JSONObject class AbsDatabase : Plugin() { val tag = "AbsDatabase" + data class LocalMediaProgressPayload(val value:List) + data class LocalLibraryItemsPayload(val value:List) + data class LocalFoldersPayload(val value:List) + @PluginMethod fun getDeviceData(call:PluginCall) { GlobalScope.launch(Dispatchers.IO) { @@ -29,10 +33,7 @@ class AbsDatabase : Plugin() { fun getLocalFolders(call:PluginCall) { GlobalScope.launch(Dispatchers.IO) { var folders = DeviceManager.dbManager.getAllLocalFolders() - var folderObjArray = jacksonObjectMapper().writeValueAsString(folders) - var jsobj = JSObject() - jsobj.put("folders", folderObjArray) - call.resolve(jsobj) + call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(LocalFoldersPayload(folders)))) } } @@ -80,9 +81,7 @@ class AbsDatabase : Plugin() { GlobalScope.launch(Dispatchers.IO) { var localLibraryItems = DeviceManager.dbManager.getLocalLibraryItems(mediaType) - var jsobj = JSObject() - jsobj.put("localLibraryItems", jacksonObjectMapper().writeValueAsString(localLibraryItems)) - call.resolve(jsobj) + call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(LocalLibraryItemsPayload(localLibraryItems)))) } } @@ -90,11 +89,8 @@ class AbsDatabase : Plugin() { fun getLocalLibraryItemsInFolder(call:PluginCall) { var folderId = call.getString("folderId", "").toString() GlobalScope.launch(Dispatchers.IO) { - var localMediaItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(folderId) - var mediaItemsArray = jacksonObjectMapper().writeValueAsString(localMediaItems) - var jsobj = JSObject() - jsobj.put("localLibraryItems", mediaItemsArray) - call.resolve(jsobj) + var localLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(folderId) + call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(LocalLibraryItemsPayload(localLibraryItems)))) } } @@ -103,6 +99,7 @@ class AbsDatabase : Plugin() { var serverConnectionConfigId = call.getString("id", "").toString() var serverConnectionConfig = DeviceManager.deviceData.serverConnectionConfigs.find { it.id == serverConnectionConfigId } + var userId = call.getString("userId", "").toString() var username = call.getString("username", "").toString() var token = call.getString("token", "").toString() @@ -113,7 +110,7 @@ class AbsDatabase : Plugin() { // Create new server connection config var sscId = DeviceManager.getBase64Id("$serverAddress@$username") var sscIndex = DeviceManager.deviceData.serverConnectionConfigs.size - serverConnectionConfig = ServerConnectionConfig(sscId, sscIndex, "$serverAddress ($username)", serverAddress, username, token) + serverConnectionConfig = ServerConnectionConfig(sscId, sscIndex, "$serverAddress ($username)", serverAddress, userId, username, token) // Add and save DeviceManager.deviceData.serverConnectionConfigs.add(serverConnectionConfig!!) @@ -122,6 +119,7 @@ class AbsDatabase : Plugin() { } else { var shouldSave = false if (serverConnectionConfig?.username != username || serverConnectionConfig?.token != token) { + serverConnectionConfig?.userId = userId serverConnectionConfig?.username = username serverConnectionConfig?.name = "${serverConnectionConfig?.address} (${serverConnectionConfig?.username})" serverConnectionConfig?.token = token @@ -168,6 +166,22 @@ class AbsDatabase : Plugin() { } } + + @PluginMethod + fun getAllLocalMediaProgress(call:PluginCall) { + GlobalScope.launch(Dispatchers.IO) { + var localMediaProgress = DeviceManager.dbManager.getAllLocalMediaProgress() + call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(LocalMediaProgressPayload(localMediaProgress)))) + } + } + + @PluginMethod + fun removeLocalMediaProgress(call:PluginCall) { + var localMediaProgressId = call.getString("localMediaProgressId", "").toString() + DeviceManager.dbManager.removeLocalMediaProgress(localMediaProgressId) + call.resolve() + } + // // Generic Webview calls to db // diff --git a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDownloader.kt b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDownloader.kt index 1c9edc1b..e7899183 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDownloader.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDownloader.kt @@ -62,7 +62,9 @@ class AbsDownloader : Plugin() { data class DownloadItem( val id: String, + val serverConnectionConfigId:String, val serverAddress:String, + val serverUserId:String, val mediaType: String, val itemFolderPath:String, val localFolder: LocalFolder, @@ -143,7 +145,7 @@ class AbsDownloader : Plugin() { var tracks = libraryItem.media.getAudioTracks() Log.d(tag, "Starting library item download with ${tracks.size} tracks") var itemFolderPath = localFolder.absolutePath + "/" + bookTitle - var downloadItem = DownloadItem(libraryItem.id, DeviceManager.serverAddress, libraryItem.mediaType, itemFolderPath, localFolder, bookTitle, libraryItem.media, mutableListOf()) + var downloadItem = DownloadItem(libraryItem.id, DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, bookTitle, libraryItem.media, mutableListOf()) // Create download item part for each audio track tracks.forEach { audioTrack -> 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 7902aa4b..5fe9daf7 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 @@ -104,18 +104,20 @@ class ApiHandler { } } - fun playLibraryItem(libraryItemId:String, forceTranscode:Boolean, cb: (PlaybackSession) -> Unit) { + fun playLibraryItem(libraryItemId:String, episodeId:String, forceTranscode:Boolean, cb: (PlaybackSession) -> Unit) { val mapper = jacksonObjectMapper() var payload = JSObject() payload.put("mediaPlayer", "exo-player") // Only if direct play fails do we force transcode + // TODO: Fallback to transcode if (!forceTranscode) payload.put("forceDirectPlay", true) else payload.put("forceTranscode", true) - postRequest("/api/items/$libraryItemId/play", payload) { - it.put("serverUrl", DeviceManager.serverAddress) - it.put("token", DeviceManager.token) + val endpoint = if (episodeId.isNullOrEmpty()) "/api/items/$libraryItemId/play" else "/api/items/$libraryItemId/play/$episodeId" + postRequest(endpoint, payload) { + it.put("serverConnectionConfigId", DeviceManager.serverConnectionConfig?.id) + it.put("serverAddress", DeviceManager.serverAddress) val playbackSession = mapper.readValue(it.toString()) cb(playbackSession) } diff --git a/components/app/AudioPlayerContainer.vue b/components/app/AudioPlayerContainer.vue index fc37753e..653c28df 100644 --- a/components/app/AudioPlayerContainer.vue +++ b/components/app/AudioPlayerContainer.vue @@ -26,6 +26,7 @@ export default { isSleepTimerRunning: false, sleepTimerEndTime: 0, sleepTimeRemaining: 0, + onLocalMediaProgressUpdateListener: null, onSleepTimerEndedListener: null, onSleepTimerSetListener: null, sleepInterval: null, @@ -174,9 +175,14 @@ export default { .catch((error) => { console.error('Failed', error) }) + }, + onLocalMediaProgressUpdate(localMediaProgress) { + console.log('Got local media progress update', localMediaProgress.progress, JSON.stringify(localMediaProgress)) + this.$store.commit('globals/updateLocalMediaProgress', localMediaProgress) } }, mounted() { + this.onLocalMediaProgressUpdateListener = AbsAudioPlayer.addListener('onLocalMediaProgressUpdate', this.onLocalMediaProgressUpdate) this.onSleepTimerEndedListener = AbsAudioPlayer.addListener('onSleepTimerEnded', this.onSleepTimerEnded) this.onSleepTimerSetListener = AbsAudioPlayer.addListener('onSleepTimerSet', this.onSleepTimerSet) @@ -189,6 +195,7 @@ export default { this.$store.commit('user/addSettingsListener', { id: 'streamContainer', meth: this.settingsUpdated }) }, beforeDestroy() { + if (this.onLocalMediaProgressUpdateListener) this.onLocalMediaProgressUpdateListener.remove() if (this.onSleepTimerEndedListener) this.onSleepTimerEndedListener.remove() if (this.onSleepTimerSetListener) this.onSleepTimerSetListener.remove() diff --git a/components/cards/LazyBookCard.vue b/components/cards/LazyBookCard.vue index 58f54c55..44fbda38 100644 --- a/components/cards/LazyBookCard.vue +++ b/components/cards/LazyBookCard.vue @@ -207,6 +207,7 @@ export default { return null }, userProgress() { + if (this.isLocal) return this.store.getters['globals/getLocalMediaProgressById'](this.libraryItemId) return this.store.getters['user/getUserMediaProgress'](this.libraryItemId) }, userProgressPercent() { diff --git a/components/connection/ServerConnectForm.vue b/components/connection/ServerConnectForm.vue index 976e5219..ea61f8c8 100644 --- a/components/connection/ServerConnectForm.vue +++ b/components/connection/ServerConnectForm.vue @@ -123,6 +123,7 @@ export default { this.error = null this.serverConfig = { address: null, + userId: null, username: null } }, @@ -160,6 +161,7 @@ export default { this.deviceData.serverConnectionConfigs = this.deviceData.serverConnectionConfigs.filter((scc) => scc.id != this.serverConfig.id) this.serverConfig = { address: null, + userId: null, username: null } this.password = null @@ -266,6 +268,7 @@ export default { this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId) } + this.serverConfig.userId = user.id this.serverConfig.token = user.token var serverConnectionConfig = await this.$db.setServerConnectionConfig(this.serverConfig) diff --git a/layouts/default.vue b/layouts/default.vue index 8c1cb480..bc4d196d 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -144,15 +144,6 @@ export default { // } // }) // }, - async initMediaStore() { - // Request and setup listeners for media files on native - // AbsDownloader.addListener('onItemDownloadUpdate', (data) => { - // this.onItemDownloadUpdate(data) - // }) - // AbsDownloader.addListener('onItemDownloadComplete', (data) => { - // this.onItemDownloadComplete(data) - // }) - }, async loadSavedSettings() { var userSavedServerSettings = await this.$localStore.getServerSettings() if (userSavedServerSettings) { @@ -266,9 +257,9 @@ export default { await this.attemptConnection() } + this.$store.dispatch('globals/loadLocalMediaProgress') this.checkForUpdate() this.loadSavedSettings() - this.initMediaStore() } }, beforeDestroy() { diff --git a/pages/item/_id.vue b/pages/item/_id.vue index c6d5c343..ef57fd21 100644 --- a/pages/item/_id.vue +++ b/pages/item/_id.vue @@ -164,6 +164,7 @@ export default { return this.$store.getters['user/getToken'] }, userItemProgress() { + if (this.isLocal) return this.$store.getters['globals/getLocalMediaProgressById'](this.libraryItemId) return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId) }, userIsFinished() { @@ -238,17 +239,22 @@ export default { }) if (value) { this.resettingProgress = true - this.$axios - .$delete(`/api/me/progress/${this.libraryItemId}`) - .then(() => { - console.log('Progress reset complete') - this.$toast.success(`Your progress was reset`) - this.resettingProgress = false - }) - .catch((error) => { - console.error('Progress reset failed', error) - this.resettingProgress = false - }) + if (this.isLocal) { + await this.$db.removeLocalMediaProgress(this.libraryItemId) + this.$store.commit('globals/removeLocalMediaProgress', this.libraryItemId) + } else { + await this.$axios + .$delete(`/api/me/progress/${this.libraryItemId}`) + .then(() => { + console.log('Progress reset complete') + this.$toast.success(`Your progress was reset`) + }) + .catch((error) => { + console.error('Progress reset failed', error) + }) + } + + this.resettingProgress = false } }, itemUpdated(libraryItem) { diff --git a/plugins/capacitor/AbsDatabase.js b/plugins/capacitor/AbsDatabase.js index dd00c6f5..e7448451 100644 --- a/plugins/capacitor/AbsDatabase.js +++ b/plugins/capacitor/AbsDatabase.js @@ -13,7 +13,7 @@ class AbsDatabaseWeb extends WebPlugin { const deviceData = { serverConnectionConfigs: [], lastServerConnectionConfigId: null, - localLibraryItemIdPlaying: null + currentLocalPlaybackSession: null } return deviceData } @@ -26,6 +26,7 @@ class AbsDatabaseWeb extends WebPlugin { deviceData.lastServerConnectionConfigId = ssc.id ssc.name = `${ssc.address} (${serverConnectionConfig.username})` ssc.token = serverConnectionConfig.token + ssc.userId = serverConnectionConfig.userId ssc.username = serverConnectionConfig.username localStorage.setItem('device', JSON.stringify(deviceData)) } else { @@ -33,6 +34,7 @@ class AbsDatabaseWeb extends WebPlugin { id: encodeURIComponent(Buffer.from(`${serverConnectionConfig.address}@${serverConnectionConfig.username}`).toString('base64')), index: deviceData.serverConnectionConfigs.length, name: `${serverConnectionConfig.address} (${serverConnectionConfig.username})`, + userId: serverConnectionConfig.userId, username: serverConnectionConfig.username, address: serverConnectionConfig.address, token: serverConnectionConfig.token @@ -62,7 +64,7 @@ class AbsDatabaseWeb extends WebPlugin { // async getLocalFolders() { return { - folders: [ + value: [ { id: 'test1', name: 'Audiobooks', @@ -76,11 +78,11 @@ class AbsDatabaseWeb extends WebPlugin { } } async getLocalFolder({ folderId }) { - return this.getLocalFolders().then((data) => data.folders[0]) + return this.getLocalFolders().then((data) => data.value[0]) } async getLocalLibraryItems(payload) { return { - localLibraryItems: [{ + value: [{ id: 'local_test', libraryItemId: 'test34', folderId: 'test1', @@ -133,10 +135,36 @@ class AbsDatabaseWeb extends WebPlugin { return this.getLocalLibraryItems() } async getLocalLibraryItem({ id }) { - return this.getLocalLibraryItems().then((data) => data.localLibraryItems[0]) + return this.getLocalLibraryItems().then((data) => data.value[0]) } async getLocalLibraryItemByLLId({ libraryItemId }) { - return this.getLocalLibraryItems().then((data) => data.localLibraryItems.find(lli => lli.libraryItemId == libraryItemId)) + return this.getLocalLibraryItems().then((data) => data.value.find(lli => lli.libraryItemId == libraryItemId)) + } + async getAllLocalMediaProgress() { + return { + value: [ + { + id: 'local_test', + localLibraryItemId: 'local_test', + episodeId: null, + duration: 100, + progress: 0.5, + currentTime: 50, + isFinished: false, + lastUpdate: 394089090, + startedAt: 239048209, + finishedAt: null, + // For local lib items from server to support server sync + // var serverConnectionConfigId:String?, + // var serverAddress:String?, + // var serverUserId:String?, + // var libraryItemId:String? + } + ] + } + } + async removeLocalMediaProgress({ localMediaProgressId }) { + return null } } diff --git a/plugins/db.js b/plugins/db.js index 8ce41e05..37a43db9 100644 --- a/plugins/db.js +++ b/plugins/db.js @@ -52,13 +52,7 @@ class DbService { } getLocalFolders() { - return AbsDatabase.getLocalFolders().then((data) => { - console.log('Loaded local folders', JSON.stringify(data)) - if (data.folders && typeof data.folders == 'string') { - return JSON.parse(data.folders) - } - return data.folders - }).catch((error) => { + return AbsDatabase.getLocalFolders().then((data) => data.value).catch((error) => { console.error('Failed to load', error) return null }) @@ -72,23 +66,11 @@ class DbService { } getLocalLibraryItemsInFolder(folderId) { - return AbsDatabase.getLocalLibraryItemsInFolder({ folderId }).then((data) => { - console.log('Loaded local library items in folder', JSON.stringify(data)) - if (data.localLibraryItems && typeof data.localLibraryItems == 'string') { - return JSON.parse(data.localLibraryItems) - } - return data.localLibraryItems - }) + return AbsDatabase.getLocalLibraryItemsInFolder({ folderId }).then((data) => data.value) } getLocalLibraryItems(mediaType = null) { - return AbsDatabase.getLocalLibraryItems({ mediaType }).then((data) => { - console.log('Loaded all local media items', JSON.stringify(data)) - if (data.localLibraryItems && typeof data.localLibraryItems == 'string') { - return JSON.parse(data.localLibraryItems) - } - return data.localLibraryItems - }) + return AbsDatabase.getLocalLibraryItems({ mediaType }).then((data) => data.value) } getLocalLibraryItem(id) { @@ -98,6 +80,14 @@ class DbService { getLocalLibraryItemByLLId(libraryItemId) { return AbsDatabase.getLocalLibraryItemByLLId({ libraryItemId }) } + + getAllLocalMediaProgress() { + return AbsDatabase.getAllLocalMediaProgress().then((data) => data.value) + } + + removeLocalMediaProgress(localMediaProgressId) { + return AbsDatabase.removeLocalMediaProgress({ localMediaProgressId }) + } } export default ({ app, store }, inject) => { diff --git a/store/globals.js b/store/globals.js index e80678bd..1e9c4019 100644 --- a/store/globals.js +++ b/store/globals.js @@ -1,7 +1,8 @@ export const state = () => ({ itemDownloads: [], bookshelfListView: false, - series: null + series: null, + localMediaProgress: [] }) export const getters = { @@ -25,11 +26,21 @@ export const getters = { var url = new URL(`/api/items/${libraryItem.id}/cover`, rootGetters['user/getServerAddress']) return `${url}?token=${userToken}&ts=${lastUpdate}` + }, + getLocalMediaProgressById: (state) => (localLibraryItemId, episodeId = null) => { + return state.localMediaProgress.find(lmp => { + if (episodeId != null && lmp.episodeId != episodeId) return false + return lmp.localLibraryItemId == localLibraryItemId + }) } } export const actions = { - + async loadLocalMediaProgress({ state, commit }) { + var mediaProgress = await this.$db.getAllLocalMediaProgress() + console.log('Got all local media progress', JSON.stringify(mediaProgress)) + commit('setLocalMediaProgress', mediaProgress) + } } export const mutations = { @@ -49,5 +60,22 @@ export const mutations = { }, setSeries(state, val) { state.series = val + }, + setLocalMediaProgress(state, val) { + state.localMediaProgress = val + }, + updateLocalMediaProgress(state, prog) { + if (!prog || !prog.id) { + return + } + var index = state.localMediaProgress.findIndex(lmp => lmp.id == prog.id) + if (index >= 0) { + state.localMediaProgress.splice(index, 1, prog) + } else { + state.localMediaProgress.push(prog) + } + }, + removeLocalMediaProgress(state, id) { + state.localMediaProgress = state.localMediaProgress.filter(lmp => lmp.id != id) } } \ No newline at end of file