This commit is contained in:
advplyr 2022-04-15 20:48:39 -05:00
parent ccba8dc3c7
commit ae195e7b58
32 changed files with 626 additions and 191 deletions

View file

@ -12,8 +12,9 @@ class CastOptionsProvider : OptionsProvider {
override fun getCastOptions(context: Context): CastOptions { override fun getCastOptions(context: Context): CastOptions {
Log.d("CastOptionsProvider", "getCastOptions") Log.d("CastOptionsProvider", "getCastOptions")
var appId = "FD1F76C5" var appId = "FD1F76C5"
var defaultId =CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID
return CastOptions.Builder() return CastOptions.Builder()
.setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID).setCastMediaOptions( .setReceiverApplicationId(appId).setCastMediaOptions(
CastMediaOptions.Builder() CastMediaOptions.Builder()
// We manage the media session and the notifications ourselves. // We manage the media session and the notifications ourselves.
.setMediaSessionEnabled(false) .setMediaSessionEnabled(false)

View file

@ -2,6 +2,11 @@ package com.audiobookshelf.app.data
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.jsonschema.JsonSerializableSchema
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class AudioProbeStream( data class AudioProbeStream(

View file

@ -71,6 +71,8 @@ open class MediaType(var metadata:MediaTypeMetadata, var coverPath:String?) {
open fun addAudioTrack(audioTrack:AudioTrack) { } open fun addAudioTrack(audioTrack:AudioTrack) { }
@JsonIgnore @JsonIgnore
open fun removeAudioTrack(localFileId:String) { } open fun removeAudioTrack(localFileId:String) { }
@JsonIgnore
open fun getLocalCopy():MediaType { return MediaType(MediaTypeMetadata(""),null) }
} }
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
@ -92,10 +94,11 @@ class Podcast(
episodes = episodes?.filter { ep -> episodes = episodes?.filter { ep ->
audioTracks.find { it.localFileId == ep.audioTrack?.localFileId } != null audioTracks.find { it.localFileId == ep.audioTrack?.localFileId } != null
} as MutableList<PodcastEpisode> } as MutableList<PodcastEpisode>
// Add new episodes // Add new episodes
audioTracks.forEach { at -> audioTracks.forEach { at ->
if (episodes?.find{ it.audioTrack?.localFileId == at.localFileId } == null) { if (episodes?.find{ it.audioTrack?.localFileId == at.localFileId } == null) {
var newEpisode = PodcastEpisode("local_" + at.localFileId,episodes?.size ?: 0 + 1,null,null,at.title,null,null,null,at) var newEpisode = PodcastEpisode("local_" + at.localFileId,episodes?.size ?: 0 + 1,null,null,at.title,null,null,null,at,at.duration,0, null)
episodes?.add(newEpisode) episodes?.add(newEpisode)
} }
} }
@ -108,7 +111,7 @@ class Podcast(
} }
@JsonIgnore @JsonIgnore
override fun addAudioTrack(audioTrack:AudioTrack) { override fun addAudioTrack(audioTrack:AudioTrack) {
var newEpisode = PodcastEpisode("local_" + audioTrack.localFileId,episodes?.size ?: 0 + 1,null,null,audioTrack.title,null,null,null,audioTrack) var newEpisode = PodcastEpisode("local_" + audioTrack.localFileId,episodes?.size ?: 0 + 1,null,null,audioTrack.title,null,null,null,audioTrack,audioTrack.duration,0, null)
episodes?.add(newEpisode) episodes?.add(newEpisode)
var index = 1 var index = 1
@ -129,7 +132,7 @@ class Podcast(
} }
@JsonIgnore @JsonIgnore
fun addEpisode(audioTrack:AudioTrack, episode:PodcastEpisode) { fun addEpisode(audioTrack:AudioTrack, episode:PodcastEpisode) {
var newEpisode = PodcastEpisode("local_" + episode.id,episodes?.size ?: 0 + 1,episode.episode,episode.episodeType,episode.title,episode.subtitle,episode.description,null,audioTrack) var newEpisode = PodcastEpisode("local_" + episode.id,episodes?.size ?: 0 + 1,episode.episode,episode.episodeType,episode.title,episode.subtitle,episode.description,null,audioTrack,audioTrack.duration,0, episode.id)
episodes?.add(newEpisode) episodes?.add(newEpisode)
var index = 1 var index = 1
@ -138,6 +141,12 @@ class Podcast(
index++ index++
} }
} }
// Used for FolderScanner local podcast item to get copy of Podcast excluding episodes
@JsonIgnore
override fun getLocalCopy(): Podcast {
return Podcast(metadata as PodcastMetadata,coverPath,tags, mutableListOf(),autoDownloadEpisodes)
}
} }
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
@ -158,7 +167,7 @@ class Book(
@JsonIgnore @JsonIgnore
override fun setAudioTracks(audioTracks:MutableList<AudioTrack>) { override fun setAudioTracks(audioTracks:MutableList<AudioTrack>) {
tracks = audioTracks tracks = audioTracks
tracks?.sortBy { it.index }
// TODO: Is it necessary to calculate this each time? check if can remove safely // TODO: Is it necessary to calculate this each time? check if can remove safely
var totalDuration = 0.0 var totalDuration = 0.0
tracks?.forEach { tracks?.forEach {
@ -195,6 +204,11 @@ class Book(
} }
duration = totalDuration duration = totalDuration
} }
@JsonIgnore
override fun getLocalCopy(): Book {
return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(),null,null)
}
} }
// This auto-detects whether it is a BookMetadata or PodcastMetadata // This auto-detects whether it is a BookMetadata or PodcastMetadata
@ -261,7 +275,10 @@ data class PodcastEpisode(
var subtitle:String?, var subtitle:String?,
var description:String?, var description:String?,
var audioFile:AudioFile?, var audioFile:AudioFile?,
var audioTrack:AudioTrack? var audioTrack:AudioTrack?,
var duration:Double?,
var size:Long?,
var serverEpisodeId:String? // For local podcasts to match with server podcasts
) )
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
@ -275,7 +292,8 @@ data class FileMetadata(
var filename:String, var filename:String,
var ext:String, var ext:String,
var path:String, var path:String,
var relPath:String var relPath:String,
var size:Long?
) )
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)

View file

@ -4,6 +4,7 @@ import android.util.Log
import com.audiobookshelf.app.plugins.AbsDownloader import com.audiobookshelf.app.plugins.AbsDownloader
import io.paperdb.Paper import io.paperdb.Paper
import org.json.JSONObject import org.json.JSONObject
import java.io.File
class DbManager { class DbManager {
val tag = "DbManager" val tag = "DbManager"
@ -130,6 +131,92 @@ class DbManager {
Paper.book("localMediaProgress").delete(localMediaProgressId) Paper.book("localMediaProgress").delete(localMediaProgressId)
} }
fun removeAllLocalMediaProgress() {
Paper.book("localMediaProgress").destroy()
}
// Make sure all local file ids still exist
fun cleanLocalLibraryItems() {
var localLibraryItems = getLocalLibraryItems()
localLibraryItems.forEach { lli ->
var hasUpates = false
// Check local files
lli.localFiles = lli.localFiles.filter { localFile ->
var file = File(localFile.absolutePath)
if (!file.exists()) {
Log.d(tag, "cleanLocalLibraryItems: Local file ${localFile.absolutePath} was removed from library item ${lli.media.metadata.title}")
hasUpates = true
}
file.exists()
} as MutableList<LocalFile>
// Check audio tracks and episodes
if (lli.isPodcast) {
var podcast = lli.media as Podcast
podcast.episodes = podcast.episodes?.filter { ep ->
if (lli.localFiles.find { lf -> lf.id == ep.audioTrack?.localFileId } == null) {
Log.d(tag, "cleanLocalLibraryItems: Podcast episode ${ep.title} was removed from library item ${lli.media.metadata.title}")
hasUpates = true
}
ep.audioTrack != null && lli.localFiles.find { lf -> lf.id == ep.audioTrack?.localFileId } != null
} as MutableList<PodcastEpisode>
} else {
var book = lli.media as Book
book.tracks = book.tracks?.filter { track ->
if (lli.localFiles.find { lf -> lf.id == track.localFileId } == null) {
Log.d(tag, "cleanLocalLibraryItems: Audio track ${track.title} was removed from library item ${lli.media.metadata.title}")
hasUpates = true
}
lli.localFiles.find { lf -> lf.id == track.localFileId } != null
} as MutableList<AudioTrack>
}
// Check cover still there
lli.coverAbsolutePath?.let {
var coverFile = File(it)
if (!coverFile.exists()) {
Log.d(tag, "cleanLocalLibraryItems: Cover $it was removed from library item ${lli.media.metadata.title}")
lli.coverAbsolutePath = null
lli.coverContentUrl = null
hasUpates = true
}
}
if (hasUpates) {
Log.d(tag, "cleanLocalLibraryItems: Saving local library item ${lli.id}")
Paper.book("localLibraryItems").write(lli.id, lli)
}
}
}
// Remove any local media progress where the local media item is not found
fun cleanLocalMediaProgress() {
var localMediaProgress = getAllLocalMediaProgress()
var localLibraryItems = getLocalLibraryItems()
localMediaProgress.forEach {
var matchingLLI = localLibraryItems.find { lli -> lli.id == it.localLibraryItemId }
if (matchingLLI == null) {
Log.d(tag, "cleanLocalMediaProgress: No matching local library item for local media progress ${it.id} - removing")
Paper.book("localMediaProgress").delete(it.id)
} else if (matchingLLI.isPodcast) {
if (it.localEpisodeId.isNullOrEmpty()) {
Log.d(tag, "cleanLocalMediaProgress: Podcast media progress has no episode id - removing")
Paper.book("localMediaProgress").delete(it.id)
} else {
var podcast = matchingLLI.media as Podcast
var matchingLEp = podcast.episodes?.find { ep -> ep.id == it.localEpisodeId }
if (matchingLEp == null) {
Log.d(tag, "cleanLocalMediaProgress: Podcast media progress for episode ${it.localEpisodeId} not found - removing")
Paper.book("localMediaProgress").delete(it.id)
}
}
}
}
}
fun saveLocalPlaybackSession(playbackSession:PlaybackSession) { fun saveLocalPlaybackSession(playbackSession:PlaybackSession) {
Paper.book("localPlaybackSession").write(playbackSession.id,playbackSession) Paper.book("localPlaybackSession").write(playbackSession.id,playbackSession)
} }

View file

@ -1,5 +1,6 @@
package com.audiobookshelf.app.data package com.audiobookshelf.app.data
import android.util.Log
import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.DeviceManager
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@ -26,6 +27,9 @@ data class LocalLibraryItem(
var libraryItemId:String? var libraryItemId:String?
) { ) {
@get:JsonIgnore
val isPodcast get() = mediaType == "podcast"
@JsonIgnore @JsonIgnore
fun getDuration():Double { fun getDuration():Double {
var total = 0.0 var total = 0.0
@ -50,25 +54,26 @@ data class LocalLibraryItem(
} }
@JsonIgnore @JsonIgnore
fun getPlaybackSession(episodeId:String):PlaybackSession { fun getPlaybackSession(episode:PodcastEpisode?):PlaybackSession {
var sessionId = "play-${UUID.randomUUID()}" var localEpisodeId = episode?.id
var sessionId = "play_local_${UUID.randomUUID()}"
val mediaProgressId = if (episodeId.isNullOrEmpty()) id else "$id-$episodeId" val mediaProgressId = if (localEpisodeId.isNullOrEmpty()) id else "$id-$localEpisodeId"
var mediaProgress = DeviceManager.dbManager.getLocalMediaProgress(mediaProgressId) var mediaProgress = DeviceManager.dbManager.getLocalMediaProgress(mediaProgressId)
var currentTime = mediaProgress?.currentTime ?: 0.0 var currentTime = mediaProgress?.currentTime ?: 0.0
// TODO: Clean up add mediaType methods for displayTitle and displayAuthor // TODO: Clean up add mediaType methods for displayTitle and displayAuthor
var mediaMetadata = media.metadata var mediaMetadata = media.metadata
var chapters = if (mediaType == "book") (media as Book).chapters else mutableListOf() var chapters = if (mediaType == "book") (media as Book).chapters else mutableListOf()
var authorName = "Unknown" var audioTracks = media.getAudioTracks() as MutableList<AudioTrack>
if (mediaType == "book") { var authorName = mediaMetadata.getAuthorDisplayName()
var bookMetadata = mediaMetadata as BookMetadata if (episode != null) { // Get podcast episode audio track
authorName = bookMetadata?.authorName ?: "Unknown" episode.audioTrack?.let { at -> mutableListOf(at) }?.let { tracks -> audioTracks = tracks }
Log.d("LocalLibraryItem", "getPlaybackSession: Got podcast episode audio track ${audioTracks.size}")
} }
var episodeIdNullable = if (episodeId.isNullOrEmpty()) null else episodeId
var dateNow = System.currentTimeMillis() var dateNow = System.currentTimeMillis()
return PlaybackSession(sessionId,serverUserId,libraryItemId,episodeIdNullable, mediaType, mediaMetadata, chapters ?: mutableListOf(), mediaMetadata.title, authorName,null,getDuration(),PLAYMETHOD_LOCAL,dateNow,0L,0L, media.getAudioTracks() as MutableList<AudioTrack>,currentTime,null,this,serverConnectionConfigId, serverAddress) return PlaybackSession(sessionId,serverUserId,libraryItemId,episode?.serverEpisodeId, mediaType, mediaMetadata, chapters ?: mutableListOf(), mediaMetadata.title, authorName,null,getDuration(),PLAYMETHOD_LOCAL,dateNow,0L,0L, audioTracks,currentTime,null,this,localEpisodeId,serverConnectionConfigId, serverAddress)
} }
@JsonIgnore @JsonIgnore

View file

@ -65,6 +65,7 @@ data class LocalMediaItem(
return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true,null,null,null,null) return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true,null,null,null,null)
} else { } else {
var podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false) var podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false)
podcast.setAudioTracks(audioTracks) // Builds episodes from audio tracks
return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false, mediaType, podcast,localFiles,coverContentUrl, coverAbsolutePath, true, null,null,null,null) return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false, mediaType, podcast,localFiles,coverContentUrl, coverAbsolutePath, true, null,null,null,null)
} }
} }

View file

@ -1,12 +1,13 @@
package com.audiobookshelf.app.data package com.audiobookshelf.app.data
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class LocalMediaProgress( data class LocalMediaProgress(
var id:String, var id:String,
var localLibraryItemId:String, var localLibraryItemId:String,
var episodeId:String?, var localEpisodeId:String?,
var duration:Double, var duration:Double,
var progress:Double, // 0 to 1 var progress:Double, // 0 to 1
var currentTime:Double, var currentTime:Double,
@ -18,5 +19,27 @@ data class LocalMediaProgress(
var serverConnectionConfigId:String?, var serverConnectionConfigId:String?,
var serverAddress:String?, var serverAddress:String?,
var serverUserId:String?, var serverUserId:String?,
var libraryItemId:String? var libraryItemId:String?,
) var episodeId:String?
) {
@JsonIgnore
fun updateIsFinished(finished:Boolean) {
if (isFinished != finished) { // If finished changed then set progress
progress = if (finished) 1.0 else 0.0
}
isFinished = finished
lastUpdate = System.currentTimeMillis()
finishedAt = if (isFinished) lastUpdate else null
}
@JsonIgnore
fun updateFromPlaybackSession(playbackSession:PlaybackSession) {
currentTime = playbackSession.currentTime
progress = playbackSession.progress
lastUpdate = System.currentTimeMillis()
isFinished = playbackSession.progress >= 0.99
finishedAt = if (isFinished) lastUpdate else null
}
}

View file

@ -40,6 +40,7 @@ class PlaybackSession(
var currentTime:Double, var currentTime:Double,
var libraryItem:LibraryItem?, var libraryItem:LibraryItem?,
var localLibraryItem:LocalLibraryItem?, var localLibraryItem:LocalLibraryItem?,
var localEpisodeId:String?,
var serverConnectionConfigId:String?, var serverConnectionConfigId:String?,
var serverAddress:String? var serverAddress:String?
) { ) {
@ -53,7 +54,7 @@ class PlaybackSession(
@get:JsonIgnore @get:JsonIgnore
val localLibraryItemId get() = localLibraryItem?.id ?: "" val localLibraryItemId get() = localLibraryItem?.id ?: ""
@get:JsonIgnore @get:JsonIgnore
val localMediaProgressId get() = if (episodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$episodeId" val localMediaProgressId get() = if (episodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
@get:JsonIgnore @get:JsonIgnore
val progress get() = currentTime / getTotalDuration() val progress get() = currentTime / getTotalDuration()
@ -175,7 +176,7 @@ class PlaybackSession(
@JsonIgnore @JsonIgnore
fun clone():PlaybackSession { fun clone():PlaybackSession {
return PlaybackSession(id,userId,libraryItemId,episodeId,mediaType,mediaMetadata,chapters,displayTitle,displayAuthor,coverPath,duration,playMethod,startedAt,updatedAt,timeListening,audioTracks,currentTime,libraryItem,localLibraryItem,serverConnectionConfigId,serverAddress) return PlaybackSession(id,userId,libraryItemId,episodeId,mediaType,mediaMetadata,chapters,displayTitle,displayAuthor,coverPath,duration,playMethod,startedAt,updatedAt,timeListening,audioTracks,currentTime,libraryItem,localLibraryItem,localEpisodeId,serverConnectionConfigId,serverAddress)
} }
@JsonIgnore @JsonIgnore
@ -187,6 +188,6 @@ class PlaybackSession(
@JsonIgnore @JsonIgnore
fun getNewLocalMediaProgress():LocalMediaProgress { fun getNewLocalMediaProgress():LocalMediaProgress {
return LocalMediaProgress(localMediaProgressId,localLibraryItemId,episodeId,getTotalDuration(),progress,currentTime,false,updatedAt,startedAt,null,serverConnectionConfigId,serverAddress,userId,libraryItemId) return LocalMediaProgress(localMediaProgressId,localLibraryItemId,localEpisodeId,getTotalDuration(),progress,currentTime,false,updatedAt,startedAt,null,serverConnectionConfigId,serverAddress,userId,libraryItemId,episodeId)
} }
} }

View file

@ -11,6 +11,7 @@ object DeviceManager {
var deviceData:DeviceData = dbManager.getDeviceData() var deviceData:DeviceData = dbManager.getDeviceData()
var serverConnectionConfig: ServerConnectionConfig? = null var serverConnectionConfig: ServerConnectionConfig? = null
val serverConnectionConfigId get() = serverConnectionConfig?.id ?: ""
val serverAddress get() = serverConnectionConfig?.address ?: "" val serverAddress get() = serverConnectionConfig?.address ?: ""
val serverUserId get() = serverConnectionConfig?.userId ?: "" val serverUserId get() = serverConnectionConfig?.userId ?: ""
val token get() = serverConnectionConfig?.token ?: "" val token get() = serverConnectionConfig?.token ?: ""
@ -20,6 +21,6 @@ object DeviceManager {
} }
fun getBase64Id(id:String):String { fun getBase64Id(id:String):String {
return android.util.Base64.encodeToString(id.toByteArray(), android.util.Base64.DEFAULT) return android.util.Base64.encodeToString(id.toByteArray(), android.util.Base64.NO_WRAP)
} }
} }

View file

@ -10,11 +10,15 @@ import com.arthenica.ffmpegkit.FFprobeKit
import com.arthenica.ffmpegkit.Level import com.arthenica.ffmpegkit.Level
import com.audiobookshelf.app.data.* import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.plugins.AbsDownloader import com.audiobookshelf.app.plugins.AbsDownloader
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import com.getcapacitor.JSObject
class FolderScanner(var ctx: Context) { class FolderScanner(var ctx: Context) {
private val tag = "FolderScanner" private val tag = "FolderScanner"
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
private fun getLocalLibraryItemId(mediaItemId:String):String { private fun getLocalLibraryItemId(mediaItemId:String):String {
return "local_" + DeviceManager.getBase64Id(mediaItemId) return "local_" + DeviceManager.getBase64Id(mediaItemId)
@ -52,6 +56,7 @@ class FolderScanner(var ctx: Context) {
// Remove existing items no longer there // Remove existing items no longer there
existingLocalLibraryItems = existingLocalLibraryItems.filter { lli -> existingLocalLibraryItems = existingLocalLibraryItems.filter { lli ->
Log.d(tag, "scanForMediaItems Checking Existing LLI ${lli.id}")
var fileFound = foldersFound.find { f -> lli.id == getLocalLibraryItemId(f.id) } var fileFound = foldersFound.find { f -> lli.id == getLocalLibraryItemId(f.id) }
if (fileFound == null) { if (fileFound == null) {
Log.d(tag, "Existing local library item is no longer in file system ${lli.media.metadata.title}") Log.d(tag, "Existing local library item is no longer in file system ${lli.media.metadata.title}")
@ -129,25 +134,22 @@ class FolderScanner(var ctx: Context) {
var existingAudioTrack = existingAudioTracks.find { eat -> eat.localFileId == localFileId } var existingAudioTrack = existingAudioTracks.find { eat -> eat.localFileId == localFileId }
if (existingAudioTrack != null) { // Update existing audio track if (existingAudioTrack != null) { // Update existing audio track
if (existingAudioTrack.index != index) { if (existingAudioTrack.index != index) {
Log.d(tag, "Updating Audio track index from ${existingAudioTrack.index} to $index") Log.d(tag, "scanLibraryItemFolder Updating Audio track index from ${existingAudioTrack.index} to $index")
existingAudioTrack.index = index existingAudioTrack.index = index
isNewOrUpdated = true isNewOrUpdated = true
} }
if (existingAudioTrack.startOffset != startOffset) { if (existingAudioTrack.startOffset != startOffset) {
Log.d(tag, "Updating Audio track startOffset ${existingAudioTrack.startOffset} to $startOffset") Log.d(tag, "scanLibraryItemFolder Updating Audio track startOffset ${existingAudioTrack.startOffset} to $startOffset")
existingAudioTrack.startOffset = startOffset existingAudioTrack.startOffset = startOffset
isNewOrUpdated = true isNewOrUpdated = true
} }
} }
if (existingAudioTrack == null || forceAudioProbe) { if (existingAudioTrack == null || forceAudioProbe) {
Log.d(tag, "Scanning Audio File Path ${localFile.absolutePath}") Log.d(tag, "scanLibraryItemFolder Scanning Audio File Path ${localFile.absolutePath} | ForceAudioProbe=${forceAudioProbe}")
// TODO: Make asynchronous // TODO: Make asynchronous
var session = FFprobeKit.execute("-i \"${localFile.absolutePath}\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet") var audioProbeResult = probeAudioFile(localFile.absolutePath)
val audioProbeResult = jacksonObjectMapper().readValue<AudioProbeResult>(session.output)
Log.d(tag, "Probe Result DATA ${audioProbeResult.duration} | ${audioProbeResult.size} | ${audioProbeResult.title} | ${audioProbeResult.artist}")
if (existingAudioTrack != null) { if (existingAudioTrack != null) {
// Update audio probe data on existing audio track // Update audio probe data on existing audio track
@ -172,10 +174,12 @@ class FolderScanner(var ctx: Context) {
var existingLocalFile = existingLocalFiles.find { elf -> elf.id == localFileId } var existingLocalFile = existingLocalFiles.find { elf -> elf.id == localFileId }
if (existingLocalFile == null) { if (existingLocalFile == null) {
Log.d(tag, "scanLibraryItemFolder new local file found ${localFile.absolutePath}")
isNewOrUpdated = true isNewOrUpdated = true
} }
if (existingItem != null && existingItem.coverContentUrl == null) { if (existingItem != null && existingItem.coverContentUrl == null) {
// Existing media item did not have a cover - cover found on scan // Existing media item did not have a cover - cover found on scan
Log.d(tag, "scanLibraryItemFolder setting cover ${localFile.absolutePath}")
isNewOrUpdated = true isNewOrUpdated = true
existingItem.coverAbsolutePath = localFile.absolutePath existingItem.coverAbsolutePath = localFile.absolutePath
existingItem.coverContentUrl = localFile.contentUrl existingItem.coverContentUrl = localFile.contentUrl
@ -217,11 +221,14 @@ class FolderScanner(var ctx: Context) {
fun scanDownloadItem(downloadItem: AbsDownloader.DownloadItem):LocalLibraryItem? { fun scanDownloadItem(downloadItem: AbsDownloader.DownloadItem):LocalLibraryItem? {
var folderDf = DocumentFileCompat.fromUri(ctx, Uri.parse(downloadItem.localFolder.contentUrl)) var folderDf = DocumentFileCompat.fromUri(ctx, Uri.parse(downloadItem.localFolder.contentUrl))
var foldersFound = folderDf?.search(false, DocumentFileType.FOLDER) ?: mutableListOf() var foldersFound = folderDf?.search(false, DocumentFileType.FOLDER) ?: mutableListOf()
var itemFolderId = ""
var itemFolderUrl = "" var itemFolderUrl = ""
var itemFolderBasePath = "" var itemFolderBasePath = ""
var itemFolderAbsolutePath = "" var itemFolderAbsolutePath = ""
foldersFound.forEach { foldersFound.forEach {
if (it.name == downloadItem.itemTitle) { if (it.name == downloadItem.itemTitle) {
itemFolderId = it.id
itemFolderUrl = it.uri.toString() itemFolderUrl = it.uri.toString()
itemFolderBasePath = it.getBasePath(ctx) itemFolderBasePath = it.getBasePath(ctx)
itemFolderAbsolutePath = it.getAbsolutePath(ctx) itemFolderAbsolutePath = it.getAbsolutePath(ctx)
@ -238,7 +245,9 @@ class FolderScanner(var ctx: Context) {
Log.e(tag, "Folder Doc File Invalid ${downloadItem.itemFolderPath}") Log.e(tag, "Folder Doc File Invalid ${downloadItem.itemFolderPath}")
return null return null
} }
Log.d(tag, "scanDownloadItem starting for ${downloadItem.itemFolderPath} | ${df.uri}")
var localLibraryItemId = getLocalLibraryItemId(itemFolderId)
Log.d(tag, "scanDownloadItem starting for ${downloadItem.itemFolderPath} | ${df.uri} | Item Folder Id:$itemFolderId | LLI Id:$localLibraryItemId")
// Search for files in media item folder // Search for files in media item folder
var filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*")) var filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*"))
@ -246,13 +255,13 @@ class FolderScanner(var ctx: Context) {
var localLibraryItem:LocalLibraryItem? = null var localLibraryItem:LocalLibraryItem? = null
if (downloadItem.mediaType == "book") { if (downloadItem.mediaType == "book") {
localLibraryItem = LocalLibraryItem("local_${downloadItem.libraryItemId}", downloadItem.localFolder.id, itemFolderBasePath, itemFolderAbsolutePath, itemFolderUrl, false, downloadItem.mediaType, downloadItem.media, mutableListOf(), null, null, true, downloadItem.serverConnectionConfigId, downloadItem.serverAddress, downloadItem.serverUserId, downloadItem.libraryItemId) localLibraryItem = LocalLibraryItem(localLibraryItemId, downloadItem.localFolder.id, itemFolderBasePath, itemFolderAbsolutePath, itemFolderUrl, false, downloadItem.mediaType, downloadItem.media.getLocalCopy(), mutableListOf(), null, null, true, downloadItem.serverConnectionConfigId, downloadItem.serverAddress, downloadItem.serverUserId, downloadItem.libraryItemId)
} else { } else {
// Lookup or create podcast local library item // Lookup or create podcast local library item
localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem("local_${downloadItem.libraryItemId}") localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
if (localLibraryItem == null) { if (localLibraryItem == null) {
Log.d(tag, "Podcast local library item not created yet for ${downloadItem.media.metadata.title}") Log.d(tag, "[FolderScanner] Podcast local library item not created yet for ${downloadItem.media.metadata.title}")
localLibraryItem = LocalLibraryItem("local_${downloadItem.libraryItemId}", downloadItem.localFolder.id, itemFolderBasePath, itemFolderAbsolutePath, itemFolderUrl, false, downloadItem.mediaType, downloadItem.media, mutableListOf(), null, null, true,downloadItem.serverConnectionConfigId,downloadItem.serverAddress,downloadItem.serverUserId,downloadItem.libraryItemId) localLibraryItem = LocalLibraryItem(localLibraryItemId, downloadItem.localFolder.id, itemFolderBasePath, itemFolderAbsolutePath, itemFolderUrl, false, downloadItem.mediaType, downloadItem.media.getLocalCopy(), mutableListOf(), null, null, true,downloadItem.serverConnectionConfigId,downloadItem.serverAddress,downloadItem.serverUserId,downloadItem.libraryItemId)
} }
} }
@ -268,25 +277,26 @@ class FolderScanner(var ctx: Context) {
} }
} else if (itemPart.audioTrack != null) { // Is audio track } else if (itemPart.audioTrack != null) { // Is audio track
var audioTrackFromServer = itemPart.audioTrack var audioTrackFromServer = itemPart.audioTrack
Log.d(tag, "scanDownloadItem: Audio Track from Server index = ${audioTrackFromServer?.index}")
var localFileId = DeviceManager.getBase64Id(docFile.id) var localFileId = DeviceManager.getBase64Id(docFile.id)
var localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length()) var localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
localLibraryItem.localFiles.add(localFile) localLibraryItem.localFiles.add(localFile)
// TODO: Make asynchronous // TODO: Make asynchronous
var session = FFprobeKit.execute("-i \"${localFile.absolutePath}\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet") var audioProbeResult = probeAudioFile(localFile.absolutePath)
val audioProbeResult = jacksonObjectMapper().readValue<AudioProbeResult>(session.output)
Log.d(tag, "Probe Result DATA ${audioProbeResult.duration} | ${audioProbeResult.size} | ${audioProbeResult.title} | ${audioProbeResult.artist}")
// Create new audio track // Create new audio track
var track = AudioTrack(audioTrackFromServer?.index ?: -1, audioTrackFromServer?.startOffset ?: 0.0, audioProbeResult.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, audioProbeResult, audioTrackFromServer?.index ?: -1) var track = AudioTrack(audioTrackFromServer?.index ?: -1, audioTrackFromServer?.startOffset ?: 0.0, audioProbeResult.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, audioProbeResult, audioTrackFromServer?.index ?: -1)
audioTracks.add(track) audioTracks.add(track)
Log.d(tag, "scanDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}")
// Add podcast episodes to library // Add podcast episodes to library
itemPart.episode?.let { podcastEpisode -> itemPart.episode?.let { podcastEpisode ->
var podcast = localLibraryItem.media as Podcast var podcast = localLibraryItem.media as Podcast
podcast.addEpisode(track, podcastEpisode) podcast.addEpisode(track, podcastEpisode)
Log.d(tag, "scanDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}")
} }
} else { // Cover image } else { // Cover image
var localFileId = DeviceManager.getBase64Id(docFile.id) var localFileId = DeviceManager.getBase64Id(docFile.id)
@ -381,10 +391,7 @@ class FolderScanner(var ctx: Context) {
if (localFile.isAudioFile()) { if (localFile.isAudioFile()) {
// TODO: Make asynchronous // TODO: Make asynchronous
var session = FFprobeKit.execute("-i \"${localFile.absolutePath}\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet") var audioProbeResult = probeAudioFile(localFile.absolutePath)
val audioProbeResult = jacksonObjectMapper().readValue<AudioProbeResult>(session.output)
Log.d(tag, "Probe Result DATA ${audioProbeResult.duration} | ${audioProbeResult.size} | ${audioProbeResult.title} | ${audioProbeResult.artist}")
var existingTrack = existingAudioTracks.find { audioTrack -> var existingTrack = existingAudioTracks.find { audioTrack ->
audioTrack.localFileId == localFile.id audioTrack.localFileId == localFile.id
@ -421,4 +428,13 @@ class FolderScanner(var ctx: Context) {
} }
return LocalLibraryItemScanResult(wasUpdated, localLibraryItem) return LocalLibraryItemScanResult(wasUpdated, localLibraryItem)
} }
fun probeAudioFile(absolutePath:String):AudioProbeResult {
var session = FFprobeKit.execute("-i \"${absolutePath}\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet")
Log.d(tag, "FFprobe output ${JSObject(session.output)}")
val audioProbeResult = jacksonMapper.readValue<AudioProbeResult>(session.output)
Log.d(tag, "Probe Result DATA ${audioProbeResult.duration} | ${audioProbeResult.size} | ${audioProbeResult.title} | ${audioProbeResult.artist}")
return audioProbeResult
}
} }

View file

@ -2,6 +2,7 @@ package com.audiobookshelf.app.player
import android.app.Activity import android.app.Activity
import android.app.AlertDialog import android.app.AlertDialog
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.appcompat.R import androidx.appcompat.R
@ -13,10 +14,7 @@ import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.ext.cast.CastPlayer import com.google.android.exoplayer2.ext.cast.CastPlayer
import com.google.android.exoplayer2.ext.cast.MediaItemConverter import com.google.android.exoplayer2.ext.cast.MediaItemConverter
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener
import com.google.android.gms.cast.Cast import com.google.android.gms.cast.*
import com.google.android.gms.cast.CastDevice
import com.google.android.gms.cast.CastMediaControlIntent
import com.google.android.gms.cast.MediaQueueItem
import com.google.android.gms.cast.framework.* import com.google.android.gms.cast.framework.*
import org.json.JSONObject import org.json.JSONObject
@ -321,12 +319,36 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
try { try {
val castContext = CastContext.getSharedInstance(mainActivity) val castContext = CastContext.getSharedInstance(mainActivity)
playerNotificationService.castPlayer = CastPlayer(castContext, CustomConverter()).apply { // Work in progress using the cast api
setSessionAvailabilityListener(CastSessionAvailabilityListener()) var currentSession = playerNotificationService.getCurrentPlaybackSessionCopy()
addListener(PlayerListener(playerNotificationService)) var firstTrack = currentSession?.audioTracks?.get(0)
var uri = firstTrack?.let { currentSession?.getContentUri(it) } ?: Uri.EMPTY
var url = uri.toString()
var mimeType = firstTrack?.mimeType ?: ""
var castMediaMetadata = firstTrack?.let { currentSession?.getCastMediaMetadata(it) }
Log.d(tag, "CastManager set url $url")
var duration = (currentSession?.getTotalDuration() ?: 0L * 1000L).toLong()
if (castMediaMetadata != null) {
Log.d(tag, "CastManager duration $duration got cast media metadata $castMediaMetadata")
val mediaInfo = MediaInfo.Builder(url)
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.setContentType(mimeType)
.setMetadata(castMediaMetadata)
.setStreamDuration(duration)
.build()
val remoteMediaClient = castSession?.remoteMediaClient
remoteMediaClient?.load(MediaLoadRequestData.Builder().setMediaInfo(mediaInfo).build())
} }
Log.d(tag, "CAST Cast Player Applied")
switchToPlayer(true) // Not working using the exo player CastPlayer
// playerNotificationService.castPlayer = CastPlayer(castContext, CustomConverter()).apply {
// setSessionAvailabilityListener(CastSessionAvailabilityListener())
// addListener(PlayerListener(playerNotificationService))
// }
// Log.d(tag, "CAST Cast Player Applied")
// switchToPlayer(true)
} catch (e: Exception) { } catch (e: Exception) {
Log.i(tag, "Cast is not available on this device. " + Log.i(tag, "Cast is not available on this device. " +
"Exception thrown when attempting to obtain CastContext. " + e.message) "Exception thrown when attempting to obtain CastContext. " + e.message)

View file

@ -110,14 +110,12 @@ class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, a
currentLocalMediaProgress = mediaProgress currentLocalMediaProgress = mediaProgress
} }
} else { } else {
currentLocalMediaProgress?.currentTime = playbackSession.currentTime currentLocalMediaProgress?.updateFromPlaybackSession(playbackSession)
currentLocalMediaProgress?.lastUpdate = playbackSession.updatedAt
currentLocalMediaProgress?.progress = playbackSession.progress
} }
currentLocalMediaProgress?.let { currentLocalMediaProgress?.let {
DeviceManager.dbManager.saveLocalMediaProgress(it) DeviceManager.dbManager.saveLocalMediaProgress(it)
playerNotificationService.clientEventEmitter?.onLocalMediaProgressUpdate(it) playerNotificationService.clientEventEmitter?.onLocalMediaProgressUpdate(it)
Log.d(tag, "Saved Local Progress Current Time: ${it.currentTime} | Duration ${it.duration} | Progress ${(it.progress * 100).roundToInt()}%") Log.d(tag, "Saved Local Progress Current Time: ID ${it.id} | ${it.currentTime} | Duration ${it.duration} | Progress ${(it.progress * 100).roundToInt()}%")
} }
} }

View file

@ -6,9 +6,7 @@ import android.os.Looper
import android.util.Log import android.util.Log
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.audiobookshelf.app.MainActivity import com.audiobookshelf.app.MainActivity
import com.audiobookshelf.app.data.LocalMediaProgress import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.data.PlaybackMetadata
import com.audiobookshelf.app.data.PlaybackSession
import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.player.CastManager import com.audiobookshelf.app.player.CastManager
import com.audiobookshelf.app.player.PlayerNotificationService import com.audiobookshelf.app.player.PlayerNotificationService
@ -22,6 +20,7 @@ import org.json.JSONObject
@CapacitorPlugin(name = "AbsAudioPlayer") @CapacitorPlugin(name = "AbsAudioPlayer")
class AbsAudioPlayer : Plugin() { class AbsAudioPlayer : Plugin() {
private val tag = "AbsAudioPlayer" private val tag = "AbsAudioPlayer"
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
lateinit var mainActivity: MainActivity lateinit var mainActivity: MainActivity
lateinit var apiHandler:ApiHandler lateinit var apiHandler:ApiHandler
@ -36,7 +35,7 @@ class AbsAudioPlayer : Plugin() {
playerNotificationService.clientEventEmitter = (object : PlayerNotificationService.ClientEventEmitter { playerNotificationService.clientEventEmitter = (object : PlayerNotificationService.ClientEventEmitter {
override fun onPlaybackSession(playbackSession: PlaybackSession) { override fun onPlaybackSession(playbackSession: PlaybackSession) {
notifyListeners("onPlaybackSession", JSObject(jacksonObjectMapper().writeValueAsString(playbackSession))) notifyListeners("onPlaybackSession", JSObject(jacksonMapper.writeValueAsString(playbackSession)))
} }
override fun onPlaybackClosed() { override fun onPlaybackClosed() {
@ -48,7 +47,7 @@ class AbsAudioPlayer : Plugin() {
} }
override fun onMetadata(metadata: PlaybackMetadata) { override fun onMetadata(metadata: PlaybackMetadata) {
notifyListeners("onMetadata", JSObject(jacksonObjectMapper().writeValueAsString(metadata))) notifyListeners("onMetadata", JSObject(jacksonMapper.writeValueAsString(metadata)))
} }
override fun onPrepare(audiobookId: String, playWhenReady: Boolean) { override fun onPrepare(audiobookId: String, playWhenReady: Boolean) {
@ -67,7 +66,7 @@ class AbsAudioPlayer : Plugin() {
} }
override fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress) { override fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress) {
notifyListeners("onLocalMediaProgressUpdate", JSObject(jacksonObjectMapper().writeValueAsString(localMediaProgress))) notifyListeners("onLocalMediaProgressUpdate", JSObject(jacksonMapper.writeValueAsString(localMediaProgress)))
} }
}) })
} }
@ -101,9 +100,19 @@ class AbsAudioPlayer : Plugin() {
if (libraryItemId.startsWith("local")) { // Play local media item if (libraryItemId.startsWith("local")) { // Play local media item
DeviceManager.dbManager.getLocalLibraryItem(libraryItemId)?.let { DeviceManager.dbManager.getLocalLibraryItem(libraryItemId)?.let {
var episode: PodcastEpisode? = null
if (!episodeId.isNullOrEmpty()) {
var podcastMedia = it.media as Podcast
episode = podcastMedia.episodes?.find { ep -> ep.id == episodeId }
if (episode == null) {
Log.e(tag, "prepareLibraryItem: Podcast episode not found $episodeId")
return call.resolve(JSObject())
}
}
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
Log.d(tag, "Preparing Local Media item ${jacksonObjectMapper().writeValueAsString(it)}") Log.d(tag, "prepareLibraryItem: Preparing Local Media item ${jacksonMapper.writeValueAsString(it)}")
var playbackSession = it.getPlaybackSession(episodeId) var playbackSession = it.getPlaybackSession(episode)
playerNotificationService.preparePlayer(playbackSession, playWhenReady) playerNotificationService.preparePlayer(playbackSession, playWhenReady)
} }
return call.resolve(JSObject()) return call.resolve(JSObject())
@ -112,11 +121,11 @@ class AbsAudioPlayer : Plugin() {
apiHandler.playLibraryItem(libraryItemId, episodeId, false) { apiHandler.playLibraryItem(libraryItemId, episodeId, false) {
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
Log.d(tag, "Preparing Player TEST ${jacksonObjectMapper().writeValueAsString(it)}") Log.d(tag, "Preparing Player TEST ${jacksonMapper.writeValueAsString(it)}")
playerNotificationService.preparePlayer(it, playWhenReady) playerNotificationService.preparePlayer(it, playWhenReady)
} }
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(it))) call.resolve(JSObject(jacksonMapper.writeValueAsString(it)))
} }
} }
} }

View file

@ -4,6 +4,7 @@ import android.util.Log
import com.audiobookshelf.app.MainActivity import com.audiobookshelf.app.MainActivity
import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.server.ApiHandler import com.audiobookshelf.app.server.ApiHandler
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.getcapacitor.* import com.getcapacitor.*
import com.getcapacitor.annotation.CapacitorPlugin import com.getcapacitor.annotation.CapacitorPlugin
@ -15,6 +16,7 @@ import org.json.JSONObject
@CapacitorPlugin(name = "AbsDatabase") @CapacitorPlugin(name = "AbsDatabase")
class AbsDatabase : Plugin() { class AbsDatabase : Plugin() {
val tag = "AbsDatabase" val tag = "AbsDatabase"
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
lateinit var mainActivity: MainActivity lateinit var mainActivity: MainActivity
lateinit var apiHandler: ApiHandler lateinit var apiHandler: ApiHandler
@ -26,13 +28,16 @@ class AbsDatabase : Plugin() {
override fun load() { override fun load() {
mainActivity = (activity as MainActivity) mainActivity = (activity as MainActivity)
apiHandler = ApiHandler(mainActivity) apiHandler = ApiHandler(mainActivity)
DeviceManager.dbManager.cleanLocalMediaProgress()
DeviceManager.dbManager.cleanLocalLibraryItems()
} }
@PluginMethod @PluginMethod
fun getDeviceData(call:PluginCall) { fun getDeviceData(call:PluginCall) {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
var deviceData = DeviceManager.dbManager.getDeviceData() var deviceData = DeviceManager.dbManager.getDeviceData()
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(deviceData))) call.resolve(JSObject(jacksonMapper.writeValueAsString(deviceData)))
} }
} }
@ -40,7 +45,7 @@ class AbsDatabase : Plugin() {
fun getLocalFolders(call:PluginCall) { fun getLocalFolders(call:PluginCall) {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
var folders = DeviceManager.dbManager.getAllLocalFolders() var folders = DeviceManager.dbManager.getAllLocalFolders()
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(LocalFoldersPayload(folders)))) call.resolve(JSObject(jacksonMapper.writeValueAsString(LocalFoldersPayload(folders))))
} }
} }
@ -49,7 +54,7 @@ class AbsDatabase : Plugin() {
var folderId = call.getString("folderId", "").toString() var folderId = call.getString("folderId", "").toString()
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
DeviceManager.dbManager.getLocalFolder(folderId)?.let { DeviceManager.dbManager.getLocalFolder(folderId)?.let {
var folderObj = jacksonObjectMapper().writeValueAsString(it) var folderObj = jacksonMapper.writeValueAsString(it)
call.resolve(JSObject(folderObj)) call.resolve(JSObject(folderObj))
} ?: call.resolve() } ?: call.resolve()
} }
@ -64,7 +69,7 @@ class AbsDatabase : Plugin() {
if (localLibraryItem == null) { if (localLibraryItem == null) {
call.resolve() call.resolve()
} else { } else {
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(localLibraryItem))) call.resolve(JSObject(jacksonMapper.writeValueAsString(localLibraryItem)))
} }
} }
} }
@ -77,7 +82,7 @@ class AbsDatabase : Plugin() {
if (localLibraryItem == null) { if (localLibraryItem == null) {
call.resolve() call.resolve()
} else { } else {
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(localLibraryItem))) call.resolve(JSObject(jacksonMapper.writeValueAsString(localLibraryItem)))
} }
} }
} }
@ -88,7 +93,7 @@ class AbsDatabase : Plugin() {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
var localLibraryItems = DeviceManager.dbManager.getLocalLibraryItems(mediaType) var localLibraryItems = DeviceManager.dbManager.getLocalLibraryItems(mediaType)
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(LocalLibraryItemsPayload(localLibraryItems)))) call.resolve(JSObject(jacksonMapper.writeValueAsString(LocalLibraryItemsPayload(localLibraryItems))))
} }
} }
@ -97,7 +102,7 @@ class AbsDatabase : Plugin() {
var folderId = call.getString("folderId", "").toString() var folderId = call.getString("folderId", "").toString()
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
var localLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(folderId) var localLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(folderId)
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(LocalLibraryItemsPayload(localLibraryItems)))) call.resolve(JSObject(jacksonMapper.writeValueAsString(LocalLibraryItemsPayload(localLibraryItems))))
} }
} }
@ -143,7 +148,7 @@ class AbsDatabase : Plugin() {
} }
DeviceManager.serverConnectionConfig = serverConnectionConfig DeviceManager.serverConnectionConfig = serverConnectionConfig
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(DeviceManager.serverConnectionConfig))) call.resolve(JSObject(jacksonMapper.writeValueAsString(DeviceManager.serverConnectionConfig)))
} }
} }
@ -177,7 +182,7 @@ class AbsDatabase : Plugin() {
fun getAllLocalMediaProgress(call:PluginCall) { fun getAllLocalMediaProgress(call:PluginCall) {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
var localMediaProgress = DeviceManager.dbManager.getAllLocalMediaProgress() var localMediaProgress = DeviceManager.dbManager.getAllLocalMediaProgress()
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(LocalMediaProgressPayload(localMediaProgress)))) call.resolve(JSObject(jacksonMapper.writeValueAsString(LocalMediaProgressPayload(localMediaProgress))))
} }
} }
@ -195,7 +200,51 @@ class AbsDatabase : Plugin() {
return call.resolve() return call.resolve()
} }
apiHandler.syncMediaProgress { apiHandler.syncMediaProgress {
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(it))) call.resolve(JSObject(jacksonMapper.writeValueAsString(it)))
}
}
@PluginMethod
fun updateLocalMediaProgressFinished(call:PluginCall) {
var localMediaProgressId = call.getString("localMediaProgressId", "").toString()
var isFinished = call.getBoolean("isFinished", false) == true
Log.d(tag, "updateLocalMediaProgressFinished $localMediaProgressId | Is Finished:$isFinished")
var localMediaProgress = DeviceManager.dbManager.getLocalMediaProgress(localMediaProgressId)
if (localMediaProgress == null) {
Log.e(tag, "updateLocalMediaProgressFinished Local Media Progress not found $localMediaProgressId")
call.resolve(JSObject("{\"error\":\"Progress not found\"}"))
} else {
localMediaProgress.updateIsFinished(isFinished)
var lmpstring = jacksonMapper.writeValueAsString(localMediaProgress)
Log.d(tag, "updateLocalMediaProgressFinished: Local Media Progress String $lmpstring")
// Send update to server media progress is linked to a server and user is logged into that server
localMediaProgress.serverConnectionConfigId?.let { configId ->
if (DeviceManager.serverConnectionConfigId == configId) {
var libraryItemId = localMediaProgress.libraryItemId ?: ""
var episodeId = localMediaProgress.episodeId ?: ""
var updatePayload = JSObject()
updatePayload.put("isFinished", isFinished)
apiHandler.updateMediaProgress(libraryItemId,episodeId,updatePayload) {
Log.d(tag, "updateLocalMediaProgressFinished: Updated media progress isFinished on server")
var jsobj = JSObject()
jsobj.put("local", true)
jsobj.put("server", true)
jsobj.put("localMediaProgress", JSObject(lmpstring))
call.resolve(jsobj)
// call.resolve(JSObject("{\"local\":true,\"server\":true,\"localMediaProgress\":$lmpstring}"))
}
}
}
if (localMediaProgress.serverConnectionConfigId == null || DeviceManager.serverConnectionConfigId != localMediaProgress.serverConnectionConfigId) {
// call.resolve(JSObject("{\"local\":true,\"localMediaProgress\":$lmpstring}}"))
var jsobj = JSObject()
jsobj.put("local", true)
jsobj.put("server", false)
jsobj.put("localMediaProgress", JSObject(lmpstring))
call.resolve(jsobj)
}
} }
} }
@ -208,17 +257,37 @@ class AbsDatabase : Plugin() {
return return
} }
var audioTracks = localLibraryItem.media.getAudioTracks() as MutableList
var tracks:JSArray = call.getArray("tracks") ?: JSArray() var tracks:JSArray = call.getArray("tracks") ?: JSArray()
Log.d(tag, "updateLocalTrackOrder $tracks") Log.d(tag, "updateLocalTrackOrder $tracks")
for (i in 0..tracks.length()) { var index = 1
var hasUpdates = false
for (i in 0 until tracks.length()) {
var track = tracks.getJSONObject(i) var track = tracks.getJSONObject(i)
var localFileId = track.getString("localFileId") var localFileId = track.getString("localFileId")
Log.d(tag, "LOCAL FILE ID $localFileId")
var existingTrack = audioTracks.find{ it.localFileId == localFileId }
if (existingTrack != null) {
Log.d(tag, "Found existing track ${existingTrack.localFileId} that has index ${existingTrack.index} should be index $index")
if (existingTrack.index != index) hasUpdates = true
existingTrack.index = index++
} else {
Log.e(tag, "Audio track with local file id not found")
}
} }
if (hasUpdates) {
Log.d(tag, "Save library item track orders")
localLibraryItem.media.setAudioTracks(audioTracks)
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
call.resolve(JSObject(jacksonMapper.writeValueAsString(localLibraryItem)))
} else {
Log.d(tag, "No tracks need to be updated")
call.resolve() call.resolve()
} }
}
// //
// Generic Webview calls to db // Generic Webview calls to db

View file

@ -11,6 +11,7 @@ import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.device.FolderScanner import com.audiobookshelf.app.device.FolderScanner
import com.audiobookshelf.app.server.ApiHandler import com.audiobookshelf.app.server.ApiHandler
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.getcapacitor.JSObject import com.getcapacitor.JSObject
import com.getcapacitor.Plugin import com.getcapacitor.Plugin
@ -28,6 +29,7 @@ import java.util.*
@CapacitorPlugin(name = "AbsDownloader") @CapacitorPlugin(name = "AbsDownloader")
class AbsDownloader : Plugin() { class AbsDownloader : Plugin() {
private val tag = "AbsDownloader" private val tag = "AbsDownloader"
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
lateinit var mainActivity: MainActivity lateinit var mainActivity: MainActivity
lateinit var downloadManager: DownloadManager lateinit var downloadManager: DownloadManager
@ -294,7 +296,7 @@ class AbsDownloader : Plugin() {
DeviceManager.dbManager.saveDownloadItem(downloadItem) DeviceManager.dbManager.saveDownloadItem(downloadItem)
} }
notifyListeners("onItemDownloadUpdate", JSObject(jacksonObjectMapper().writeValueAsString(downloadItem))) notifyListeners("onItemDownloadUpdate", JSObject(jacksonMapper.writeValueAsString(downloadItem)))
delay(500) delay(500)
} }
@ -308,7 +310,7 @@ class AbsDownloader : Plugin() {
jsobj.put("libraryItemId", downloadItem.id) jsobj.put("libraryItemId", downloadItem.id)
jsobj.put("localFolderId", downloadItem.localFolder.id) jsobj.put("localFolderId", downloadItem.localFolder.id)
if (localLibraryItem != null) { if (localLibraryItem != null) {
jsobj.put("localLibraryItem", JSObject(jacksonObjectMapper().writeValueAsString(localLibraryItem))) jsobj.put("localLibraryItem", JSObject(jacksonMapper.writeValueAsString(localLibraryItem)))
} }
notifyListeners("onItemDownloadComplete", jsobj) notifyListeners("onItemDownloadComplete", jsobj)
} }

View file

@ -15,6 +15,7 @@ import com.audiobookshelf.app.data.LocalFolder
import com.audiobookshelf.app.data.LocalLibraryItem import com.audiobookshelf.app.data.LocalLibraryItem
import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.device.FolderScanner import com.audiobookshelf.app.device.FolderScanner
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.getcapacitor.* import com.getcapacitor.*
import com.getcapacitor.annotation.CapacitorPlugin import com.getcapacitor.annotation.CapacitorPlugin
@ -26,6 +27,7 @@ import kotlinx.coroutines.launch
class AbsFileSystem : Plugin() { class AbsFileSystem : Plugin() {
private val TAG = "AbsFileSystem" private val TAG = "AbsFileSystem"
private val tag = "AbsFileSystem" private val tag = "AbsFileSystem"
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
lateinit var mainActivity: MainActivity lateinit var mainActivity: MainActivity
@ -77,7 +79,7 @@ class AbsFileSystem : Plugin() {
var localFolder = LocalFolder(folderId, folder.name ?: "", folder.uri.toString(),basePath,absolutePath, simplePath, storageType.toString(), mediaType) var localFolder = LocalFolder(folderId, folder.name ?: "", folder.uri.toString(),basePath,absolutePath, simplePath, storageType.toString(), mediaType)
DeviceManager.dbManager.saveLocalFolder(localFolder) DeviceManager.dbManager.saveLocalFolder(localFolder)
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(localFolder))) call.resolve(JSObject(jacksonMapper.writeValueAsString(localFolder)))
} }
override fun onStorageAccessDenied(requestCode: Int, folder: DocumentFile?, storageType: StorageType) { override fun onStorageAccessDenied(requestCode: Int, folder: DocumentFile?, storageType: StorageType) {
@ -148,8 +150,8 @@ class AbsFileSystem : Plugin() {
Log.d(TAG, "NO Scan DATA") Log.d(TAG, "NO Scan DATA")
return call.resolve(JSObject()) return call.resolve(JSObject())
} else { } else {
Log.d(TAG, "Scan DATA ${jacksonObjectMapper().writeValueAsString(folderScanResult)}") Log.d(TAG, "Scan DATA ${jacksonMapper.writeValueAsString(folderScanResult)}")
return call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(folderScanResult))) return call.resolve(JSObject(jacksonMapper.writeValueAsString(folderScanResult)))
} }
} ?: call.resolve(JSObject()) } ?: call.resolve(JSObject())
} }
@ -182,8 +184,8 @@ class AbsFileSystem : Plugin() {
Log.d(TAG, "NO Scan DATA") Log.d(TAG, "NO Scan DATA")
call.resolve(JSObject()) call.resolve(JSObject())
} else { } else {
Log.d(TAG, "Scan DATA ${jacksonObjectMapper().writeValueAsString(scanResult)}") Log.d(TAG, "Scan DATA ${jacksonMapper.writeValueAsString(scanResult)}")
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(scanResult))) call.resolve(JSObject(jacksonMapper.writeValueAsString(scanResult)))
} }
} ?: call.resolve(JSObject()) } ?: call.resolve(JSObject())
} }
@ -223,7 +225,7 @@ class AbsFileSystem : Plugin() {
localLibraryItem?.media?.removeAudioTrack(trackLocalFileId) localLibraryItem?.media?.removeAudioTrack(trackLocalFileId)
localLibraryItem?.removeLocalFile(trackLocalFileId) localLibraryItem?.removeLocalFile(trackLocalFileId)
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem) DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(localLibraryItem))) call.resolve(JSObject(jacksonMapper.writeValueAsString(localLibraryItem)))
} else { } else {
call.resolve(JSObject("{\"success\":false}")) call.resolve(JSObject("{\"success\":false}"))
} }

View file

@ -13,6 +13,7 @@ import com.audiobookshelf.app.data.PlaybackSession
import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.player.MediaProgressSyncData import com.audiobookshelf.app.player.MediaProgressSyncData
import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import com.getcapacitor.JSArray import com.getcapacitor.JSArray
@ -26,6 +27,7 @@ import java.io.IOException
class ApiHandler { class ApiHandler {
val tag = "ApiHandler" val tag = "ApiHandler"
private var client = OkHttpClient() private var client = OkHttpClient()
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
var ctx: Context var ctx: Context
var storageSharedPreferences: SharedPreferences? = null var storageSharedPreferences: SharedPreferences? = null
@ -54,6 +56,15 @@ class ApiHandler {
makeRequest(request, cb) makeRequest(request, cb)
} }
fun patchRequest(endpoint:String, payload: JSObject, cb: (JSObject) -> Unit) {
val mediaType = "application/json; charset=utf-8".toMediaType()
val requestBody = payload.toString().toRequestBody(mediaType)
val request = Request.Builder().patch(requestBody)
.url("${DeviceManager.serverAddress}$endpoint").addHeader("Authorization", "Bearer ${DeviceManager.token}")
.build()
makeRequest(request, cb)
}
fun isOnline(): Boolean { fun isOnline(): Boolean {
val connectivityManager = ctx.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val connectivityManager = ctx.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (connectivityManager != null) { if (connectivityManager != null) {
@ -105,7 +116,7 @@ class ApiHandler {
} }
fun getLibraries(cb: (List<Library>) -> Unit) { fun getLibraries(cb: (List<Library>) -> Unit) {
val mapper = jacksonObjectMapper() val mapper = jacksonMapper
getRequest("/api/libraries") { getRequest("/api/libraries") {
val libraries = mutableListOf<Library>() val libraries = mutableListOf<Library>()
if (it.has("value")) { if (it.has("value")) {
@ -121,7 +132,7 @@ class ApiHandler {
fun getLibraryItem(libraryItemId:String, cb: (LibraryItem) -> Unit) { fun getLibraryItem(libraryItemId:String, cb: (LibraryItem) -> Unit) {
getRequest("/api/items/$libraryItemId?expanded=1") { getRequest("/api/items/$libraryItemId?expanded=1") {
val libraryItem = jacksonObjectMapper().readValue<LibraryItem>(it.toString()) val libraryItem = jacksonMapper.readValue<LibraryItem>(it.toString())
cb(libraryItem) cb(libraryItem)
} }
} }
@ -132,7 +143,7 @@ class ApiHandler {
if (it.has("results")) { if (it.has("results")) {
var array = it.getJSONArray("results") var array = it.getJSONArray("results")
for (i in 0 until array.length()) { for (i in 0 until array.length()) {
val item = jacksonObjectMapper().readValue<LibraryItem>(array.get(i).toString()) val item = jacksonMapper.readValue<LibraryItem>(array.get(i).toString())
items.add(item) items.add(item)
} }
} }
@ -153,13 +164,13 @@ class ApiHandler {
postRequest(endpoint, payload) { postRequest(endpoint, payload) {
it.put("serverConnectionConfigId", DeviceManager.serverConnectionConfig?.id) it.put("serverConnectionConfigId", DeviceManager.serverConnectionConfig?.id)
it.put("serverAddress", DeviceManager.serverAddress) it.put("serverAddress", DeviceManager.serverAddress)
val playbackSession = jacksonObjectMapper().readValue<PlaybackSession>(it.toString()) val playbackSession = jacksonMapper.readValue<PlaybackSession>(it.toString())
cb(playbackSession) cb(playbackSession)
} }
} }
fun sendProgressSync(sessionId:String, syncData: MediaProgressSyncData, cb: () -> Unit) { fun sendProgressSync(sessionId:String, syncData: MediaProgressSyncData, cb: () -> Unit) {
var payload = JSObject(jacksonObjectMapper().writeValueAsString(syncData)) var payload = JSObject(jacksonMapper.writeValueAsString(syncData))
postRequest("/api/session/$sessionId/sync", payload) { postRequest("/api/session/$sessionId/sync", payload) {
cb() cb()
@ -167,7 +178,7 @@ class ApiHandler {
} }
fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: () -> Unit) { fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: () -> Unit) {
var payload = JSObject(jacksonObjectMapper().writeValueAsString(playbackSession)) var payload = JSObject(jacksonMapper.writeValueAsString(playbackSession))
postRequest("/api/session/local", payload) { postRequest("/api/session/local", payload) {
cb() cb()
@ -190,14 +201,14 @@ class ApiHandler {
if (localMediaProgress.isNotEmpty()) { if (localMediaProgress.isNotEmpty()) {
Log.d(tag, "Sending sync local progress request with ${localMediaProgress.size} progress items") Log.d(tag, "Sending sync local progress request with ${localMediaProgress.size} progress items")
var payload = JSObject(jacksonObjectMapper().writeValueAsString(LocalMediaProgressSyncPayload(localMediaProgress))) var payload = JSObject(jacksonMapper.writeValueAsString(LocalMediaProgressSyncPayload(localMediaProgress)))
postRequest("/api/me/sync-local-progress", payload) { postRequest("/api/me/sync-local-progress", payload) {
Log.d(tag, "Media Progress Sync payload $payload - response ${it.toString()}") Log.d(tag, "Media Progress Sync payload $payload - response ${it.toString()}")
if (it.toString() == "{}") { if (it.toString() == "{}") {
Log.e(tag, "Progress sync received empty object") Log.e(tag, "Progress sync received empty object")
} else { } else {
val progressSyncResponsePayload = jacksonObjectMapper().readValue<MediaProgressSyncResponsePayload>(it.toString()) val progressSyncResponsePayload = jacksonMapper.readValue<MediaProgressSyncResponsePayload>(it.toString())
localSyncResultsPayload.numLocalProgressUpdates = progressSyncResponsePayload.localProgressUpdates.size localSyncResultsPayload.numLocalProgressUpdates = progressSyncResponsePayload.localProgressUpdates.size
localSyncResultsPayload.numServerProgressUpdates = progressSyncResponsePayload.numServerProgressUpdates localSyncResultsPayload.numServerProgressUpdates = progressSyncResponsePayload.numServerProgressUpdates
@ -217,4 +228,13 @@ class ApiHandler {
cb(localSyncResultsPayload) cb(localSyncResultsPayload)
} }
} }
fun updateMediaProgress(libraryItemId:String,episodeId:String?,updatePayload:JSObject, cb: () -> Unit) {
Log.d(tag, "updateMediaProgress $libraryItemId $episodeId $updatePayload")
var endpoint = if(episodeId.isNullOrEmpty()) "/api/me/progress/$libraryItemId" else "/api/me/progress/$libraryItemId/$episodeId"
patchRequest(endpoint,updatePayload) {
Log.d(tag, "updateMediaProgress patched progress")
cb()
}
}
} }

View file

@ -17,7 +17,7 @@
<widgets-download-progress-indicator /> <widgets-download-progress-indicator />
<nuxt-link class="h-7 mx-2" to="/search"> <nuxt-link v-if="user" class="h-7 mx-2" to="/search">
<span class="material-icons" style="font-size: 1.75rem">search</span> <span class="material-icons" style="font-size: 1.75rem">search</span>
</nuxt-link> </nuxt-link>

View file

@ -43,7 +43,10 @@
<div id="streamContainer" class="w-full z-20 bg-primary absolute bottom-0 left-0 right-0 p-2 pointer-events-auto transition-all" @click="clickContainer"> <div id="streamContainer" class="w-full z-20 bg-primary absolute bottom-0 left-0 right-0 p-2 pointer-events-auto transition-all" @click="clickContainer">
<div v-if="showFullscreen" class="absolute top-0 left-0 right-0 w-full py-3 mx-auto px-3" style="max-width: 380px"> <div v-if="showFullscreen" class="absolute top-0 left-0 right-0 w-full py-3 mx-auto px-3" style="max-width: 380px">
<div class="flex items-center justify-between pointer-events-auto"> <div class="flex items-center justify-between pointer-events-auto">
<span class="material-icons text-3xl text-white text-opacity-75 cursor-pointer" @click="$emit('showBookmarks')">{{ bookmarks.length ? 'bookmark' : 'bookmark_border' }}</span> <span v-if="!isPodcast" class="material-icons text-3xl text-white text-opacity-75 cursor-pointer" @click="$emit('showBookmarks')">{{ bookmarks.length ? 'bookmark' : 'bookmark_border' }}</span>
<!-- hidden for podcasts but still using this as a placeholder -->
<span v-else class="material-icons text-3xl text-white text-opacity-0">bookmark</span>
<span class="font-mono text-white text-opacity-75 cursor-pointer" style="font-size: 1.35rem" @click="$emit('selectPlaybackSpeed')">{{ currentPlaybackRate }}x</span> <span class="font-mono text-white text-opacity-75 cursor-pointer" style="font-size: 1.35rem" @click="$emit('selectPlaybackSpeed')">{{ currentPlaybackRate }}x</span>
<svg v-if="!sleepTimerRunning" xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 text-white text-opacity-75 cursor-pointer" fill="none" viewBox="0 0 24 24" stroke="currentColor" @click.stop="$emit('showSleepTimer')"> <svg v-if="!sleepTimerRunning" xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 text-white text-opacity-75 cursor-pointer" fill="none" viewBox="0 0 24 24" stroke="currentColor" @click.stop="$emit('showSleepTimer')">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
@ -156,6 +159,12 @@ export default {
} }
return this.showFullscreen ? 200 : 60 return this.showFullscreen ? 200 : 60
}, },
mediaType() {
return this.playbackSession ? this.playbackSession.mediaType : null
},
isPodcast() {
return this.mediaType === 'podcast'
},
mediaMetadata() { mediaMetadata() {
return this.playbackSession ? this.playbackSession.mediaMetadata : null return this.playbackSession ? this.playbackSession.mediaMetadata : null
}, },
@ -552,6 +561,10 @@ export default {
} }
this.isEnded = data.playerState == this.$constants.PlayerState.ENDED this.isEnded = data.playerState == this.$constants.PlayerState.ENDED
console.log('received metadata update', data)
if (data.currentRate && data.currentRate > 0) this.playbackSpeed = data.currentRate
this.timeupdate() this.timeupdate()
}, },
// When a playback session is started the native android/ios will send the session // When a playback session is started the native android/ios will send the session

View file

@ -101,12 +101,14 @@ export default {
}) })
} }
if (this.$platform !== 'ios') {
items.push({ items.push({
icon: 'folder', icon: 'folder',
iconOutlined: true, iconOutlined: true,
text: 'Local Media', text: 'Local Media',
to: '/localMedia/folders' to: '/localMedia/folders'
}) })
}
return items return items
}, },
currentRoutePath() { currentRoutePath() {

View file

@ -120,7 +120,7 @@ export default {
return this.media.coverPath || this.placeholderUrl return this.media.coverPath || this.placeholderUrl
}, },
hasCover() { hasCover() {
return !!this.media.coverPath || this.localCover return !!this.media.coverPath || this.localCover || this.downloadCover
}, },
sizeMultiplier() { sizeMultiplier() {
var baseSize = this.squareAspectRatio ? 192 : 120 var baseSize = this.squareAspectRatio ? 192 : 120

View file

@ -1,12 +1,16 @@
<template> <template>
<div class="w-full px-0 py-4 overflow-hidden relative border-b border-white border-opacity-10"> <div class="w-full px-0 py-4 overflow-hidden relative border-b border-white border-opacity-10">
<div v-if="episode" class="flex items-center"> <div v-if="episode" class="w-full px-1">
<!-- <div class="w-12 min-w-12 max-w-16 h-full"> <!-- Help debug for testing -->
<div class="flex h-full items-center justify-center"> <!-- <template>
<span class="material-icons drag-handle text-lg text-white text-opacity-50 hover:text-opacity-100">menu</span> <p class="text-xs mb-1">{{ isLocal ? 'LOCAL' : 'NOT LOCAL' }}</p>
</div> <p class="text-xs mb-4">Lid:{{ libraryItemId }}<br />Eid:{{ episode.id }}<br />LLid:{{ localLibraryItemId }}<br />LEid:{{ localEpisodeId }}</p>
</div> --> <p v-if="itemProgress">Server Media Progress {{ Math.round(itemProgress.progress * 100) }}</p>
<div class="flex-grow px-1"> <p v-else>No Server Media Progress</p>
<p v-if="localMediaProgress">Local Media Progress {{ Math.round(localMediaProgress.progress * 100) }}</p>
<p v-else>No Local Media Progress</p>
</template> -->
<p v-if="publishedAt" class="text-xs text-gray-400 mb-1">Published {{ $formatDate(publishedAt, 'MMM do, yyyy') }}</p> <p v-if="publishedAt" class="text-xs text-gray-400 mb-1">Published {{ $formatDate(publishedAt, 'MMM do, yyyy') }}</p>
<p class="text-sm font-semibold"> <p class="text-sm font-semibold">
@ -22,8 +26,10 @@
</div> </div>
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" /> <ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
<span class="material-icons px-2" :class="downloadItem ? 'animate-bounce text-warning text-opacity-75 text-xl' : 'text-gray-300 text-xl'" @click="downloadClick">{{ downloadItem ? 'downloading' : 'download' }}</span>
</div> <span v-if="isLocal" class="material-icons-outlined px-2 text-success text-lg">audio_file</span>
<span v-else-if="!localEpisode" class="material-icons px-2" :class="downloadItem ? 'animate-bounce text-warning text-opacity-75 text-xl' : 'text-gray-300 text-xl'" @click="downloadClick">{{ downloadItem ? 'downloading' : 'download' }}</span>
<span v-else class="material-icons px-2 text-success text-xl">download_done</span>
</div> </div>
</div> </div>
@ -38,11 +44,16 @@ import { AbsDownloader } from '@/plugins/capacitor'
export default { export default {
props: { props: {
libraryItemId: String, libraryItemId: String,
isLocal: Boolean,
episode: { episode: {
type: Object, type: Object,
default: () => {} default: () => {}
} },
localLibraryItemId: String,
localEpisode: {
type: Object,
default: () => {}
},
isLocal: Boolean
}, },
data() { data() {
return { return {
@ -68,8 +79,15 @@ export default {
return this.$secondsToTimestamp(this.episode.duration) return this.$secondsToTimestamp(this.episode.duration)
}, },
isStreaming() { isStreaming() {
if (this.playerIsLocal && this.localLibraryItemId && this.localEpisode) {
// Check is streaming local version of this episode
return this.$store.getters['getIsEpisodeStreaming'](this.localLibraryItemId, this.localEpisode.id)
}
return this.$store.getters['getIsEpisodeStreaming'](this.libraryItemId, this.episode.id) return this.$store.getters['getIsEpisodeStreaming'](this.libraryItemId, this.episode.id)
}, },
playerIsLocal() {
return !!this.$store.state.playerIsLocal
},
streamIsPlaying() { streamIsPlaying() {
return this.$store.state.playerIsPlaying && this.isStreaming return this.$store.state.playerIsPlaying && this.isStreaming
}, },
@ -77,6 +95,14 @@ export default {
if (this.isLocal) return this.$store.getters['globals/getLocalMediaProgressById'](this.libraryItemId, this.episode.id) if (this.isLocal) return this.$store.getters['globals/getLocalMediaProgressById'](this.libraryItemId, this.episode.id)
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, this.episode.id) return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, this.episode.id)
}, },
localMediaProgress() {
if (this.isLocal) return this.$store.getters['globals/getLocalMediaProgressById'](this.libraryItemId, this.episode.id)
else if (this.localLibraryItemId && this.localEpisode) {
return this.$store.getters['globals/getLocalMediaProgressById'](this.localLibraryItemId, this.localEpisode.id)
} else {
return null
}
},
itemProgressPercent() { itemProgressPercent() {
return this.itemProgress ? this.itemProgress.progress : 0 return this.itemProgress ? this.itemProgress.progress : 0
}, },
@ -95,6 +121,9 @@ export default {
}, },
downloadItem() { downloadItem() {
return this.$store.getters['globals/getDownloadItem'](this.libraryItemId, this.episode.id) return this.$store.getters['globals/getDownloadItem'](this.libraryItemId, this.episode.id)
},
localEpisodeId() {
return this.localEpisode ? this.localEpisode.id : null
} }
}, },
methods: { methods: {
@ -153,18 +182,57 @@ export default {
playClick() { playClick() {
if (this.streamIsPlaying) { if (this.streamIsPlaying) {
this.$eventBus.$emit('pause-item') this.$eventBus.$emit('pause-item')
} else {
if (this.localEpisode && this.localLibraryItemId) {
console.log('Play local episode', this.localEpisode.id, this.localLibraryItemId)
this.$eventBus.$emit('play-item', {
libraryItemId: this.localLibraryItemId,
episodeId: this.localEpisode.id
})
} else { } else {
this.$eventBus.$emit('play-item', { this.$eventBus.$emit('play-item', {
libraryItemId: this.libraryItemId, libraryItemId: this.libraryItemId,
episodeId: this.episode.id episodeId: this.episode.id
}) })
} }
}
}, },
toggleFinished() { async toggleFinished() {
this.isProcessingReadUpdate = true
if (this.isLocal || this.localEpisode) {
var isFinished = !this.userIsFinished
var localLibraryItemId = this.isLocal ? this.libraryItemId : this.localLibraryItemId
var localEpisodeId = this.isLocal ? this.episode.id : this.localEpisode.id
var localMediaProgressId = `${localLibraryItemId}-${localEpisodeId}`
console.log('toggleFinished local media progress id', localMediaProgressId, isFinished)
var payload = await this.$db.updateLocalMediaProgressFinished({ localMediaProgressId, isFinished })
console.log('toggleFinished payload', JSON.stringify(payload))
if (!payload || payload.error) {
var errorMsg = payload ? payload.error : 'Unknown error'
this.$toast.error(errorMsg)
} else {
var localMediaProgress = payload.localMediaProgress
console.log('toggleFinished localMediaProgress', JSON.stringify(localMediaProgress))
if (localMediaProgress) {
this.$store.commit('globals/updateLocalMediaProgress', localMediaProgress)
}
var lmp = this.$store.getters['globals/getLocalMediaProgressById'](this.libraryItemId, this.episode.id)
console.log('toggleFinished Check LMP', this.libraryItemId, this.episode.id, JSON.stringify(lmp))
var serverUpdated = payload.server
if (serverUpdated) {
this.$toast.success(`Local & Server Item marked as ${isFinished ? 'Finished' : 'Not Finished'}`)
} else {
this.$toast.success(`Local Item marked as ${isFinished ? 'Finished' : 'Not Finished'}`)
}
}
this.isProcessingReadUpdate = false
} else {
var updatePayload = { var updatePayload = {
isFinished: !this.userIsFinished isFinished: !this.userIsFinished
} }
this.isProcessingReadUpdate = true
this.$axios this.$axios
.$patch(`/api/me/progress/${this.libraryItemId}/${this.episode.id}`, updatePayload) .$patch(`/api/me/progress/${this.libraryItemId}/${this.episode.id}`, updatePayload)
.then(() => { .then(() => {
@ -177,7 +245,7 @@ export default {
this.$toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`) this.$toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
}) })
} }
}, }
mounted() {} }
} }
</script> </script>

View file

@ -3,7 +3,7 @@
<p class="text-lg mb-1 font-semibold">Episodes ({{ episodes.length }})</p> <p class="text-lg mb-1 font-semibold">Episodes ({{ episodes.length }})</p>
<template v-for="episode in episodes"> <template v-for="episode in episodes">
<tables-podcast-episode-row :episode="episode" :library-item-id="libraryItemId" :key="episode.id" /> <tables-podcast-episode-row :episode="episode" :local-episode="localEpisodeMap[episode.id]" :library-item-id="libraryItemId" :local-library-item-id="localLibraryItemId" :is-local="isLocal" :key="episode.id" />
</template> </template>
</div> </div>
</template> </template>
@ -15,12 +15,29 @@ export default {
episodes: { episodes: {
type: Array, type: Array,
default: () => [] default: () => []
} },
localLibraryItemId: String,
localEpisodes: {
type: Array,
default: () => []
},
isLocal: Boolean // If is local then episodes and libraryItemId are local, otherwise local is passed in localLibraryItemId and localEpisodes
}, },
data() { data() {
return {} return {}
}, },
computed: {}, computed: {
// Map of local episodes where server episode id is key
localEpisodeMap() {
var epmap = {}
this.localEpisodes.forEach((localEp) => {
if (localEp.serverEpisodeId) {
epmap[localEp.serverEpisodeId] = localEp
}
})
return epmap
}
},
methods: {}, methods: {},
mounted() {} mounted() {}
} }

View file

@ -71,7 +71,9 @@ export default {
data.itemProgress = itemProgress data.itemProgress = itemProgress
data.episodes = downloadItemParts.filter((dip) => dip.episode).map((dip) => dip.episode) data.episodes = downloadItemParts.filter((dip) => dip.episode).map((dip) => dip.episode)
console.log('Saving item update download payload', JSON.stringify(update)) console.log('[download] Saving item update download payload', JSON.stringify(update))
console.log('[download] Download Progress indicator data', JSON.stringify(data))
this.$set(this.itemDownloadingMap, update.id, update) this.$set(this.itemDownloadingMap, update.id, update)
this.$store.commit('globals/addUpdateItemDownload', data) this.$store.commit('globals/addUpdateItemDownload', data)

View file

@ -58,7 +58,7 @@
<p class="text-sm">{{ description }}</p> <p class="text-sm">{{ description }}</p>
</div> </div>
<tables-podcast-episodes-table v-if="isPodcast" :library-item-id="libraryItemId" :episodes="episodes" /> <tables-podcast-episodes-table v-if="isPodcast" :library-item-id="libraryItemId" :local-library-item-id="localLibraryItemId" :episodes="episodes" :local-episodes="localLibraryItemEpisodes" :is-local="isLocal" />
<modals-select-local-folder-modal v-model="showSelectLocalFolder" :media-type="mediaType" @select="selectedLocalFolder" /> <modals-select-local-folder-modal v-model="showSelectLocalFolder" :media-type="mediaType" @select="selectedLocalFolder" />
</div> </div>
@ -120,6 +120,14 @@ export default {
if (this.isLocal) return this.libraryItem if (this.isLocal) return this.libraryItem
return this.libraryItem.localLibraryItem || null return this.libraryItem.localLibraryItem || null
}, },
localLibraryItemId() {
return this.localLibraryItem ? this.localLibraryItem.id : null
},
localLibraryItemEpisodes() {
if (!this.isPodcast || !this.localLibraryItem) return []
var podcastMedia = this.localLibraryItem.media
return podcastMedia ? podcastMedia.episodes || [] : []
},
isConnected() { isConnected() {
return this.$store.state.socketConnected return this.$store.state.socketConnected
}, },

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="w-full h-full py-6 px-2"> <div class="w-full h-full py-6 px-2">
<div v-if="localLibraryItem" class="w-full h-full"> <div v-if="localLibraryItem" class="w-full h-full" :class="orderChanged ? 'pb-20' : ''">
<div class="px-2 flex items-center mb-2"> <div class="px-2 flex items-center mb-2">
<p class="text-base font-book font-semibold">{{ mediaMetadata.title }}</p> <p class="text-base font-book font-semibold">{{ mediaMetadata.title }}</p>
<div class="flex-grow" /> <div class="flex-grow" />
@ -46,28 +46,10 @@
</template> </template>
</transition-group> </transition-group>
</draggable> </draggable>
<!-- <template v-for="track in audioTracks">
<div :key="track.localFileId" class="flex items-center my-1">
<div class="w-10 h-12 flex items-center justify-center" style="min-width: 48px">
<p class="font-mono font-bold text-xl">{{ track.index }}</p>
</div>
<div class="flex-grow px-2">
<p class="text-xs">{{ track.title }}</p>
</div>
<div class="w-20 text-center text-gray-300" style="min-width: 80px">
<p class="text-xs">{{ track.mimeType }}</p>
<p class="text-sm">{{ $elapsedPretty(track.duration) }}</p>
</div>
<div class="w-12 h-12 flex items-center justify-center" style="min-width: 48px">
<span class="material-icons" @click="showTrackDialog(track)">more_vert</span>
</div>
</div>
</template> -->
</div> </div>
<div v-else class="w-full"> <div v-else class="w-full">
<p class="text-base mb-2">Episodes ({{ audioTracks.length }})</p> <p class="text-base mb-2">Episodes ({{ episodes.length }})</p>
<template v-for="episode in audioTracks"> <template v-for="episode in episodes">
<div :key="episode.id" class="flex items-center my-1"> <div :key="episode.id" class="flex items-center my-1">
<div class="w-10 h-12 flex items-center justify-center" style="min-width: 48px"> <div class="w-10 h-12 flex items-center justify-center" style="min-width: 48px">
<p class="font-mono font-bold text-xl">{{ episode.index }}</p> <p class="font-mono font-bold text-xl">{{ episode.index }}</p>
@ -108,6 +90,11 @@
<p class="text-lg text-center px-8">{{ failed ? 'Failed to get local library item ' + localLibraryItemId : 'Loading..' }}</p> <p class="text-lg text-center px-8">{{ failed ? 'Failed to get local library item ' + localLibraryItemId : 'Loading..' }}</p>
</div> </div>
<div v-if="orderChanged" class="fixed bottom-0 left-0 w-full py-4 px-4 bg-bg box-shadow-book flex items-center">
<div class="flex-grow" />
<ui-btn small color="success" @click="saveTrackOrder">Save Order</ui-btn>
</div>
<modals-dialog v-model="showDialog" :items="dialogItems" @action="dialogAction" /> <modals-dialog v-model="showDialog" :items="dialogItems" @action="dialogAction" />
</div> </div>
</template> </template>
@ -146,7 +133,8 @@ export default {
isScanning: false, isScanning: false,
showDialog: false, showDialog: false,
selectedAudioTrack: null, selectedAudioTrack: null,
selectedEpisode: null selectedEpisode: null,
orderChanged: false
} }
}, },
computed: { computed: {
@ -162,7 +150,6 @@ export default {
return [] return []
} }
return this.localFiles.filter((lf) => { return this.localFiles.filter((lf) => {
if (this.isPodcast) return !this.audioTracks.find((episode) => episode.audioTrack.localFileId == lf.id)
return !this.audioTracks.find((at) => at.localFileId == lf.id) return !this.audioTracks.find((at) => at.localFileId == lf.id)
}) })
}, },
@ -187,16 +174,19 @@ export default {
mediaMetadata() { mediaMetadata() {
return this.media ? this.media.metadata || {} : {} return this.media ? this.media.metadata || {} : {}
}, },
episodes() {
return this.media.episodes || []
},
audioTracks() { audioTracks() {
if (!this.media) return [] if (!this.media) return []
if (this.mediaType == 'book') { if (this.mediaType == 'book') {
return this.media.tracks || [] return this.media.tracks || []
} else { } else {
return this.media.episodes || [] return (this.media.episodes || []).map((ep) => ep.audioTrack)
} }
}, },
dialogItems() { dialogItems() {
if (this.selectedAudioTrack) { if (this.selectedAudioTrack || this.selectedEpisode) {
return [ return [
{ {
text: 'Hard Delete', text: 'Hard Delete',
@ -227,13 +217,33 @@ export default {
}, },
methods: { methods: {
draggableUpdate() { draggableUpdate() {
console.log('Draggable update', this.audioTracksCopy) console.log('Draggable Update')
// var copyOfCopy = this.audioTracksCopy.map((at) => ({ ...at })) for (let i = 0; i < this.audioTracksCopy.length; i++) {
// const payload = { var trackCopy = this.audioTracksCopy[i]
// localLibraryItemId: this.localLibraryItemId, var track = this.audioTracks[i]
// tracks: copyOfCopy if (track.localFileId !== trackCopy.localFileId) {
// } this.orderChanged = true
// this.$db.updateLocalTrackOrder(payload) return
}
}
this.orderChanged = false
},
async saveTrackOrder() {
var copyOfCopy = this.audioTracksCopy.map((at) => ({ ...at }))
const payload = {
localLibraryItemId: this.localLibraryItemId,
tracks: copyOfCopy
}
var response = await this.$db.updateLocalTrackOrder(payload)
if (response) {
this.$toast.success('Library item updated')
console.log('updateLocal track order response', JSON.stringify(response))
this.localLibraryItem = response
this.audioTracksCopy = this.audioTracks.map((at) => ({ ...at }))
} else {
this.$toast.info(`No updates necessary`)
}
this.orderChanged = false
}, },
showItemDialog() { showItemDialog() {
this.selectedAudioTrack = null this.selectedAudioTrack = null

View file

@ -198,6 +198,10 @@ class AbsDatabaseWeb extends WebPlugin {
async updateLocalTrackOrder({ localLibraryItemId, tracks }) { async updateLocalTrackOrder({ localLibraryItemId, tracks }) {
return [] return []
} }
async updateLocalMediaProgressFinished({ localMediaProgressId, isFinished }) {
return null
}
} }
const AbsDatabase = registerPlugin('AbsDatabase', { const AbsDatabase = registerPlugin('AbsDatabase', {

View file

@ -96,6 +96,11 @@ class DbService {
updateLocalTrackOrder(payload) { updateLocalTrackOrder(payload) {
return AbsDatabase.updateLocalTrackOrder(payload) return AbsDatabase.updateLocalTrackOrder(payload)
} }
// input: { localMediaProgressId:String, isFinished:Boolean }
updateLocalMediaProgressFinished(payload) {
return AbsDatabase.updateLocalMediaProgressFinished(payload)
}
} }
export default ({ app, store }, inject) => { export default ({ app, store }, inject) => {

View file

@ -46,9 +46,10 @@ class ServerSocket extends EventEmitter {
this.socket.on('disconnect', this.onDisconnect.bind(this)) this.socket.on('disconnect', this.onDisconnect.bind(this))
this.socket.on('init', this.onInit.bind(this)) this.socket.on('init', this.onInit.bind(this))
this.socket.on('user_updated', this.onUserUpdated.bind(this)) this.socket.on('user_updated', this.onUserUpdated.bind(this))
this.socket.on('user_item_progress_updated', this.onUserItemProgressUpdated.bind(this))
this.socket.onAny((evt, args) => { this.socket.onAny((evt, args) => {
console.log(`[SOCKET] ${this.socket.id}: ${evt} ${JSON.stringify(args)}`) console.log(`[SOCKET] onAny: ${this.socket.id}: ${evt} ${JSON.stringify(args)}`)
}) })
} }
@ -84,6 +85,12 @@ class ServerSocket extends EventEmitter {
console.log('[SOCKET] User updated', data) console.log('[SOCKET] User updated', data)
this.emit('user_updated', data) this.emit('user_updated', data)
} }
onUserItemProgressUpdated(data) {
console.log('[SOCKET] User Item Progress Updated', JSON.stringify(data))
var progress = data.data
this.$store.commit('user/updateUserMediaProgress', progress)
}
} }
export default ({ app, store }, inject) => { export default ({ app, store }, inject) => {

View file

@ -9,7 +9,7 @@ export const getters = {
getDownloadItem: state => (libraryItemId, episodeId = null) => { getDownloadItem: state => (libraryItemId, episodeId = null) => {
return state.itemDownloads.find(i => { return state.itemDownloads.find(i => {
if (episodeId && !i.episodes.some(e => e.id == episodeId)) return false if (episodeId && !i.episodes.some(e => e.id == episodeId)) return false
return i.id == libraryItemId return i.libraryItemId == libraryItemId
}) })
}, },
getLibraryItemCoverSrc: (state, getters, rootState, rootGetters) => (libraryItem, placeholder = '/book_placeholder.jpg') => { getLibraryItemCoverSrc: (state, getters, rootState, rootGetters) => (libraryItem, placeholder = '/book_placeholder.jpg') => {
@ -32,7 +32,7 @@ export const getters = {
}, },
getLocalMediaProgressById: (state) => (localLibraryItemId, episodeId = null) => { getLocalMediaProgressById: (state) => (localLibraryItemId, episodeId = null) => {
return state.localMediaProgress.find(lmp => { return state.localMediaProgress.find(lmp => {
if (episodeId != null && lmp.episodeId != episodeId) return false if (episodeId != null && lmp.localEpisodeId != episodeId) return false
return lmp.localLibraryItemId == localLibraryItemId return lmp.localLibraryItemId == localLibraryItemId
}) })
} }
@ -73,8 +73,10 @@ export const mutations = {
} }
var index = state.localMediaProgress.findIndex(lmp => lmp.id == prog.id) var index = state.localMediaProgress.findIndex(lmp => lmp.id == prog.id)
if (index >= 0) { if (index >= 0) {
console.log('UpdateLocalMediaProgress updating', prog.id, prog.progress)
state.localMediaProgress.splice(index, 1, prog) state.localMediaProgress.splice(index, 1, prog)
} else { } else {
console.log('updateLocalMediaProgress inserting new progress', prog.id, prog.progress)
state.localMediaProgress.push(prog) state.localMediaProgress.push(prog)
} }
}, },

View file

@ -57,9 +57,17 @@ export const actions = {
export const mutations = { export const mutations = {
setPlayerItem(state, playbackSession) { setPlayerItem(state, playbackSession) {
state.playerIsLocal = playbackSession ? playbackSession.playMethod == this.$constants.PlayMethod.LOCAL : false
if (state.playerIsLocal) {
state.playerLibraryItemId = playbackSession ? playbackSession.localLibraryItem.id || null : null
state.playerEpisodeId = playbackSession ? playbackSession.localEpisodeId || null : null
} else {
state.playerLibraryItemId = playbackSession ? playbackSession.libraryItemId || null : null state.playerLibraryItemId = playbackSession ? playbackSession.libraryItemId || null : null
state.playerEpisodeId = playbackSession ? playbackSession.episodeId || null : null state.playerEpisodeId = playbackSession ? playbackSession.episodeId || null : null
state.playerIsLocal = playbackSession ? playbackSession.playMethod == this.$constants.PlayMethod.LOCAL : false }
console.log('setPlayerItem', state.playerLibraryItemId, state.playerEpisodeId, state.playerIsLocal)
}, },
setPlayerPlaying(state, val) { setPlayerPlaying(state, val) {
state.playerIsPlaying = val state.playerIsPlaying = val

View file

@ -74,6 +74,15 @@ export const mutations = {
if (!state.user) return if (!state.user) return
state.user.mediaProgress = state.user.mediaProgress.filter(mp => mp.id != id) state.user.mediaProgress = state.user.mediaProgress.filter(mp => mp.id != id)
}, },
updateUserMediaProgress(state, data) {
if (!data || !state.user) return
var mediaProgressIndex = state.user.mediaProgress.findIndex(mp => mp.id === data.id)
if (mediaProgressIndex >= 0) {
state.user.mediaProgress.splice(mediaProgressIndex, 1, data)
} else {
state.user.mediaProgress.push(data)
}
},
setServerConnectionConfig(state, serverConnectionConfig) { setServerConnectionConfig(state, serverConnectionConfig) {
state.serverConnectionConfig = serverConnectionConfig state.serverConnectionConfig = serverConnectionConfig
}, },