Merge branch 'advplyr:master' into vangorra/aa-button-cleanup

This commit is contained in:
Robert Van Gorkom
2022-09-04 09:23:51 -07:00
committed by GitHub
42 changed files with 1465 additions and 814 deletions
+2 -2
View File
@@ -33,8 +33,8 @@ android {
applicationId "com.audiobookshelf.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 87
versionName "0.9.56-beta"
versionCode 88
versionName "0.9.57-beta"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -1,69 +1,11 @@
package com.audiobookshelf.app.data
import android.net.Uri
import android.os.Bundle
import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.MediaMetadataCompat
import com.audiobookshelf.app.R
import com.audiobookshelf.app.device.DeviceManager
import androidx.media.utils.MediaConstants
import com.fasterxml.jackson.annotation.*
@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:MediaType,
var libraryFiles:MutableList<LibraryFile>?,
var userMediaProgress:MediaProgress? // Only included when requesting library item with progress (for downloads)
) : LibraryItemWrapper() {
@get:JsonIgnore
val title get() = media.metadata.title
@get:JsonIgnore
val authorName get() = media.metadata.getAuthorDisplayName()
@JsonIgnore
fun getCoverUri():Uri {
if (media.coverPath == null) {
return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
}
return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover?token=${DeviceManager.token}")
}
@JsonIgnore
fun checkHasTracks():Boolean {
return if (mediaType == "podcast") {
((media as Podcast).numEpisodes ?: 0) > 0
} else {
((media as Book).numTracks ?: 0) > 0
}
}
@JsonIgnore
fun getMediaMetadata(): MediaMetadataCompat {
return MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, authorName)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, getCoverUri().toString())
putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, authorName)
}.build()
}
}
// This auto-detects whether it is a Book or Podcast
@JsonTypeInfo(use=JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes(
@@ -121,7 +63,7 @@ class Podcast(
}
@JsonIgnore
override fun addAudioTrack(audioTrack:AudioTrack) {
val newEpisode = PodcastEpisode("local_" + audioTrack.localFileId,episodes?.size ?: 0 + 1,null,null,audioTrack.title,null,null,null,audioTrack,audioTrack.duration,0, null)
val 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
@@ -142,7 +84,7 @@ class Podcast(
}
@JsonIgnore
fun addEpisode(audioTrack:AudioTrack, episode:PodcastEpisode):PodcastEpisode {
val 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)
val 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
@@ -293,24 +235,51 @@ data class PodcastEpisode(
var serverEpisodeId:String? // For local podcasts to match with server podcasts
) {
@JsonIgnore
fun getMediaMetadata(libraryItem:LibraryItemWrapper): MediaMetadataCompat {
var coverUri:Uri = Uri.EMPTY
val podcast = if(libraryItem is LocalLibraryItem) {
coverUri = libraryItem.getCoverUri()
libraryItem.media as Podcast
fun getMediaDescription(libraryItem:LibraryItemWrapper, progress:MediaProgressWrapper?): MediaDescriptionCompat {
val coverUri = if(libraryItem is LocalLibraryItem) {
libraryItem.getCoverUri()
} else {
coverUri = (libraryItem as LibraryItem).getCoverUri()
(libraryItem as LibraryItem).media as Podcast
(libraryItem as LibraryItem).getCoverUri()
}
return MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, podcast.metadata.getAuthorDisplayName())
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString())
}.build()
val extras = Bundle()
if (progress != null) {
if (progress.isFinished) {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED
)
} else {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
)
extras.putDouble(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress.progress
)
}
} else {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
)
}
// return MediaMetadataCompat.Builder().apply {
// putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
// putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
// putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
// putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, podcast.metadata.getAuthorDisplayName())
// putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString())
//
// }.build()
val libraryItemDescription = libraryItem.getMediaDescription(null)
return MediaDescriptionCompat.Builder()
.setMediaId(id)
.setTitle(title)
.setIconUri(coverUri)
.setSubtitle(libraryItemDescription.title)
.setExtras(extras)
.build()
}
}
@@ -1,5 +1,6 @@
package com.audiobookshelf.app.data
import android.support.v4.media.MediaDescriptionCompat
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonSubTypes
@@ -92,7 +93,10 @@ data class LocalFolder(
JsonSubTypes.Type(LibraryItem::class),
JsonSubTypes.Type(LocalLibraryItem::class)
)
open class LibraryItemWrapper()
open class LibraryItemWrapper(var id:String) {
@JsonIgnore
open fun getMediaDescription(progress:MediaProgressWrapper?): MediaDescriptionCompat { return MediaDescriptionCompat.Builder().build() }
}
@JsonIgnoreProperties(ignoreUnknown = true)
data class DeviceInfo(
@@ -0,0 +1,34 @@
/*
Used in Android Auto to represent a podcast episode or an audiobook in progress
*/
package com.audiobookshelf.app.data
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 org.json.JSONObject
@JsonIgnoreProperties(ignoreUnknown = true)
data class ItemInProgress(
val libraryItemWrapper: LibraryItemWrapper,
val episode: PodcastEpisode?,
val progressLastUpdate: Long,
val isLocal: Boolean
) {
companion object {
fun makeFromServerObject(serverItem: JSONObject):ItemInProgress {
val jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
val libraryItem = jacksonMapper.readValue<LibraryItem>(serverItem.toString())
var episode:PodcastEpisode? = null
if (serverItem.has("recentEpisode")) {
episode = jacksonMapper.readValue<PodcastEpisode>(serverItem.get("recentEpisode").toString())
}
val progressLastUpdate:Long = serverItem.getLong("progressLastUpdate")
return ItemInProgress(libraryItem, episode, progressLastUpdate, false)
}
}
}
@@ -0,0 +1,90 @@
package com.audiobookshelf.app.data
import android.net.Uri
import android.os.Bundle
import android.support.v4.media.MediaDescriptionCompat
import androidx.media.utils.MediaConstants
import com.audiobookshelf.app.R
import com.audiobookshelf.app.device.DeviceManager
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
class LibraryItem(
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:MediaType,
var libraryFiles:MutableList<LibraryFile>?,
var userMediaProgress:MediaProgress? // Only included when requesting library item with progress (for downloads)
) : LibraryItemWrapper(id) {
@get:JsonIgnore
val title get() = media.metadata.title
@get:JsonIgnore
val authorName get() = media.metadata.getAuthorDisplayName()
@JsonIgnore
fun getCoverUri(): Uri {
if (media.coverPath == null) {
return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
}
return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover?token=${DeviceManager.token}")
}
@JsonIgnore
fun checkHasTracks():Boolean {
return if (mediaType == "podcast") {
((media as Podcast).numEpisodes ?: 0) > 0
} else {
((media as Book).numTracks ?: 0) > 0
}
}
@JsonIgnore
override fun getMediaDescription(progress:MediaProgressWrapper?): MediaDescriptionCompat {
val extras = Bundle()
if (progress != null) {
if (progress.isFinished) {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED
)
} else {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
)
extras.putDouble(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress.progress
)
}
} else if (mediaType != "podcast") {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
)
}
return MediaDescriptionCompat.Builder()
.setMediaId(id)
.setTitle(title)
.setIconUri(getCoverUri())
.setSubtitle(authorName)
.setExtras(extras)
.build()
}
}
@@ -1,10 +1,8 @@
package com.audiobookshelf.app.data
import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.MediaMetadataCompat
import android.util.Log
import androidx.media.utils.MediaConstants
import com.audiobookshelf.app.R
@@ -14,8 +12,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import java.util.*
@JsonIgnoreProperties(ignoreUnknown = true)
data class LocalLibraryItem(
var id:String,
class LocalLibraryItem(
id:String,
var folderId:String,
var basePath:String,
var absolutePath:String,
@@ -32,7 +30,7 @@ data class LocalLibraryItem(
var serverAddress:String?,
var serverUserId:String?,
var libraryItemId:String?
) : LibraryItemWrapper() {
) : LibraryItemWrapper(id) {
@get:JsonIgnore
val title get() = media.metadata.title
@get:JsonIgnore
@@ -98,16 +96,38 @@ data class LocalLibraryItem(
}
@JsonIgnore
fun getMediaMetadata(): MediaMetadataCompat {
override fun getMediaDescription(progress:MediaProgressWrapper?): MediaDescriptionCompat {
val coverUri = getCoverUri()
return MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, authorName)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString())
putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, authorName)
}.build()
val extras = Bundle()
if (progress != null) {
if (progress.isFinished) {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED
)
} else {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
)
extras.putDouble(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress.progress
)
}
} else if (mediaType != "podcast") {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
)
}
return MediaDescriptionCompat.Builder()
.setMediaId(id)
.setTitle(title)
.setIconUri(coverUri)
.setSubtitle(authorName)
.setExtras(extras)
.build()
}
}
@@ -2,12 +2,8 @@ package com.audiobookshelf.app.media
import android.app.Activity
import android.content.Context
import android.os.Bundle
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.MediaMetadataCompat
import android.util.Log
import androidx.media.utils.MediaConstants
import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.server.ApiHandler
@@ -26,11 +22,11 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
var selectedLibraryItems = mutableListOf<LibraryItem>()
var selectedLibraryId = ""
var selectedLibraryItemWrapper:LibraryItemWrapper? = null
var selectedPodcast:Podcast? = null
var selectedLibraryItemId:String? = null
var serverPodcastEpisodes = listOf<PodcastEpisode>()
var podcastEpisodeLibraryItemMap = mutableMapOf<String, LibraryItemWithEpisode>()
var serverLibraryCategories = listOf<LibraryCategory>()
var serverItemsInProgress = listOf<ItemInProgress>()
var serverLibraries = listOf<Library>()
var serverConfigIdUsed:String? = null
var serverConfigLastPing:Long = 0L
@@ -71,7 +67,7 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
val serverConnConfig = if (DeviceManager.isConnectedToServer) DeviceManager.serverConnectionConfig else DeviceManager.deviceData.getLastServerConnectionConfig()
if (!DeviceManager.isConnectedToServer || !apiHandler.isOnline() || serverConnConfig == null || serverConnConfig.id !== serverConfigIdUsed) {
serverPodcastEpisodes = listOf()
podcastEpisodeLibraryItemMap = mutableMapOf()
serverLibraryCategories = listOf()
serverLibraries = listOf()
serverLibraryItems = mutableListOf()
@@ -80,13 +76,13 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
}
}
fun loadLibraryCategories(libraryId:String, cb: (List<LibraryCategory>) -> Unit) {
if (serverLibraryCategories.isNotEmpty()) {
cb(serverLibraryCategories)
fun loadItemsInProgressForAllLibraries(cb: (List<ItemInProgress>) -> Unit) {
if (serverItemsInProgress.isNotEmpty()) {
cb(serverItemsInProgress)
} else {
apiHandler.getLibraryCategories(libraryId) {
serverLibraryCategories = it
cb(it)
apiHandler.getAllItemsInProgress { itemsInProgress ->
serverItemsInProgress = itemsInProgress
cb(serverItemsInProgress)
}
}
}
@@ -129,44 +125,39 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
loadLibraryItem(libraryItemId) { libraryItemWrapper ->
Log.d(tag, "Loaded Podcast library item $libraryItemWrapper")
selectedLibraryItemWrapper = libraryItemWrapper
libraryItemWrapper?.let {
if (libraryItemWrapper is LocalLibraryItem) { // Local podcast episodes
if (libraryItemWrapper.mediaType != "podcast" || libraryItemWrapper.media.getAudioTracks().isEmpty()) {
serverPodcastEpisodes = listOf()
cb(mutableListOf())
} else {
val podcast = libraryItemWrapper.media as Podcast
serverPodcastEpisodes = podcast.episodes ?: listOf()
selectedLibraryItemId = libraryItemWrapper.id
selectedPodcast = podcast
val children = podcast.episodes?.map { podcastEpisode ->
Log.d(tag, "Local Podcast Episode ${podcastEpisode.title} | ${podcastEpisode.id}")
val mediaMetadata = podcastEpisode.getMediaMetadata(libraryItemWrapper)
val progress = DeviceManager.dbManager.getLocalMediaProgress("${libraryItemWrapper.id}-${podcastEpisode.id}")
val description = getMediaDescriptionFromMediaMetadata(mediaMetadata, progress)
val description = podcastEpisode.getMediaDescription(libraryItemWrapper, progress)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
}
children?.let { cb(children as MutableList) } ?: cb(mutableListOf())
}
} else if (libraryItemWrapper is LibraryItem) { // Server podcast episodes
if (libraryItemWrapper.mediaType != "podcast" || libraryItemWrapper.media.getAudioTracks().isEmpty()) {
serverPodcastEpisodes = listOf()
cb(mutableListOf())
} else {
val podcast = libraryItemWrapper.media as Podcast
serverPodcastEpisodes = podcast.episodes ?: listOf()
podcast.episodes?.forEach { podcastEpisode ->
podcastEpisodeLibraryItemMap[podcastEpisode.id] = LibraryItemWithEpisode(libraryItemWrapper, podcastEpisode)
}
selectedLibraryItemId = libraryItemWrapper.id
selectedPodcast = podcast
val children = podcast.episodes?.map { podcastEpisode ->
val mediaMetadata = podcastEpisode.getMediaMetadata(libraryItemWrapper)
val progress = serverUserMediaProgress.find { it.libraryItemId == libraryItemWrapper.id && it.episodeId == podcastEpisode.id }
val description = getMediaDescriptionFromMediaMetadata(mediaMetadata, progress)
val description = podcastEpisode.getMediaDescription(libraryItemWrapper, progress)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
}
children?.let { cb(children as MutableList) } ?: cb(mutableListOf())
@@ -275,26 +266,8 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
}
// TODO: Load currently listening category for local items
fun loadLocalCategory():List<LibraryCategory> {
val localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
val localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast")
val cats = mutableListOf<LibraryCategory>()
if (localBooks.isNotEmpty()) {
cats.add(LibraryCategory("local-books", "Local Books", "book", localBooks, true))
}
if (localPodcasts.isNotEmpty()) {
cats.add(LibraryCategory("local-podcasts", "Local Podcasts", "podcast", localPodcasts, true))
}
return cats
}
fun loadAndroidAutoItems(cb: (List<LibraryCategory>) -> Unit) {
fun loadAndroidAutoItems(cb: () -> Unit) {
Log.d(tag, "Load android auto items")
val cats = mutableListOf<LibraryCategory>()
val localCategories = loadLocalCategory()
cats.addAll(localCategories)
// Check if any valid server connection if not use locally downloaded books
checkSetValidServerConnectionConfig { isConnected ->
@@ -304,40 +277,29 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
loadLibraries { libraries ->
if (libraries.isEmpty()) {
Log.w(tag, "No libraries returned from server request")
cb(cats) // Return download category only
cb()
} else {
val library = libraries[0]
Log.d(tag, "Loading categories for library ${library.name} - ${library.id} - ${library.mediaType}")
loadLibraryCategories(library.id) { libraryCategories ->
// Only using book or podcast library categories for now
libraryCategories.forEach {
// Add items in continue listening to serverLibraryItems
if (it.id == "continue-listening") {
it.entities.forEach { libraryItemWrapper ->
val libraryItem = libraryItemWrapper as LibraryItem
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
}
loadItemsInProgressForAllLibraries { itemsInProgress ->
itemsInProgress.forEach {
val libraryItem = it.libraryItemWrapper as LibraryItem
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
// Log.d(tag, "Found library category ${it.label} with type ${it.type}")
if (it.type == library.mediaType) {
// Log.d(tag, "Using library category ${it.id}")
cats.add(it)
if (it.episode != null) {
podcastEpisodeLibraryItemMap[it.episode.id] = LibraryItemWithEpisode(it.libraryItemWrapper, it.episode)
}
}
cb(cats)
cb() // Fully loaded
}
}
}
} else { // Not connected/no internet sent downloaded cats only
cb(cats)
} else { // Not connected to server
cb()
}
}
}
@@ -355,12 +317,7 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
if (id.startsWith("local")) {
return DeviceManager.dbManager.getLocalLibraryItemWithEpisode(id)
} else {
val podcastEpisode = serverPodcastEpisodes.find { it.id == id }
return if (podcastEpisode != null && selectedLibraryItemWrapper != null) {
LibraryItemWithEpisode(selectedLibraryItemWrapper!!, podcastEpisode)
} else {
null
}
return podcastEpisodeLibraryItemMap[id]
}
}
@@ -394,41 +351,6 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
}
}
fun getMediaDescriptionFromMediaMetadata(item: MediaMetadataCompat, progress:MediaProgressWrapper?): MediaDescriptionCompat {
val extras = Bundle()
if (progress != null) {
Log.d(tag, "Has media progress for ${item.description.title} | ${progress}")
if (progress.isFinished) {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED
)
} else {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
)
extras.putDouble(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress.progress
)
}
} else {
Log.d(tag, "No media progress for ${item.description.title} | ${item.description.mediaId}")
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
)
}
return MediaDescriptionCompat.Builder()
.setMediaId(item.description.mediaId)
.setTitle(item.description.title)
.setIconUri(item.description.iconUri)
.setSubtitle(item.description.subtitle)
.setExtras(extras).build()
}
private fun levenshtein(lhs : CharSequence, rhs : CharSequence) : Int {
val lhsLength = lhs.length + 1
val rhsLength = rhs.length + 1
@@ -10,7 +10,7 @@ import com.audiobookshelf.app.data.*
class BrowseTree(
val context: Context,
libraryCategories: List<LibraryCategory>,
itemsInProgress: List<ItemInProgress>,
libraries: List<Library>
) {
private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>()
@@ -21,8 +21,7 @@ class BrowseTree(
* @param drawableId - drawable res id
* @return - uri
*/
fun getUriToDrawable(context: Context,
@AnyRes drawableId: Int): Uri {
fun getUriToDrawable(@AnyRes drawableId: Int): Uri {
return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE
+ "://" + context.resources.getResourcePackageName(drawableId)
+ '/' + context.resources.getResourceTypeName(drawableId)
@@ -32,38 +31,26 @@ class BrowseTree(
init {
val rootList = mediaIdToChildren[AUTO_BROWSE_ROOT] ?: mutableListOf()
val continueReadingMetadata = MediaMetadataCompat.Builder().apply {
val continueListeningMetadata = MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, CONTINUE_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Listening")
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_localaudio).toString())
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.exo_icon_localaudio).toString())
}.build()
val downloadsMetadata = MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, DOWNLOADS_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Downloads")
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_downloaddone).toString())
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.exo_icon_downloaddone).toString())
}.build()
val librariesMetadata = MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, LIBRARIES_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Libraries")
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.icon_library_folder).toString())
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.icon_library_folder).toString())
}.build()
// Server continue Listening cat
libraryCategories.find { it.id == "continue-listening" }?.let { continueListeningCategory ->
val continueListeningMediaMetadata = continueListeningCategory.entities.map { liw ->
val libraryItem = liw as LibraryItem
libraryItem.getMediaMetadata()
}
if (continueListeningMediaMetadata.isNotEmpty()) {
rootList += continueReadingMetadata
}
continueListeningMediaMetadata.forEach {
val children = mediaIdToChildren[CONTINUE_ROOT] ?: mutableListOf()
children += it
mediaIdToChildren[CONTINUE_ROOT] = children
}
if (!itemsInProgress.isEmpty()) {
rootList += continueListeningMetadata
}
if (libraries.isNotEmpty()) {
@@ -78,23 +65,6 @@ class BrowseTree(
}
rootList += downloadsMetadata
libraryCategories.find { it.id == "local-books" }?.let { localBooksCat ->
localBooksCat.entities.forEach { libc ->
val libraryItem = libc as LocalLibraryItem
val children = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf()
children += libraryItem.getMediaMetadata()
mediaIdToChildren[DOWNLOADS_ROOT] = children
}
}
libraryCategories.find { it.id == "local-podcasts" }?.let { localPodcastsCat ->
localPodcastsCat.entities.forEach { libc ->
val libraryItem = libc as LocalLibraryItem
val children = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf()
children += libraryItem.getMediaMetadata()
mediaIdToChildren[DOWNLOADS_ROOT] = children
}
}
mediaIdToChildren[AUTO_BROWSE_ROOT] = rootList
}
@@ -807,8 +807,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
private val VALID_MEDIA_BROWSERS = mutableListOf("com.audiobookshelf.app", ANDROID_AUTO_PKG_NAME, ANDROID_AUTO_SIMULATOR_PKG_NAME, ANDROID_WEARABLE_PKG_NAME, ANDROID_GSEARCH_PKG_NAME, ANDROID_AUTOMOTIVE_PKG_NAME)
private val AUTO_MEDIA_ROOT = "/"
private val ALL_ROOT = "__ALL__"
private val LIBRARIES_ROOT = "__LIBRARIES__"
private val DOWNLOADS_ROOT = "__DOWNLOADS__"
private val CONTINUE_ROOT = "__CONTINUE__"
private lateinit var browseTree:BrowseTree
@@ -851,8 +852,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
override fun onLoadChildren(parentMediaId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
Log.d(tag, "ON LOAD CHILDREN $parentMediaId")
val flag = if (parentMediaId == AUTO_MEDIA_ROOT || parentMediaId == LIBRARIES_ROOT) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
result.detach()
if (parentMediaId.startsWith("li_") || parentMediaId.startsWith("local_")) { // Show podcast episodes
@@ -864,53 +863,67 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
mediaManager.loadLibraryItemsWithAudio(parentMediaId) { libraryItems ->
val children = libraryItems.map { libraryItem ->
val libraryItemMediaMetadata = libraryItem.getMediaMetadata()
if (libraryItem.mediaType == "podcast") { // Podcasts are browseable
MediaBrowserCompat.MediaItem(libraryItemMediaMetadata.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
val mediaDescription = libraryItem.getMediaDescription(null)
MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
} else {
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItemMediaMetadata.description.mediaId }
val description = mediaManager.getMediaDescriptionFromMediaMetadata(libraryItemMediaMetadata, progress)
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
val description = libraryItem.getMediaDescription(progress)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
}
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
} else if (parentMediaId == "__DOWNLOADS__") { // Load downloads
} else if (parentMediaId == DOWNLOADS_ROOT) { // Load downloads
val localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
val localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast")
val localBrowseItems:MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
localBooks.forEach { localLibraryItem ->
val mediaMetadata = localLibraryItem.getMediaMetadata()
val progress = DeviceManager.dbManager.getLocalMediaProgress(mediaMetadata.description.mediaId ?: "")
val description = mediaManager.getMediaDescriptionFromMediaMetadata(mediaMetadata, progress)
val progress = DeviceManager.dbManager.getLocalMediaProgress(localLibraryItem.id)
val description = localLibraryItem.getMediaDescription(progress)
localBrowseItems += MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
}
localPodcasts.forEach { localLibraryItem ->
val mediaMetadata = localLibraryItem.getMediaMetadata()
localBrowseItems += MediaBrowserCompat.MediaItem(mediaMetadata.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
val mediaDescription = localLibraryItem.getMediaDescription(null)
localBrowseItems += MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
}
result.sendResult(localBrowseItems)
} else { // Load categories
mediaManager.loadAndroidAutoItems { libraryCategories ->
browseTree = BrowseTree(this, libraryCategories, mediaManager.serverLibraries)
} else if (parentMediaId == CONTINUE_ROOT) {
val localBrowseItems:MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
mediaManager.serverItemsInProgress.forEach { itemInProgress ->
val progress: MediaProgressWrapper?
val mediaDescription:MediaDescriptionCompat
if (itemInProgress.episode != null) {
if (itemInProgress.isLocal) {
progress = DeviceManager.dbManager.getLocalMediaProgress("${itemInProgress.libraryItemWrapper.id}-${itemInProgress.episode.id}")
} else {
progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == itemInProgress.libraryItemWrapper.id && it.episodeId == itemInProgress.episode.id }
}
mediaDescription = itemInProgress.episode.getMediaDescription(itemInProgress.libraryItemWrapper,progress)
} else {
if (itemInProgress.isLocal) {
progress = DeviceManager.dbManager.getLocalMediaProgress(itemInProgress.libraryItemWrapper.id)
} else {
progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == itemInProgress.libraryItemWrapper.id }
}
mediaDescription = itemInProgress.libraryItemWrapper.getMediaDescription(progress)
}
localBrowseItems += MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
}
result.sendResult(localBrowseItems)
} else {
mediaManager.loadAndroidAutoItems {
browseTree = BrowseTree(this, mediaManager.serverItemsInProgress, mediaManager.serverLibraries)
val children = browseTree[parentMediaId]?.map { item ->
Log.d(tag, "Loading Browser Media Item ${item.description.title} $flag")
if (flag == MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) {
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == item.description.mediaId }
val description = mediaManager.getMediaDescriptionFromMediaMetadata(item, progress)
MediaBrowserCompat.MediaItem(description, flag)
} else {
MediaBrowserCompat.MediaItem(item.description, flag)
}
Log.d(tag, "Loading Browser Media Item ${item.description.title}")
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
@@ -919,10 +932,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
override fun onSearch(query: String, extras: Bundle?, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
result.detach()
mediaManager.loadAndroidAutoItems() { libraryCategories ->
browseTree = BrowseTree(this, libraryCategories, mediaManager.serverLibraries)
val children = browseTree[ALL_ROOT]?.map { item ->
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
mediaManager.loadAndroidAutoItems {
browseTree = BrowseTree(this, mediaManager.serverItemsInProgress, mediaManager.serverLibraries)
val children = browseTree[LIBRARIES_ROOT]?.map { item ->
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
@@ -189,21 +189,16 @@ class ApiHandler(var ctx:Context) {
}
}
fun getLibraryCategories(libraryId:String, cb: (List<LibraryCategory>) -> Unit) {
getRequest("/api/libraries/$libraryId/personalized", null, null) {
val items = mutableListOf<LibraryCategory>()
if (it.has("value")) {
val array = it.getJSONArray("value")
fun getAllItemsInProgress(cb: (List<ItemInProgress>) -> Unit) {
getRequest("/api/me/items-in-progress", null, null) {
val items = mutableListOf<ItemInProgress>()
if (it.has("libraryItems")) {
val array = it.getJSONArray("libraryItems")
for (i in 0 until array.length()) {
val jsobj = array.get(i) as JSONObject
val type = jsobj.get("type").toString()
// Only support for podcast and book in android auto
if (type == "podcast" || type == "book") {
jsobj.put("isLocal", false)
val item = jacksonMapper.readValue<LibraryCategory>(jsobj.toString())
items.add(item)
}
val itemInProgress = ItemInProgress.makeFromServerObject(jsobj)
items.add(itemInProgress)
}
}
cb(items)