From bba388aefa5091536a8f4dd6ce59a661ae43ac13 Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 24 Aug 2022 17:55:07 -0500 Subject: [PATCH] Update:Android auto show in progress media items across all libraries #285 --- .../audiobookshelf/app/data/DataClasses.kt | 125 +++++++---------- .../audiobookshelf/app/data/DeviceClasses.kt | 6 +- .../audiobookshelf/app/data/ItemInProgress.kt | 34 +++++ .../audiobookshelf/app/data/LibraryItem.kt | 90 ++++++++++++ .../app/data/LocalLibraryItem.kt | 48 +++++-- .../audiobookshelf/app/media/MediaManager.kt | 132 ++++-------------- .../audiobookshelf/app/player/BrowseTree.kt | 46 ++---- .../app/player/PlayerNotificationService.kt | 73 ++++++---- .../audiobookshelf/app/server/ApiHandler.kt | 19 +-- 9 files changed, 295 insertions(+), 278 deletions(-) create mode 100644 android/app/src/main/java/com/audiobookshelf/app/data/ItemInProgress.kt create mode 100644 android/app/src/main/java/com/audiobookshelf/app/data/LibraryItem.kt diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt b/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt index 9e4997f5..807457e6 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt @@ -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?, - 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() } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt b/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt index b81c67da..ce13089d 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt @@ -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( diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/ItemInProgress.kt b/android/app/src/main/java/com/audiobookshelf/app/data/ItemInProgress.kt new file mode 100644 index 00000000..48dd51fb --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/data/ItemInProgress.kt @@ -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(serverItem.toString()) + + var episode:PodcastEpisode? = null + if (serverItem.has("recentEpisode")) { + episode = jacksonMapper.readValue(serverItem.get("recentEpisode").toString()) + } + + val progressLastUpdate:Long = serverItem.getLong("progressLastUpdate") + return ItemInProgress(libraryItem, episode, progressLastUpdate, false) + } + } +} diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/LibraryItem.kt b/android/app/src/main/java/com/audiobookshelf/app/data/LibraryItem.kt new file mode 100644 index 00000000..4166a6e3 --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/data/LibraryItem.kt @@ -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?, + 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() + } +} diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt b/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt index c1b2e2ce..390cfd14 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt @@ -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() } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt b/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt index 0ef698bb..3199e8e8 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt @@ -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() var selectedLibraryId = "" - var selectedLibraryItemWrapper:LibraryItemWrapper? = null var selectedPodcast:Podcast? = null var selectedLibraryItemId:String? = null - var serverPodcastEpisodes = listOf() + var podcastEpisodeLibraryItemMap = mutableMapOf() var serverLibraryCategories = listOf() + var serverItemsInProgress = listOf() var serverLibraries = listOf() 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) -> Unit) { - if (serverLibraryCategories.isNotEmpty()) { - cb(serverLibraryCategories) + fun loadItemsInProgressForAllLibraries(cb: (List) -> 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 { - val localBooks = DeviceManager.dbManager.getLocalLibraryItems("book") - val localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast") - val cats = mutableListOf() - 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) -> Unit) { + fun loadAndroidAutoItems(cb: () -> Unit) { Log.d(tag, "Load android auto items") - val cats = mutableListOf() - - 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 diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/BrowseTree.kt b/android/app/src/main/java/com/audiobookshelf/app/player/BrowseTree.kt index cf519f8e..099996c3 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/BrowseTree.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/BrowseTree.kt @@ -10,7 +10,7 @@ import com.audiobookshelf.app.data.* class BrowseTree( val context: Context, - libraryCategories: List, + itemsInProgress: List, libraries: List ) { private val mediaIdToChildren = mutableMapOf>() @@ -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 } diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt index 92a0cdde..0ad77416 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt @@ -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 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 @@ -839,8 +840,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { override fun onLoadChildren(parentMediaId: String, result: Result>) { 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 @@ -852,53 +851,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?) } - } 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 = 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 = 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?) } @@ -907,10 +920,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { override fun onSearch(query: String, extras: Bundle?, result: Result>) { 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?) } diff --git a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt index 3893b558..bbf3a0ab 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt @@ -189,21 +189,16 @@ class ApiHandler(var ctx:Context) { } } - fun getLibraryCategories(libraryId:String, cb: (List) -> Unit) { - getRequest("/api/libraries/$libraryId/personalized", null, null) { - val items = mutableListOf() - if (it.has("value")) { - val array = it.getJSONArray("value") + fun getAllItemsInProgress(cb: (List) -> Unit) { + getRequest("/api/me/items-in-progress", null, null) { + val items = mutableListOf() + 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(jsobj.toString()) - items.add(item) - } + val itemInProgress = ItemInProgress.makeFromServerObject(jsobj) + items.add(itemInProgress) } } cb(items)