New data model in android: Adding jackson for JSON deserialization, adding data classes, setting up API caller

This commit is contained in:
advplyr
2022-03-24 19:28:26 -05:00
parent b849882664
commit 461733854a
7 changed files with 416 additions and 6 deletions
+3
View File
@@ -85,6 +85,9 @@ dependencies {
// OK HTTP
implementation 'com.squareup.okhttp3:okhttp:4.9.2'
// Jackson for JSON
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.12.1'
}
apply from: 'capacitor.build.gradle'
@@ -5,7 +5,9 @@ import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.content.ContextCompat
import com.audiobookshelf.app.server.ApiHandler
import com.capacitorjs.plugins.app.AppPlugin
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.getcapacitor.*
import com.getcapacitor.annotation.CapacitorPlugin
import org.json.JSONObject
@@ -15,10 +17,12 @@ class MyNativeAudio : Plugin() {
private val tag = "MyNativeAudio"
lateinit var mainActivity:MainActivity
lateinit var apiHandler:ApiHandler
lateinit var playerNotificationService: PlayerNotificationService
override fun load() {
mainActivity = (activity as MainActivity)
apiHandler = ApiHandler(mainActivity)
var foregroundServiceReady : () -> Unit = {
playerNotificationService = mainActivity.foregroundService
@@ -59,6 +63,37 @@ class MyNativeAudio : Plugin() {
notifyListeners(evtName, ret)
}
@PluginMethod
fun prepareLibraryItem(call: PluginCall) {
var libraryItemId = call.getString("libraryItemId", "").toString()
var mediaEntityId = call.getString("mediaEntityId", "").toString()
apiHandler.playLibraryItem(libraryItemId) {
Handler(Looper.getMainLooper()).post() {
Log.d(tag, "Preparing Player TEST ${jacksonObjectMapper().writeValueAsString(it)}")
playerNotificationService.preparePlayer(it)
}
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(it)))
}
}
@PluginMethod
fun getLibraryItems(call: PluginCall) {
var libraryId = call.getString("libraryId", "").toString()
apiHandler.getLibraryItems(libraryId) {
val mapper = jacksonObjectMapper()
var jsobj = JSObject()
var libarray = JSArray()
it.map {
libarray.put(JSObject(mapper.writeValueAsString(it)))
}
jsobj.put("value", libarray)
call.resolve(jsobj)
}
}
@PluginMethod
fun initPlayer(call: PluginCall) {
if (!PlayerNotificationService.isStarted) {
@@ -24,6 +24,7 @@ import androidx.media.MediaBrowserServiceCompat
import androidx.media.utils.MediaConstants
import com.anggrayudi.storage.file.isExternalStorageDocument
import com.audiobookshelf.app.data.DbManager
import com.audiobookshelf.app.data.PlaybackSession
import com.getcapacitor.Bridge
import com.getcapacitor.JSObject
import com.google.android.exoplayer2.*
@@ -85,6 +86,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
private var channelName = "Audiobookshelf Channel"
private var currentAudiobookStreamData:AudiobookStreamData? = null
private var currentPlaybackSession:PlaybackSession? = null
private var mediaButtonClickCount: Int = 0
var mediaButtonClickTimeout: Long = 1000 //ms
@@ -376,11 +378,11 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
val queueNavigator: TimelineQueueNavigator = object : TimelineQueueNavigator(mediaSession) {
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
var builder = MediaDescriptionCompat.Builder()
.setMediaId(currentAudiobookStreamData!!.id)
.setTitle(currentAudiobookStreamData!!.title)
.setSubtitle(currentAudiobookStreamData!!.author)
.setMediaUri(currentAudiobookStreamData!!.playlistUri)
.setIconUri(currentAudiobookStreamData!!.coverUri)
.setMediaId(currentPlaybackSession!!.id)
.setTitle(currentPlaybackSession!!.getTitle())
.setSubtitle(currentPlaybackSession!!.getAuthor())
// .setMediaUri(currentPlaybackSession!!.getContentUri())
// .setIconUri(currentAudiobookStreamData!!.)
return builder.build()
}
}
@@ -600,7 +602,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
if (currentPlayer.playbackState == Player.STATE_READY) {
Log.d(tag, "STATE_READY : " + mPlayer.duration.toString())
currentAudiobookStreamData!!.hasPlayerLoaded = true
// currentAudiobookStreamData!!.hasPlayerLoaded = true
if (lastPauseTime == 0L) {
sendClientMetadata("ready_no_sync")
lastPauseTime = -1;
@@ -664,6 +666,23 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
/*
User callable methods
*/
fun preparePlayer(playbackSession: PlaybackSession) {
currentPlaybackSession = playbackSession
var metadata = playbackSession.getMediaMetadataCompat()
mediaSession.setMetadata(metadata)
var mediaMetadata = playbackSession.getMediaMetadata()
var mediaUrl = playbackSession.getContentUri()
var mimeType = playbackSession.getMimeType()
Log.d(tag, "Media URL $mediaUrl")
var mediaUri = Uri.parse(mediaUrl)
var mediaItem = MediaItem.Builder().setUri(mediaUri).setMediaMetadata(mediaMetadata).setMimeType(mimeType).build()
var dataSourceFactory = DefaultDataSourceFactory(ctx, channelId)
var mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
mPlayer.setMediaSource(mediaSource, 0L)
mPlayer.prepare()
mPlayer.playWhenReady = true
}
fun initPlayer(audiobookStreamData: AudiobookStreamData) {
currentAudiobookStreamData = audiobookStreamData
@@ -0,0 +1,211 @@
package com.audiobookshelf.app.data
import android.net.Uri
import android.support.v4.media.MediaMetadataCompat
import com.fasterxml.jackson.annotation.*
import com.google.android.exoplayer2.MediaMetadata
@JsonIgnoreProperties(ignoreUnknown = true)
data class LibraryItem(
var id:String,
var ino:String,
var libraryId:String,
var folderId:String,
var path:String,
var relPath:String,
var mtimeMs:Long,
var ctimeMs:Long,
var birthtimeMs:Long,
var addedAt:Long,
var updatedAt:Long,
var lastScan:Long?,
var scanVersion:String?,
var isMissing:Boolean,
var isInvalid:Boolean,
var mediaType:String,
var media:MediaEntity,
var libraryFiles:MutableList<LibraryFile>
)
// This auto-detects whether it is a Book or Podcast
@JsonTypeInfo(use=JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes(
JsonSubTypes.Type(Book::class),
JsonSubTypes.Type(Podcast::class)
)
open class MediaEntity {}
@JsonIgnoreProperties(ignoreUnknown = true)
data class Podcast(
var metadata:PodcastMetadata,
var coverPath:String?,
var tags:MutableList<String>,
var episodes:MutableList<PodcastEpisode>,
var autoDownloadEpisodes:Boolean
) : MediaEntity()
@JsonIgnoreProperties(ignoreUnknown = true)
data class Book(
var metadata:BookMetadata,
var coverPath:String?,
var tags:MutableList<String>,
var audiobooks:MutableList<Audiobook>
) : MediaEntity()
// This auto-detects whether it is a Book or Podcast
@JsonTypeInfo(use=JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes(
JsonSubTypes.Type(BookMetadata::class),
JsonSubTypes.Type(PodcastMetadata::class)
)
open class MediaEntityMetadata {}
@JsonIgnoreProperties(ignoreUnknown = true)
data class BookMetadata(
var title:String,
var subtitle:String?,
var authors:MutableList<Author>
) : MediaEntityMetadata()
@JsonIgnoreProperties(ignoreUnknown = true)
data class PodcastMetadata(
var title:String,
var author:String?,
var feedUrl:String,
var genres:MutableList<String>
) : MediaEntityMetadata()
@JsonIgnoreProperties(ignoreUnknown = true)
data class Author(
var id:String,
var name:String,
var coverPath:String?
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class Audiobook(
var id:String,
var index:Int,
var name:String,
var audioFiles:MutableList<AudioFile>
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class PodcastEpisode(
var id:String,
var index:Int,
var episode:String?,
var episodeType:String?,
var title:String?,
var subtitle:String?,
var description:String?,
var audioFile:AudioFile
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class LibraryFile(
var ino:String,
var metadata:FileMetadata
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class FileMetadata(
var filename:String,
var ext:String,
var path:String,
var relPath:String
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class AudioFile(
var index:Int,
var ino:String,
var metadata:FileMetadata
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class Library(
var id:String,
var name:String,
var folders:MutableList<Folder>,
var icon:String,
var mediaType:String
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class Folder(
var id:String,
var fullPath:String
)
@JsonIgnoreProperties(ignoreUnknown = true)
class PlaybackSession(
var id:String,
var userId:String,
var libraryItemId:String,
var mediaEntityId:String,
var mediaType:String,
var mediaMetadata:MediaEntityMetadata,
var duration:Double,
var playMethod:Int,
var audioTracks:MutableList<AudioTrack>,
var currentTime:Double,
var serverUrl:String,
var token:String
) {
fun getTitle():String {
var metadata = mediaMetadata as BookMetadata
return metadata.title
}
fun getAuthor():String {
var metadata = mediaMetadata as BookMetadata
return metadata.authors.joinToString(",") { it.name }
}
fun getContentUri():String {
// TODO: Using Uri.parse here is throwing error with jackson
var audioTrack = audioTracks[0]
return "$serverUrl${audioTrack.contentUrl}?token=$token"
}
fun getMimeType():String {
var audioTrack = audioTracks[0]
return audioTrack.mimeType
}
fun getMediaMetadataCompat(): MediaMetadataCompat {
var metadata = mediaMetadata as BookMetadata
var metadataBuilder = MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, metadata.title)
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, metadata.title)
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, metadata.authors.joinToString(",") { it.name })
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, metadata.authors.joinToString(",") { it.name })
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, metadata.authors.joinToString(",") { it.name })
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, "series")
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
return metadataBuilder.build()
}
fun getMediaMetadata(): MediaMetadata {
var metadata = mediaMetadata as BookMetadata
var authorName = metadata.authors.joinToString(",") { it.name }
var metadataBuilder = MediaMetadata.Builder()
.setTitle(metadata.title)
.setDisplayTitle(metadata.title)
.setArtist(authorName)
.setAlbumArtist(authorName)
.setSubtitle(authorName)
// var contentUri = this.getContentUri()
// metadataBuilder.setMediaUri(contentUri)
return metadataBuilder.build()
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
data class AudioTrack(
var index:Int,
var startOffset:Double,
var duration:Double,
var title:String,
var contentUrl:String,
var mimeType:String
)
@@ -0,0 +1,124 @@
package com.audiobookshelf.app.server
import android.app.Activity
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import com.audiobookshelf.app.data.Library
import com.audiobookshelf.app.data.LibraryItem
import com.audiobookshelf.app.data.PlaybackSession
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.getcapacitor.JSArray
import com.getcapacitor.JSObject
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException
class ApiHandler {
val tag = "ApiHandler"
private var client = OkHttpClient()
var ctx: Context
var serverUrl = ""
var token = ""
var storageSharedPreferences: SharedPreferences? = null
constructor(_ctx: Context) {
ctx = _ctx
init()
}
fun init() {
storageSharedPreferences = ctx.getSharedPreferences("CapacitorStorage", Activity.MODE_PRIVATE)
serverUrl = storageSharedPreferences?.getString("serverUrl", "").toString()
Log.d(tag, "SHARED PREF SERVERURL $serverUrl")
token = storageSharedPreferences?.getString("token", "").toString()
Log.d(tag, "SHARED PREF TOKEN $token")
}
fun getRequest(endpoint:String, cb: (JSObject) -> Unit) {
val request = Request.Builder()
.url("$serverUrl$endpoint").addHeader("Authorization", "Bearer $token")
.build()
makeRequest(request, cb)
}
fun postRequest(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().post(requestBody)
.url("$serverUrl$endpoint").addHeader("Authorization", "Bearer $token")
.build()
makeRequest(request, cb)
}
fun makeRequest(request:Request, cb: (JSObject) -> Unit) {
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.d(tag, "FAILURE TO CONNECT")
e.printStackTrace()
cb(JSObject())
}
override fun onResponse(call: Call, response: Response) {
response.use {
if (!response.isSuccessful) throw IOException("Unexpected code $response")
var bodyString = response.body!!.string()
var jsonObj = JSObject()
if (bodyString.startsWith("[")) {
var array = JSArray(bodyString)
jsonObj.put("value", array)
} else {
jsonObj = JSObject(bodyString)
}
cb(jsonObj)
}
}
})
}
fun getLibraries(cb: (List<Library>) -> Unit) {
val mapper = jacksonObjectMapper()
getRequest("/api/libraries") {
val libraries = mutableListOf<Library>()
if (it.has("value")) {
var array = it.getJSONArray("value")!!
for (i in 0 until array.length()) {
val library = mapper.readValue<Library>(array.get(i).toString())
libraries.add(library)
}
}
cb(libraries)
}
}
fun getLibraryItems(libraryId:String, cb: (List<LibraryItem>) -> Unit) {
val mapper = jacksonObjectMapper()
getRequest("/api/libraries/$libraryId/items") {
val items = mutableListOf<LibraryItem>()
if (it.has("results")) {
var array = it.getJSONArray("results")
for (i in 0 until array.length()) {
val item = mapper.readValue<LibraryItem>(array.get(i).toString())
items.add(item)
}
}
cb(items)
}
}
fun playLibraryItem(libraryItemId:String, cb: (PlaybackSession) -> Unit) {
val mapper = jacksonObjectMapper()
var payload = JSObject()
payload.put("mediaPlayer", "exo-player")
payload.put("forceDirectPlay", true)
postRequest("/api/items/$libraryItemId/play", payload) {
it.put("serverUrl", serverUrl)
it.put("token", token)
val playbackSession = mapper.readValue<PlaybackSession>(it.toString())
cb(playbackSession)
}
}
}
+6
View File
@@ -479,6 +479,12 @@ export default {
this.$store.commit('setLibraryItemStream', libraryItem)
// TODO: Call load library item in native
console.log('TEST prepare library item', libraryItemId)
MyNativeAudio.prepareLibraryItem({ libraryItemId }).then((data) => {
console.log('TEST library item play response', JSON.stringify(data))
}).catch((error) => {
console.error('TEST failed', error)
})
}
},
mounted() {
+12
View File
@@ -26,12 +26,16 @@
<ui-btn v-if="!isUpdateAvailable || immediateUpdateAllowed" class="w-full my-4" color="primary" @click="openAppStore">Open app store</ui-btn>
<!-- Used for testing API -->
<ui-btn @click="testCall">Test Call</ui-btn>
<p class="text-xs text-gray-400">UA: {{ updateAvailability }} | Avail: {{ availableVersion }} | Curr: {{ currentVersion }} | ImmedAllowed: {{ immediateUpdateAllowed }}</p>
</div>
</template>
<script>
import { AppUpdate } from '@robingenz/capacitor-app-update'
import MyNativeAudio from '@/plugins/my-native-audio'
export default {
asyncData({ redirect, store }) {
@@ -74,6 +78,14 @@ export default {
}
},
methods: {
testCall() {
// Used for testing API
console.log('Making Test call')
var libraryId = this.$store.state.libraries.currentLibraryId
MyNativeAudio.getLibraryItems({ libraryId }).then((res) => {
console.log('TEST CALL response', JSON.stringify(res))
})
},
async logout() {
await this.$axios.$post('/logout').catch((error) => {
console.error(error)