mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-06 20:08:48 +02:00
Merge branch 'master' into master
This commit is contained in:
@@ -12,8 +12,9 @@ class CastOptionsProvider : OptionsProvider {
|
||||
override fun getCastOptions(context: Context): CastOptions {
|
||||
Log.d("CastOptionsProvider", "getCastOptions")
|
||||
var appId = "FD1F76C5"
|
||||
var defaultId =CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID
|
||||
return CastOptions.Builder()
|
||||
.setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID).setCastMediaOptions(
|
||||
.setReceiverApplicationId(appId).setCastMediaOptions(
|
||||
CastMediaOptions.Builder()
|
||||
// We manage the media session and the notifications ourselves.
|
||||
.setMediaSessionEnabled(false)
|
||||
|
||||
@@ -2,6 +2,11 @@ package com.audiobookshelf.app.data
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
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)
|
||||
data class AudioProbeStream(
|
||||
|
||||
@@ -71,6 +71,8 @@ open class MediaType(var metadata:MediaTypeMetadata, var coverPath:String?) {
|
||||
open fun addAudioTrack(audioTrack:AudioTrack) { }
|
||||
@JsonIgnore
|
||||
open fun removeAudioTrack(localFileId:String) { }
|
||||
@JsonIgnore
|
||||
open fun getLocalCopy():MediaType { return MediaType(MediaTypeMetadata(""),null) }
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@@ -92,10 +94,11 @@ class Podcast(
|
||||
episodes = episodes?.filter { ep ->
|
||||
audioTracks.find { it.localFileId == ep.audioTrack?.localFileId } != null
|
||||
} as MutableList<PodcastEpisode>
|
||||
|
||||
// Add new episodes
|
||||
audioTracks.forEach { at ->
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -108,7 +111,7 @@ class Podcast(
|
||||
}
|
||||
@JsonIgnore
|
||||
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)
|
||||
|
||||
var index = 1
|
||||
@@ -129,7 +132,7 @@ class Podcast(
|
||||
}
|
||||
@JsonIgnore
|
||||
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)
|
||||
|
||||
var index = 1
|
||||
@@ -138,6 +141,12 @@ class Podcast(
|
||||
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)
|
||||
@@ -158,7 +167,7 @@ class Book(
|
||||
@JsonIgnore
|
||||
override fun setAudioTracks(audioTracks:MutableList<AudioTrack>) {
|
||||
tracks = audioTracks
|
||||
|
||||
tracks?.sortBy { it.index }
|
||||
// TODO: Is it necessary to calculate this each time? check if can remove safely
|
||||
var totalDuration = 0.0
|
||||
tracks?.forEach {
|
||||
@@ -195,6 +204,11 @@ class Book(
|
||||
}
|
||||
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
|
||||
@@ -261,7 +275,10 @@ data class PodcastEpisode(
|
||||
var subtitle:String?,
|
||||
var description:String?,
|
||||
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)
|
||||
@@ -275,7 +292,8 @@ data class FileMetadata(
|
||||
var filename:String,
|
||||
var ext:String,
|
||||
var path:String,
|
||||
var relPath:String
|
||||
var relPath:String,
|
||||
var size:Long?
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.util.Log
|
||||
import com.audiobookshelf.app.plugins.AbsDownloader
|
||||
import io.paperdb.Paper
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
|
||||
class DbManager {
|
||||
val tag = "DbManager"
|
||||
@@ -130,6 +131,92 @@ class DbManager {
|
||||
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) {
|
||||
Paper.book("localPlaybackSession").write(playbackSession.id,playbackSession)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.audiobookshelf.app.data
|
||||
|
||||
import android.util.Log
|
||||
import com.audiobookshelf.app.device.DeviceManager
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
@@ -26,6 +27,9 @@ data class LocalLibraryItem(
|
||||
var libraryItemId:String?
|
||||
) {
|
||||
|
||||
@get:JsonIgnore
|
||||
val isPodcast get() = mediaType == "podcast"
|
||||
|
||||
@JsonIgnore
|
||||
fun getDuration():Double {
|
||||
var total = 0.0
|
||||
@@ -50,25 +54,26 @@ data class LocalLibraryItem(
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getPlaybackSession(episodeId:String):PlaybackSession {
|
||||
var sessionId = "play-${UUID.randomUUID()}"
|
||||
fun getPlaybackSession(episode:PodcastEpisode?):PlaybackSession {
|
||||
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 currentTime = mediaProgress?.currentTime ?: 0.0
|
||||
|
||||
// TODO: Clean up add mediaType methods for displayTitle and displayAuthor
|
||||
var mediaMetadata = media.metadata
|
||||
var chapters = if (mediaType == "book") (media as Book).chapters else mutableListOf()
|
||||
var authorName = "Unknown"
|
||||
if (mediaType == "book") {
|
||||
var bookMetadata = mediaMetadata as BookMetadata
|
||||
authorName = bookMetadata?.authorName ?: "Unknown"
|
||||
var audioTracks = media.getAudioTracks() as MutableList<AudioTrack>
|
||||
var authorName = mediaMetadata.getAuthorDisplayName()
|
||||
if (episode != null) { // Get podcast episode audio track
|
||||
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()
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package com.audiobookshelf.app.data
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class LocalMediaProgress(
|
||||
var id:String,
|
||||
var localLibraryItemId:String,
|
||||
var episodeId:String?,
|
||||
var localEpisodeId:String?,
|
||||
var duration:Double,
|
||||
var progress:Double, // 0 to 1
|
||||
var currentTime:Double,
|
||||
@@ -18,5 +19,27 @@ data class LocalMediaProgress(
|
||||
var serverConnectionConfigId:String?,
|
||||
var serverAddress: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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ class PlaybackSession(
|
||||
var currentTime:Double,
|
||||
var libraryItem:LibraryItem?,
|
||||
var localLibraryItem:LocalLibraryItem?,
|
||||
var localEpisodeId:String?,
|
||||
var serverConnectionConfigId:String?,
|
||||
var serverAddress:String?
|
||||
) {
|
||||
@@ -53,7 +54,7 @@ class PlaybackSession(
|
||||
@get:JsonIgnore
|
||||
val localLibraryItemId get() = localLibraryItem?.id ?: ""
|
||||
@get:JsonIgnore
|
||||
val localMediaProgressId get() = if (episodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$episodeId"
|
||||
val localMediaProgressId get() = if (episodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
|
||||
@get:JsonIgnore
|
||||
val progress get() = currentTime / getTotalDuration()
|
||||
|
||||
@@ -175,7 +176,7 @@ class PlaybackSession(
|
||||
|
||||
@JsonIgnore
|
||||
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
|
||||
@@ -187,6 +188,6 @@ class PlaybackSession(
|
||||
|
||||
@JsonIgnore
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ object DeviceManager {
|
||||
var deviceData:DeviceData = dbManager.getDeviceData()
|
||||
var serverConnectionConfig: ServerConnectionConfig? = null
|
||||
|
||||
val serverConnectionConfigId get() = serverConnectionConfig?.id ?: ""
|
||||
val serverAddress get() = serverConnectionConfig?.address ?: ""
|
||||
val serverUserId get() = serverConnectionConfig?.userId ?: ""
|
||||
val token get() = serverConnectionConfig?.token ?: ""
|
||||
@@ -20,6 +21,6 @@ object DeviceManager {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,15 @@ import com.arthenica.ffmpegkit.FFprobeKit
|
||||
import com.arthenica.ffmpegkit.Level
|
||||
import com.audiobookshelf.app.data.*
|
||||
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.readValue
|
||||
import com.getcapacitor.JSObject
|
||||
|
||||
class FolderScanner(var ctx: Context) {
|
||||
private val tag = "FolderScanner"
|
||||
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
||||
|
||||
|
||||
private fun getLocalLibraryItemId(mediaItemId:String):String {
|
||||
return "local_" + DeviceManager.getBase64Id(mediaItemId)
|
||||
@@ -52,7 +56,8 @@ class FolderScanner(var ctx: Context) {
|
||||
|
||||
// Remove existing items no longer there
|
||||
existingLocalLibraryItems = existingLocalLibraryItems.filter { lli ->
|
||||
var fileFound = foldersFound.find { f -> lli.id == getLocalLibraryItemId(f.id) }
|
||||
Log.d(tag, "scanForMediaItems Checking Existing LLI ${lli.id}")
|
||||
var fileFound = foldersFound.find { f -> lli.id == getLocalLibraryItemId(f.id) }
|
||||
if (fileFound == null) {
|
||||
Log.d(tag, "Existing local library item is no longer in file system ${lli.media.metadata.title}")
|
||||
DeviceManager.dbManager.removeLocalLibraryItem(lli.id)
|
||||
@@ -129,25 +134,22 @@ class FolderScanner(var ctx: Context) {
|
||||
var existingAudioTrack = existingAudioTracks.find { eat -> eat.localFileId == localFileId }
|
||||
if (existingAudioTrack != null) { // Update existing audio track
|
||||
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
|
||||
isNewOrUpdated = true
|
||||
}
|
||||
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
|
||||
isNewOrUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
var session = FFprobeKit.execute("-i \"${localFile.absolutePath}\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet")
|
||||
|
||||
val audioProbeResult = jacksonObjectMapper().readValue<AudioProbeResult>(session.output)
|
||||
Log.d(tag, "Probe Result DATA ${audioProbeResult.duration} | ${audioProbeResult.size} | ${audioProbeResult.title} | ${audioProbeResult.artist}")
|
||||
var audioProbeResult = probeAudioFile(localFile.absolutePath)
|
||||
|
||||
if (existingAudioTrack != null) {
|
||||
// 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 }
|
||||
|
||||
if (existingLocalFile == null) {
|
||||
Log.d(tag, "scanLibraryItemFolder new local file found ${localFile.absolutePath}")
|
||||
isNewOrUpdated = true
|
||||
}
|
||||
if (existingItem != null && existingItem.coverContentUrl == null) {
|
||||
// Existing media item did not have a cover - cover found on scan
|
||||
Log.d(tag, "scanLibraryItemFolder setting cover ${localFile.absolutePath}")
|
||||
isNewOrUpdated = true
|
||||
existingItem.coverAbsolutePath = localFile.absolutePath
|
||||
existingItem.coverContentUrl = localFile.contentUrl
|
||||
@@ -217,11 +221,14 @@ class FolderScanner(var ctx: Context) {
|
||||
fun scanDownloadItem(downloadItem: AbsDownloader.DownloadItem):LocalLibraryItem? {
|
||||
var folderDf = DocumentFileCompat.fromUri(ctx, Uri.parse(downloadItem.localFolder.contentUrl))
|
||||
var foldersFound = folderDf?.search(false, DocumentFileType.FOLDER) ?: mutableListOf()
|
||||
|
||||
var itemFolderId = ""
|
||||
var itemFolderUrl = ""
|
||||
var itemFolderBasePath = ""
|
||||
var itemFolderAbsolutePath = ""
|
||||
foldersFound.forEach {
|
||||
if (it.name == downloadItem.itemTitle) {
|
||||
itemFolderId = it.id
|
||||
itemFolderUrl = it.uri.toString()
|
||||
itemFolderBasePath = it.getBasePath(ctx)
|
||||
itemFolderAbsolutePath = it.getAbsolutePath(ctx)
|
||||
@@ -238,7 +245,9 @@ class FolderScanner(var ctx: Context) {
|
||||
Log.e(tag, "Folder Doc File Invalid ${downloadItem.itemFolderPath}")
|
||||
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
|
||||
var filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*"))
|
||||
@@ -246,13 +255,13 @@ class FolderScanner(var ctx: Context) {
|
||||
|
||||
var localLibraryItem:LocalLibraryItem? = null
|
||||
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 {
|
||||
// Lookup or create podcast local library item
|
||||
localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem("local_${downloadItem.libraryItemId}")
|
||||
localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
|
||||
if (localLibraryItem == null) {
|
||||
Log.d(tag, "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)
|
||||
Log.d(tag, "[FolderScanner] Podcast local library item not created yet for ${downloadItem.media.metadata.title}")
|
||||
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
|
||||
var audioTrackFromServer = itemPart.audioTrack
|
||||
Log.d(tag, "scanDownloadItem: Audio Track from Server index = ${audioTrackFromServer?.index}")
|
||||
|
||||
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())
|
||||
localLibraryItem.localFiles.add(localFile)
|
||||
|
||||
// TODO: Make asynchronous
|
||||
var session = FFprobeKit.execute("-i \"${localFile.absolutePath}\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet")
|
||||
|
||||
val audioProbeResult = jacksonObjectMapper().readValue<AudioProbeResult>(session.output)
|
||||
Log.d(tag, "Probe Result DATA ${audioProbeResult.duration} | ${audioProbeResult.size} | ${audioProbeResult.title} | ${audioProbeResult.artist}")
|
||||
var audioProbeResult = probeAudioFile(localFile.absolutePath)
|
||||
|
||||
// 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)
|
||||
audioTracks.add(track)
|
||||
|
||||
Log.d(tag, "scanDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}")
|
||||
|
||||
// Add podcast episodes to library
|
||||
itemPart.episode?.let { podcastEpisode ->
|
||||
var podcast = localLibraryItem.media as Podcast
|
||||
podcast.addEpisode(track, podcastEpisode)
|
||||
Log.d(tag, "scanDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}")
|
||||
}
|
||||
} else { // Cover image
|
||||
var localFileId = DeviceManager.getBase64Id(docFile.id)
|
||||
@@ -381,10 +391,7 @@ class FolderScanner(var ctx: Context) {
|
||||
|
||||
if (localFile.isAudioFile()) {
|
||||
// TODO: Make asynchronous
|
||||
var session = FFprobeKit.execute("-i \"${localFile.absolutePath}\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet")
|
||||
|
||||
val audioProbeResult = jacksonObjectMapper().readValue<AudioProbeResult>(session.output)
|
||||
Log.d(tag, "Probe Result DATA ${audioProbeResult.duration} | ${audioProbeResult.size} | ${audioProbeResult.title} | ${audioProbeResult.artist}")
|
||||
var audioProbeResult = probeAudioFile(localFile.absolutePath)
|
||||
|
||||
var existingTrack = existingAudioTracks.find { audioTrack ->
|
||||
audioTrack.localFileId == localFile.id
|
||||
@@ -421,4 +428,13 @@ class FolderScanner(var ctx: Context) {
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.audiobookshelf.app.player
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
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.MediaItemConverter
|
||||
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener
|
||||
import com.google.android.gms.cast.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.*
|
||||
import com.google.android.gms.cast.framework.*
|
||||
import org.json.JSONObject
|
||||
|
||||
@@ -321,12 +319,36 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
|
||||
try {
|
||||
val castContext = CastContext.getSharedInstance(mainActivity)
|
||||
|
||||
playerNotificationService.castPlayer = CastPlayer(castContext, CustomConverter()).apply {
|
||||
setSessionAvailabilityListener(CastSessionAvailabilityListener())
|
||||
addListener(PlayerListener(playerNotificationService))
|
||||
// Work in progress using the cast api
|
||||
var currentSession = playerNotificationService.getCurrentPlaybackSessionCopy()
|
||||
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) {
|
||||
Log.i(tag, "Cast is not available on this device. " +
|
||||
"Exception thrown when attempting to obtain CastContext. " + e.message)
|
||||
|
||||
@@ -110,14 +110,12 @@ class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, a
|
||||
currentLocalMediaProgress = mediaProgress
|
||||
}
|
||||
} else {
|
||||
currentLocalMediaProgress?.currentTime = playbackSession.currentTime
|
||||
currentLocalMediaProgress?.lastUpdate = playbackSession.updatedAt
|
||||
currentLocalMediaProgress?.progress = playbackSession.progress
|
||||
currentLocalMediaProgress?.updateFromPlaybackSession(playbackSession)
|
||||
}
|
||||
currentLocalMediaProgress?.let {
|
||||
DeviceManager.dbManager.saveLocalMediaProgress(it)
|
||||
playerNotificationService.clientEventEmitter?.onLocalMediaProgressUpdate(it)
|
||||
Log.d(tag, "Saved Local Progress Current Time: ${it.currentTime} | Duration ${it.duration} | Progress ${(it.progress * 100).roundToInt()}%")
|
||||
Log.d(tag, "Saved Local Progress Current Time: ID ${it.id} | ${it.currentTime} | Duration ${it.duration} | Progress ${(it.progress * 100).roundToInt()}%")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,7 @@ import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.audiobookshelf.app.MainActivity
|
||||
import com.audiobookshelf.app.data.LocalMediaProgress
|
||||
import com.audiobookshelf.app.data.PlaybackMetadata
|
||||
import com.audiobookshelf.app.data.PlaybackSession
|
||||
import com.audiobookshelf.app.data.*
|
||||
import com.audiobookshelf.app.device.DeviceManager
|
||||
import com.audiobookshelf.app.player.CastManager
|
||||
import com.audiobookshelf.app.player.PlayerNotificationService
|
||||
@@ -22,6 +20,7 @@ import org.json.JSONObject
|
||||
@CapacitorPlugin(name = "AbsAudioPlayer")
|
||||
class AbsAudioPlayer : Plugin() {
|
||||
private val tag = "AbsAudioPlayer"
|
||||
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
||||
|
||||
lateinit var mainActivity: MainActivity
|
||||
lateinit var apiHandler:ApiHandler
|
||||
@@ -36,7 +35,7 @@ class AbsAudioPlayer : Plugin() {
|
||||
|
||||
playerNotificationService.clientEventEmitter = (object : PlayerNotificationService.ClientEventEmitter {
|
||||
override fun onPlaybackSession(playbackSession: PlaybackSession) {
|
||||
notifyListeners("onPlaybackSession", JSObject(jacksonObjectMapper().writeValueAsString(playbackSession)))
|
||||
notifyListeners("onPlaybackSession", JSObject(jacksonMapper.writeValueAsString(playbackSession)))
|
||||
}
|
||||
|
||||
override fun onPlaybackClosed() {
|
||||
@@ -48,7 +47,7 @@ class AbsAudioPlayer : Plugin() {
|
||||
}
|
||||
|
||||
override fun onMetadata(metadata: PlaybackMetadata) {
|
||||
notifyListeners("onMetadata", JSObject(jacksonObjectMapper().writeValueAsString(metadata)))
|
||||
notifyListeners("onMetadata", JSObject(jacksonMapper.writeValueAsString(metadata)))
|
||||
}
|
||||
|
||||
override fun onPrepare(audiobookId: String, playWhenReady: Boolean) {
|
||||
@@ -67,7 +66,7 @@ class AbsAudioPlayer : Plugin() {
|
||||
}
|
||||
|
||||
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
|
||||
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() {
|
||||
Log.d(tag, "Preparing Local Media item ${jacksonObjectMapper().writeValueAsString(it)}")
|
||||
var playbackSession = it.getPlaybackSession(episodeId)
|
||||
Log.d(tag, "prepareLibraryItem: Preparing Local Media item ${jacksonMapper.writeValueAsString(it)}")
|
||||
var playbackSession = it.getPlaybackSession(episode)
|
||||
playerNotificationService.preparePlayer(playbackSession, playWhenReady)
|
||||
}
|
||||
return call.resolve(JSObject())
|
||||
@@ -112,11 +121,11 @@ class AbsAudioPlayer : Plugin() {
|
||||
apiHandler.playLibraryItem(libraryItemId, episodeId, false) {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(it)))
|
||||
call.resolve(JSObject(jacksonMapper.writeValueAsString(it)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.util.Log
|
||||
import com.audiobookshelf.app.MainActivity
|
||||
import com.audiobookshelf.app.device.DeviceManager
|
||||
import com.audiobookshelf.app.server.ApiHandler
|
||||
import com.fasterxml.jackson.core.json.JsonReadFeature
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.getcapacitor.*
|
||||
import com.getcapacitor.annotation.CapacitorPlugin
|
||||
@@ -15,6 +16,7 @@ import org.json.JSONObject
|
||||
@CapacitorPlugin(name = "AbsDatabase")
|
||||
class AbsDatabase : Plugin() {
|
||||
val tag = "AbsDatabase"
|
||||
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
||||
|
||||
lateinit var mainActivity: MainActivity
|
||||
lateinit var apiHandler: ApiHandler
|
||||
@@ -26,13 +28,16 @@ class AbsDatabase : Plugin() {
|
||||
override fun load() {
|
||||
mainActivity = (activity as MainActivity)
|
||||
apiHandler = ApiHandler(mainActivity)
|
||||
|
||||
DeviceManager.dbManager.cleanLocalMediaProgress()
|
||||
DeviceManager.dbManager.cleanLocalLibraryItems()
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun getDeviceData(call:PluginCall) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
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) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
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()
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
DeviceManager.dbManager.getLocalFolder(folderId)?.let {
|
||||
var folderObj = jacksonObjectMapper().writeValueAsString(it)
|
||||
var folderObj = jacksonMapper.writeValueAsString(it)
|
||||
call.resolve(JSObject(folderObj))
|
||||
} ?: call.resolve()
|
||||
}
|
||||
@@ -64,7 +69,7 @@ class AbsDatabase : Plugin() {
|
||||
if (localLibraryItem == null) {
|
||||
call.resolve()
|
||||
} else {
|
||||
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(localLibraryItem)))
|
||||
call.resolve(JSObject(jacksonMapper.writeValueAsString(localLibraryItem)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +82,7 @@ class AbsDatabase : Plugin() {
|
||||
if (localLibraryItem == null) {
|
||||
call.resolve()
|
||||
} else {
|
||||
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(localLibraryItem)))
|
||||
call.resolve(JSObject(jacksonMapper.writeValueAsString(localLibraryItem)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,7 +93,7 @@ class AbsDatabase : Plugin() {
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
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()
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
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
|
||||
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) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
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()
|
||||
}
|
||||
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,16 +257,36 @@ class AbsDatabase : Plugin() {
|
||||
return
|
||||
}
|
||||
|
||||
var audioTracks = localLibraryItem.media.getAudioTracks() as MutableList
|
||||
|
||||
var tracks:JSArray = call.getArray("tracks") ?: JSArray()
|
||||
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 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")
|
||||
}
|
||||
}
|
||||
|
||||
call.resolve()
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.audiobookshelf.app.device.DeviceManager
|
||||
import com.audiobookshelf.app.device.FolderScanner
|
||||
import com.audiobookshelf.app.server.ApiHandler
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.core.json.JsonReadFeature
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.getcapacitor.JSObject
|
||||
import com.getcapacitor.Plugin
|
||||
@@ -28,6 +29,7 @@ import java.util.*
|
||||
@CapacitorPlugin(name = "AbsDownloader")
|
||||
class AbsDownloader : Plugin() {
|
||||
private val tag = "AbsDownloader"
|
||||
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
||||
|
||||
lateinit var mainActivity: MainActivity
|
||||
lateinit var downloadManager: DownloadManager
|
||||
@@ -294,7 +296,7 @@ class AbsDownloader : Plugin() {
|
||||
DeviceManager.dbManager.saveDownloadItem(downloadItem)
|
||||
}
|
||||
|
||||
notifyListeners("onItemDownloadUpdate", JSObject(jacksonObjectMapper().writeValueAsString(downloadItem)))
|
||||
notifyListeners("onItemDownloadUpdate", JSObject(jacksonMapper.writeValueAsString(downloadItem)))
|
||||
delay(500)
|
||||
}
|
||||
|
||||
@@ -308,7 +310,7 @@ class AbsDownloader : Plugin() {
|
||||
jsobj.put("libraryItemId", downloadItem.id)
|
||||
jsobj.put("localFolderId", downloadItem.localFolder.id)
|
||||
if (localLibraryItem != null) {
|
||||
jsobj.put("localLibraryItem", JSObject(jacksonObjectMapper().writeValueAsString(localLibraryItem)))
|
||||
jsobj.put("localLibraryItem", JSObject(jacksonMapper.writeValueAsString(localLibraryItem)))
|
||||
}
|
||||
notifyListeners("onItemDownloadComplete", jsobj)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.audiobookshelf.app.data.LocalFolder
|
||||
import com.audiobookshelf.app.data.LocalLibraryItem
|
||||
import com.audiobookshelf.app.device.DeviceManager
|
||||
import com.audiobookshelf.app.device.FolderScanner
|
||||
import com.fasterxml.jackson.core.json.JsonReadFeature
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.getcapacitor.*
|
||||
import com.getcapacitor.annotation.CapacitorPlugin
|
||||
@@ -26,6 +27,7 @@ import kotlinx.coroutines.launch
|
||||
class AbsFileSystem : Plugin() {
|
||||
private val TAG = "AbsFileSystem"
|
||||
private val tag = "AbsFileSystem"
|
||||
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
||||
|
||||
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)
|
||||
|
||||
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) {
|
||||
@@ -148,8 +150,8 @@ class AbsFileSystem : Plugin() {
|
||||
Log.d(TAG, "NO Scan DATA")
|
||||
return call.resolve(JSObject())
|
||||
} else {
|
||||
Log.d(TAG, "Scan DATA ${jacksonObjectMapper().writeValueAsString(folderScanResult)}")
|
||||
return call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(folderScanResult)))
|
||||
Log.d(TAG, "Scan DATA ${jacksonMapper.writeValueAsString(folderScanResult)}")
|
||||
return call.resolve(JSObject(jacksonMapper.writeValueAsString(folderScanResult)))
|
||||
}
|
||||
} ?: call.resolve(JSObject())
|
||||
}
|
||||
@@ -182,8 +184,8 @@ class AbsFileSystem : Plugin() {
|
||||
Log.d(TAG, "NO Scan DATA")
|
||||
call.resolve(JSObject())
|
||||
} else {
|
||||
Log.d(TAG, "Scan DATA ${jacksonObjectMapper().writeValueAsString(scanResult)}")
|
||||
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(scanResult)))
|
||||
Log.d(TAG, "Scan DATA ${jacksonMapper.writeValueAsString(scanResult)}")
|
||||
call.resolve(JSObject(jacksonMapper.writeValueAsString(scanResult)))
|
||||
}
|
||||
} ?: call.resolve(JSObject())
|
||||
}
|
||||
@@ -223,7 +225,7 @@ class AbsFileSystem : Plugin() {
|
||||
localLibraryItem?.media?.removeAudioTrack(trackLocalFileId)
|
||||
localLibraryItem?.removeLocalFile(trackLocalFileId)
|
||||
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
|
||||
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(localLibraryItem)))
|
||||
call.resolve(JSObject(jacksonMapper.writeValueAsString(localLibraryItem)))
|
||||
} else {
|
||||
call.resolve(JSObject("{\"success\":false}"))
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.audiobookshelf.app.data.PlaybackSession
|
||||
import com.audiobookshelf.app.device.DeviceManager
|
||||
import com.audiobookshelf.app.player.MediaProgressSyncData
|
||||
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.readValue
|
||||
import com.getcapacitor.JSArray
|
||||
@@ -26,6 +27,7 @@ import java.io.IOException
|
||||
class ApiHandler {
|
||||
val tag = "ApiHandler"
|
||||
private var client = OkHttpClient()
|
||||
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
||||
var ctx: Context
|
||||
var storageSharedPreferences: SharedPreferences? = null
|
||||
|
||||
@@ -54,6 +56,15 @@ class ApiHandler {
|
||||
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 {
|
||||
val connectivityManager = ctx.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
if (connectivityManager != null) {
|
||||
@@ -105,7 +116,7 @@ class ApiHandler {
|
||||
}
|
||||
|
||||
fun getLibraries(cb: (List<Library>) -> Unit) {
|
||||
val mapper = jacksonObjectMapper()
|
||||
val mapper = jacksonMapper
|
||||
getRequest("/api/libraries") {
|
||||
val libraries = mutableListOf<Library>()
|
||||
if (it.has("value")) {
|
||||
@@ -121,7 +132,7 @@ class ApiHandler {
|
||||
|
||||
fun getLibraryItem(libraryItemId:String, cb: (LibraryItem) -> Unit) {
|
||||
getRequest("/api/items/$libraryItemId?expanded=1") {
|
||||
val libraryItem = jacksonObjectMapper().readValue<LibraryItem>(it.toString())
|
||||
val libraryItem = jacksonMapper.readValue<LibraryItem>(it.toString())
|
||||
cb(libraryItem)
|
||||
}
|
||||
}
|
||||
@@ -132,7 +143,7 @@ class ApiHandler {
|
||||
if (it.has("results")) {
|
||||
var array = it.getJSONArray("results")
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -153,13 +164,13 @@ class ApiHandler {
|
||||
postRequest(endpoint, payload) {
|
||||
it.put("serverConnectionConfigId", DeviceManager.serverConnectionConfig?.id)
|
||||
it.put("serverAddress", DeviceManager.serverAddress)
|
||||
val playbackSession = jacksonObjectMapper().readValue<PlaybackSession>(it.toString())
|
||||
val playbackSession = jacksonMapper.readValue<PlaybackSession>(it.toString())
|
||||
cb(playbackSession)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
cb()
|
||||
@@ -167,7 +178,7 @@ class ApiHandler {
|
||||
}
|
||||
|
||||
fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: () -> Unit) {
|
||||
var payload = JSObject(jacksonObjectMapper().writeValueAsString(playbackSession))
|
||||
var payload = JSObject(jacksonMapper.writeValueAsString(playbackSession))
|
||||
|
||||
postRequest("/api/session/local", payload) {
|
||||
cb()
|
||||
@@ -190,14 +201,14 @@ class ApiHandler {
|
||||
|
||||
if (localMediaProgress.isNotEmpty()) {
|
||||
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) {
|
||||
Log.d(tag, "Media Progress Sync payload $payload - response ${it.toString()}")
|
||||
|
||||
if (it.toString() == "{}") {
|
||||
Log.e(tag, "Progress sync received empty object")
|
||||
} else {
|
||||
val progressSyncResponsePayload = jacksonObjectMapper().readValue<MediaProgressSyncResponsePayload>(it.toString())
|
||||
val progressSyncResponsePayload = jacksonMapper.readValue<MediaProgressSyncResponsePayload>(it.toString())
|
||||
|
||||
localSyncResultsPayload.numLocalProgressUpdates = progressSyncResponsePayload.localProgressUpdates.size
|
||||
localSyncResultsPayload.numServerProgressUpdates = progressSyncResponsePayload.numServerProgressUpdates
|
||||
@@ -217,4 +228,13 @@ class ApiHandler {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user