Update:Android auto show in progress media items across all libraries #285

This commit is contained in:
advplyr
2022-08-24 17:55:07 -05:00
parent 8bd0e863ae
commit bba388aefa
9 changed files with 295 additions and 278 deletions
@@ -1,69 +1,11 @@
package com.audiobookshelf.app.data 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 android.support.v4.media.MediaMetadataCompat
import com.audiobookshelf.app.R import androidx.media.utils.MediaConstants
import com.audiobookshelf.app.device.DeviceManager
import com.fasterxml.jackson.annotation.* 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 // This auto-detects whether it is a Book or Podcast
@JsonTypeInfo(use=JsonTypeInfo.Id.DEDUCTION) @JsonTypeInfo(use=JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes( @JsonSubTypes(
@@ -121,7 +63,7 @@ class Podcast(
} }
@JsonIgnore @JsonIgnore
override fun addAudioTrack(audioTrack:AudioTrack) { 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) episodes?.add(newEpisode)
var index = 1 var index = 1
@@ -142,7 +84,7 @@ class Podcast(
} }
@JsonIgnore @JsonIgnore
fun addEpisode(audioTrack:AudioTrack, episode:PodcastEpisode):PodcastEpisode { 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) episodes?.add(newEpisode)
var index = 1 var index = 1
@@ -293,24 +235,51 @@ data class PodcastEpisode(
var serverEpisodeId:String? // For local podcasts to match with server podcasts var serverEpisodeId:String? // For local podcasts to match with server podcasts
) { ) {
@JsonIgnore @JsonIgnore
fun getMediaMetadata(libraryItem:LibraryItemWrapper): MediaMetadataCompat { fun getMediaDescription(libraryItem:LibraryItemWrapper, progress:MediaProgressWrapper?): MediaDescriptionCompat {
var coverUri:Uri = Uri.EMPTY val coverUri = if(libraryItem is LocalLibraryItem) {
val podcast = if(libraryItem is LocalLibraryItem) { libraryItem.getCoverUri()
coverUri = libraryItem.getCoverUri()
libraryItem.media as Podcast
} else { } else {
coverUri = (libraryItem as LibraryItem).getCoverUri() (libraryItem as LibraryItem).getCoverUri()
(libraryItem as LibraryItem).media as Podcast
} }
return MediaMetadataCompat.Builder().apply { val extras = Bundle()
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id) if (progress != null) {
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title) if (progress.isFinished) {
putString(MediaMetadataCompat.METADATA_KEY_TITLE, title) extras.putInt(
putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, podcast.metadata.getAuthorDisplayName()) MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString()) MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED
)
}.build() } 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 package com.audiobookshelf.app.data
import android.support.v4.media.MediaDescriptionCompat
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonSubTypes
@@ -92,7 +93,10 @@ data class LocalFolder(
JsonSubTypes.Type(LibraryItem::class), JsonSubTypes.Type(LibraryItem::class),
JsonSubTypes.Type(LocalLibraryItem::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) @JsonIgnoreProperties(ignoreUnknown = true)
data class DeviceInfo( 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 package com.audiobookshelf.app.data
import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.support.v4.media.MediaDescriptionCompat import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.MediaMetadataCompat
import android.util.Log import android.util.Log
import androidx.media.utils.MediaConstants import androidx.media.utils.MediaConstants
import com.audiobookshelf.app.R import com.audiobookshelf.app.R
@@ -14,8 +12,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import java.util.* import java.util.*
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class LocalLibraryItem( class LocalLibraryItem(
var id:String, id:String,
var folderId:String, var folderId:String,
var basePath:String, var basePath:String,
var absolutePath:String, var absolutePath:String,
@@ -32,7 +30,7 @@ data class LocalLibraryItem(
var serverAddress:String?, var serverAddress:String?,
var serverUserId:String?, var serverUserId:String?,
var libraryItemId:String? var libraryItemId:String?
) : LibraryItemWrapper() { ) : LibraryItemWrapper(id) {
@get:JsonIgnore @get:JsonIgnore
val title get() = media.metadata.title val title get() = media.metadata.title
@get:JsonIgnore @get:JsonIgnore
@@ -98,16 +96,38 @@ data class LocalLibraryItem(
} }
@JsonIgnore @JsonIgnore
fun getMediaMetadata(): MediaMetadataCompat { override fun getMediaDescription(progress:MediaProgressWrapper?): MediaDescriptionCompat {
val coverUri = getCoverUri() val coverUri = getCoverUri()
return MediaMetadataCompat.Builder().apply { val extras = Bundle()
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id) if (progress != null) {
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title) if (progress.isFinished) {
putString(MediaMetadataCompat.METADATA_KEY_TITLE, title) extras.putInt(
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, authorName) MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString()) MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED
putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, authorName) )
}.build() } 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.app.Activity
import android.content.Context import android.content.Context
import android.os.Bundle
import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.MediaMetadataCompat
import android.util.Log import android.util.Log
import androidx.media.utils.MediaConstants
import com.audiobookshelf.app.data.* import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.server.ApiHandler import com.audiobookshelf.app.server.ApiHandler
@@ -26,11 +22,11 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
var selectedLibraryItems = mutableListOf<LibraryItem>() var selectedLibraryItems = mutableListOf<LibraryItem>()
var selectedLibraryId = "" var selectedLibraryId = ""
var selectedLibraryItemWrapper:LibraryItemWrapper? = null
var selectedPodcast:Podcast? = null var selectedPodcast:Podcast? = null
var selectedLibraryItemId:String? = null var selectedLibraryItemId:String? = null
var serverPodcastEpisodes = listOf<PodcastEpisode>() var podcastEpisodeLibraryItemMap = mutableMapOf<String, LibraryItemWithEpisode>()
var serverLibraryCategories = listOf<LibraryCategory>() var serverLibraryCategories = listOf<LibraryCategory>()
var serverItemsInProgress = listOf<ItemInProgress>()
var serverLibraries = listOf<Library>() var serverLibraries = listOf<Library>()
var serverConfigIdUsed:String? = null var serverConfigIdUsed:String? = null
var serverConfigLastPing:Long = 0L 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() val serverConnConfig = if (DeviceManager.isConnectedToServer) DeviceManager.serverConnectionConfig else DeviceManager.deviceData.getLastServerConnectionConfig()
if (!DeviceManager.isConnectedToServer || !apiHandler.isOnline() || serverConnConfig == null || serverConnConfig.id !== serverConfigIdUsed) { if (!DeviceManager.isConnectedToServer || !apiHandler.isOnline() || serverConnConfig == null || serverConnConfig.id !== serverConfigIdUsed) {
serverPodcastEpisodes = listOf() podcastEpisodeLibraryItemMap = mutableMapOf()
serverLibraryCategories = listOf() serverLibraryCategories = listOf()
serverLibraries = listOf() serverLibraries = listOf()
serverLibraryItems = mutableListOf() serverLibraryItems = mutableListOf()
@@ -80,13 +76,13 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
} }
} }
fun loadLibraryCategories(libraryId:String, cb: (List<LibraryCategory>) -> Unit) { fun loadItemsInProgressForAllLibraries(cb: (List<ItemInProgress>) -> Unit) {
if (serverLibraryCategories.isNotEmpty()) { if (serverItemsInProgress.isNotEmpty()) {
cb(serverLibraryCategories) cb(serverItemsInProgress)
} else { } else {
apiHandler.getLibraryCategories(libraryId) { apiHandler.getAllItemsInProgress { itemsInProgress ->
serverLibraryCategories = it serverItemsInProgress = itemsInProgress
cb(it) cb(serverItemsInProgress)
} }
} }
} }
@@ -129,44 +125,39 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
loadLibraryItem(libraryItemId) { libraryItemWrapper -> loadLibraryItem(libraryItemId) { libraryItemWrapper ->
Log.d(tag, "Loaded Podcast library item $libraryItemWrapper") Log.d(tag, "Loaded Podcast library item $libraryItemWrapper")
selectedLibraryItemWrapper = libraryItemWrapper
libraryItemWrapper?.let { libraryItemWrapper?.let {
if (libraryItemWrapper is LocalLibraryItem) { // Local podcast episodes if (libraryItemWrapper is LocalLibraryItem) { // Local podcast episodes
if (libraryItemWrapper.mediaType != "podcast" || libraryItemWrapper.media.getAudioTracks().isEmpty()) { if (libraryItemWrapper.mediaType != "podcast" || libraryItemWrapper.media.getAudioTracks().isEmpty()) {
serverPodcastEpisodes = listOf()
cb(mutableListOf()) cb(mutableListOf())
} else { } else {
val podcast = libraryItemWrapper.media as Podcast val podcast = libraryItemWrapper.media as Podcast
serverPodcastEpisodes = podcast.episodes ?: listOf()
selectedLibraryItemId = libraryItemWrapper.id selectedLibraryItemId = libraryItemWrapper.id
selectedPodcast = podcast selectedPodcast = podcast
val children = podcast.episodes?.map { podcastEpisode -> val children = podcast.episodes?.map { podcastEpisode ->
Log.d(tag, "Local Podcast Episode ${podcastEpisode.title} | ${podcastEpisode.id}") 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 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) MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
} }
children?.let { cb(children as MutableList) } ?: cb(mutableListOf()) children?.let { cb(children as MutableList) } ?: cb(mutableListOf())
} }
} else if (libraryItemWrapper is LibraryItem) { // Server podcast episodes } else if (libraryItemWrapper is LibraryItem) { // Server podcast episodes
if (libraryItemWrapper.mediaType != "podcast" || libraryItemWrapper.media.getAudioTracks().isEmpty()) { if (libraryItemWrapper.mediaType != "podcast" || libraryItemWrapper.media.getAudioTracks().isEmpty()) {
serverPodcastEpisodes = listOf()
cb(mutableListOf()) cb(mutableListOf())
} else { } else {
val podcast = libraryItemWrapper.media as Podcast val podcast = libraryItemWrapper.media as Podcast
serverPodcastEpisodes = podcast.episodes ?: listOf() podcast.episodes?.forEach { podcastEpisode ->
podcastEpisodeLibraryItemMap[podcastEpisode.id] = LibraryItemWithEpisode(libraryItemWrapper, podcastEpisode)
}
selectedLibraryItemId = libraryItemWrapper.id selectedLibraryItemId = libraryItemWrapper.id
selectedPodcast = podcast selectedPodcast = podcast
val children = podcast.episodes?.map { podcastEpisode -> val children = podcast.episodes?.map { podcastEpisode ->
val mediaMetadata = podcastEpisode.getMediaMetadata(libraryItemWrapper)
val progress = serverUserMediaProgress.find { it.libraryItemId == libraryItemWrapper.id && it.episodeId == podcastEpisode.id } 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) MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
} }
children?.let { cb(children as MutableList) } ?: cb(mutableListOf()) 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 loadAndroidAutoItems(cb: () -> Unit) {
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) {
Log.d(tag, "Load android auto items") 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 // Check if any valid server connection if not use locally downloaded books
checkSetValidServerConnectionConfig { isConnected -> checkSetValidServerConnectionConfig { isConnected ->
@@ -304,40 +277,29 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
loadLibraries { libraries -> loadLibraries { libraries ->
if (libraries.isEmpty()) { if (libraries.isEmpty()) {
Log.w(tag, "No libraries returned from server request") Log.w(tag, "No libraries returned from server request")
cb(cats) // Return download category only cb()
} else { } else {
val library = libraries[0] val library = libraries[0]
Log.d(tag, "Loading categories for library ${library.name} - ${library.id} - ${library.mediaType}") Log.d(tag, "Loading categories for library ${library.name} - ${library.id} - ${library.mediaType}")
loadLibraryCategories(library.id) { libraryCategories -> loadItemsInProgressForAllLibraries { itemsInProgress ->
itemsInProgress.forEach {
// Only using book or podcast library categories for now val libraryItem = it.libraryItemWrapper as LibraryItem
libraryCategories.forEach { if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
// 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)
}
}
} }
if (it.episode != null) {
// Log.d(tag, "Found library category ${it.label} with type ${it.type}") podcastEpisodeLibraryItemMap[it.episode.id] = LibraryItemWithEpisode(it.libraryItemWrapper, it.episode)
if (it.type == library.mediaType) {
// Log.d(tag, "Using library category ${it.id}")
cats.add(it)
} }
} }
cb(cats) cb() // Fully loaded
} }
} }
} }
} else { // Not connected/no internet sent downloaded cats only } else { // Not connected to server
cb(cats) cb()
} }
} }
} }
@@ -355,12 +317,7 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
if (id.startsWith("local")) { if (id.startsWith("local")) {
return DeviceManager.dbManager.getLocalLibraryItemWithEpisode(id) return DeviceManager.dbManager.getLocalLibraryItemWithEpisode(id)
} else { } else {
val podcastEpisode = serverPodcastEpisodes.find { it.id == id } return podcastEpisodeLibraryItemMap[id]
return if (podcastEpisode != null && selectedLibraryItemWrapper != null) {
LibraryItemWithEpisode(selectedLibraryItemWrapper!!, podcastEpisode)
} else {
null
}
} }
} }
@@ -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 { private fun levenshtein(lhs : CharSequence, rhs : CharSequence) : Int {
val lhsLength = lhs.length + 1 val lhsLength = lhs.length + 1
val rhsLength = rhs.length + 1 val rhsLength = rhs.length + 1
@@ -10,7 +10,7 @@ import com.audiobookshelf.app.data.*
class BrowseTree( class BrowseTree(
val context: Context, val context: Context,
libraryCategories: List<LibraryCategory>, itemsInProgress: List<ItemInProgress>,
libraries: List<Library> libraries: List<Library>
) { ) {
private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>() private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>()
@@ -21,8 +21,7 @@ class BrowseTree(
* @param drawableId - drawable res id * @param drawableId - drawable res id
* @return - uri * @return - uri
*/ */
fun getUriToDrawable(context: Context, fun getUriToDrawable(@AnyRes drawableId: Int): Uri {
@AnyRes drawableId: Int): Uri {
return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE
+ "://" + context.resources.getResourcePackageName(drawableId) + "://" + context.resources.getResourcePackageName(drawableId)
+ '/' + context.resources.getResourceTypeName(drawableId) + '/' + context.resources.getResourceTypeName(drawableId)
@@ -32,38 +31,26 @@ class BrowseTree(
init { init {
val rootList = mediaIdToChildren[AUTO_BROWSE_ROOT] ?: mutableListOf() 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_MEDIA_ID, CONTINUE_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Listening") 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() }.build()
val downloadsMetadata = MediaMetadataCompat.Builder().apply { val downloadsMetadata = MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, DOWNLOADS_ROOT) putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, DOWNLOADS_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Downloads") 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() }.build()
val librariesMetadata = MediaMetadataCompat.Builder().apply { val librariesMetadata = MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, LIBRARIES_ROOT) putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, LIBRARIES_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Libraries") 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() }.build()
// Server continue Listening cat if (!itemsInProgress.isEmpty()) {
libraryCategories.find { it.id == "continue-listening" }?.let { continueListeningCategory -> rootList += continueListeningMetadata
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 (libraries.isNotEmpty()) { if (libraries.isNotEmpty()) {
@@ -78,23 +65,6 @@ class BrowseTree(
} }
rootList += downloadsMetadata 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 mediaIdToChildren[AUTO_BROWSE_ROOT] = rootList
} }
@@ -795,8 +795,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 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 AUTO_MEDIA_ROOT = "/"
private val ALL_ROOT = "__ALL__"
private val LIBRARIES_ROOT = "__LIBRARIES__" private val LIBRARIES_ROOT = "__LIBRARIES__"
private val DOWNLOADS_ROOT = "__DOWNLOADS__"
private val CONTINUE_ROOT = "__CONTINUE__"
private lateinit var browseTree:BrowseTree private lateinit var browseTree:BrowseTree
@@ -839,8 +840,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
override fun onLoadChildren(parentMediaId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) { override fun onLoadChildren(parentMediaId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
Log.d(tag, "ON LOAD CHILDREN $parentMediaId") 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() result.detach()
if (parentMediaId.startsWith("li_") || parentMediaId.startsWith("local_")) { // Show podcast episodes if (parentMediaId.startsWith("li_") || parentMediaId.startsWith("local_")) { // Show podcast episodes
@@ -852,53 +851,67 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
mediaManager.loadLibraryItemsWithAudio(parentMediaId) { libraryItems -> mediaManager.loadLibraryItemsWithAudio(parentMediaId) { libraryItems ->
val children = libraryItems.map { libraryItem -> val children = libraryItems.map { libraryItem ->
val libraryItemMediaMetadata = libraryItem.getMediaMetadata()
if (libraryItem.mediaType == "podcast") { // Podcasts are browseable 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 { } else {
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItemMediaMetadata.description.mediaId } val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
val description = mediaManager.getMediaDescriptionFromMediaMetadata(libraryItemMediaMetadata, progress) val description = libraryItem.getMediaDescription(progress)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
} }
} }
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?) 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 localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
val localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast") val localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast")
val localBrowseItems:MutableList<MediaBrowserCompat.MediaItem> = mutableListOf() val localBrowseItems:MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
localBooks.forEach { localLibraryItem -> localBooks.forEach { localLibraryItem ->
val mediaMetadata = localLibraryItem.getMediaMetadata() val progress = DeviceManager.dbManager.getLocalMediaProgress(localLibraryItem.id)
val progress = DeviceManager.dbManager.getLocalMediaProgress(mediaMetadata.description.mediaId ?: "") val description = localLibraryItem.getMediaDescription(progress)
val description = mediaManager.getMediaDescriptionFromMediaMetadata(mediaMetadata, progress)
localBrowseItems += MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) localBrowseItems += MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
} }
localPodcasts.forEach { localLibraryItem -> localPodcasts.forEach { localLibraryItem ->
val mediaMetadata = localLibraryItem.getMediaMetadata() val mediaDescription = localLibraryItem.getMediaDescription(null)
localBrowseItems += MediaBrowserCompat.MediaItem(mediaMetadata.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) localBrowseItems += MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
} }
result.sendResult(localBrowseItems) result.sendResult(localBrowseItems)
} else { // Load categories } else if (parentMediaId == CONTINUE_ROOT) {
mediaManager.loadAndroidAutoItems { libraryCategories -> val localBrowseItems:MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
browseTree = BrowseTree(this, libraryCategories, mediaManager.serverLibraries) 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 -> val children = browseTree[parentMediaId]?.map { item ->
Log.d(tag, "Loading Browser Media Item ${item.description.title} $flag") Log.d(tag, "Loading Browser Media Item ${item.description.title}")
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
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)
}
} }
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?) result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
} }
@@ -907,10 +920,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
override fun onSearch(query: String, extras: Bundle?, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) { override fun onSearch(query: String, extras: Bundle?, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
result.detach() result.detach()
mediaManager.loadAndroidAutoItems() { libraryCategories -> mediaManager.loadAndroidAutoItems {
browseTree = BrowseTree(this, libraryCategories, mediaManager.serverLibraries) browseTree = BrowseTree(this, mediaManager.serverItemsInProgress, mediaManager.serverLibraries)
val children = browseTree[ALL_ROOT]?.map { item -> val children = browseTree[LIBRARIES_ROOT]?.map { item ->
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
} }
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?) result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
} }
@@ -189,21 +189,16 @@ class ApiHandler(var ctx:Context) {
} }
} }
fun getLibraryCategories(libraryId:String, cb: (List<LibraryCategory>) -> Unit) { fun getAllItemsInProgress(cb: (List<ItemInProgress>) -> Unit) {
getRequest("/api/libraries/$libraryId/personalized", null, null) { getRequest("/api/me/items-in-progress", null, null) {
val items = mutableListOf<LibraryCategory>() val items = mutableListOf<ItemInProgress>()
if (it.has("value")) { if (it.has("libraryItems")) {
val array = it.getJSONArray("value") val array = it.getJSONArray("libraryItems")
for (i in 0 until array.length()) { for (i in 0 until array.length()) {
val jsobj = array.get(i) as JSONObject val jsobj = array.get(i) as JSONObject
val type = jsobj.get("type").toString() val itemInProgress = ItemInProgress.makeFromServerObject(jsobj)
// Only support for podcast and book in android auto items.add(itemInProgress)
if (type == "podcast" || type == "book") {
jsobj.put("isLocal", false)
val item = jacksonMapper.readValue<LibraryCategory>(jsobj.toString())
items.add(item)
}
} }
} }
cb(items) cb(items)