Merge branch 'master' into master

This commit is contained in:
advplyr
2022-04-15 21:05:40 -05:00
committed by GitHub
32 changed files with 624 additions and 193 deletions
@@ -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()
}
}
}