Add keep local media progress and playback sessions, update data models to support syncing local progress and support for podcast episodes

This commit is contained in:
advplyr 2022-04-09 12:03:37 -05:00
parent 526fca98b9
commit d9e4469089
23 changed files with 295 additions and 118 deletions

View file

@ -76,10 +76,9 @@ class DbManager {
fun getAllLocalFolders():List<LocalFolder> {
var localFolders:MutableList<LocalFolder> = 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<LocalFolder>(localFolderId)?.let {
localFolders.add(it)
}
}
return localFolders
@ -103,15 +102,41 @@ class DbManager {
fun getDownloadItems():List<AbsDownloader.DownloadItem> {
var downloadItems:MutableList<AbsDownloader.DownloadItem> = 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<AbsDownloader.DownloadItem>(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<LocalMediaProgress> {
var mediaProgress:MutableList<LocalMediaProgress> = mutableListOf()
Paper.book("localMediaProgress").allKeys.forEach { localMediaProgressId ->
Paper.book("localMediaProgress").read<LocalMediaProgress>(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)

View file

@ -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<ServerConnectionConfig>,
var lastServerConnectionConfigId:String?,
var localLibraryItemIdPlaying:String?
var currentLocalPlaybackSession:PlaybackSession? // Stored to open up where left off for local media
)
@JsonIgnoreProperties(ignoreUnknown = true)

View file

@ -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<LocalFile>,
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<AudioTrack>,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<AudioTrack>,currentTime,null,this,serverConnectionConfigId, serverAddress)
}
@JsonIgnore

View file

@ -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)
}
}
}

View file

@ -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?
)

View file

@ -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?
)

View file

@ -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<AudioTrack>,
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)
}
}

View file

@ -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 {

View file

@ -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<LocalFile> = mutableListOf()
var audioTracks:MutableList<AudioTrack> = mutableListOf()

View file

@ -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

View file

@ -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)

View file

@ -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)}")

View file

@ -17,6 +17,10 @@ import org.json.JSONObject
class AbsDatabase : Plugin() {
val tag = "AbsDatabase"
data class LocalMediaProgressPayload(val value:List<LocalMediaProgress>)
data class LocalLibraryItemsPayload(val value:List<LocalLibraryItem>)
data class LocalFoldersPayload(val value:List<LocalFolder>)
@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
//

View file

@ -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 ->

View file

@ -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<PlaybackSession>(it.toString())
cb(playbackSession)
}

View file

@ -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()

View file

@ -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() {

View file

@ -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)

View file

@ -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() {

View file

@ -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,18 +239,23 @@ export default {
})
if (value) {
this.resettingProgress = true
this.$axios
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`)
this.resettingProgress = false
})
.catch((error) => {
console.error('Progress reset failed', error)
this.resettingProgress = false
})
}
this.resettingProgress = false
}
},
itemUpdated(libraryItem) {
if (libraryItem.id === this.libraryItemId) {

View file

@ -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
}
}

View file

@ -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) => {

View file

@ -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)
}
}