mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-07 12:28:50 +02:00
New data model in android: Adding jackson for JSON deserialization, adding data classes, setting up API caller
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user