Added better library browsing for Android Auto

Each library has 3 options: Library, Series and Collection. Library is grouped by authors
This commit is contained in:
ISO-B 2024-09-13 22:51:54 +03:00
parent 2b1e53b371
commit a3a58a25ef
13 changed files with 785 additions and 43 deletions

View file

@ -0,0 +1,36 @@
package com.audiobookshelf.app.data
import android.content.Context
import android.os.Bundle
import android.support.v4.media.MediaDescriptionCompat
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
class CollapsedSeries(
id:String,
var libraryId:String?,
var name:String,
//var nameIgnorePrefix:String,
var sequence:String?,
var libraryItemIds:MutableList<String>
) : LibraryItemWrapper(id) {
@get:JsonIgnore
val title get() = name
@get:JsonIgnore
val numBooks get() = libraryItemIds.size
@JsonIgnore
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
val extras = Bundle()
val mediaId = "__LIBRARY__${libraryId}__SERIE__${id}"
return MediaDescriptionCompat.Builder()
.setMediaId(mediaId)
.setTitle(title)
//.setIconUri(getCoverUri())
.setSubtitle("${numBooks} books")
.setExtras(extras)
.build()
}
}

View file

@ -0,0 +1,55 @@
package com.audiobookshelf.app.data
import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.support.v4.media.MediaDescriptionCompat
import com.audiobookshelf.app.BuildConfig
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 LibraryAuthorItem(
id:String,
var libraryId:String,
var name:String,
var lastFirst:String,
var description:String?,
var imagePath:String?,
var addedAt:Long,
var updatedAt:Long,
var numBooks:Int?,
var libraryItems:MutableList<LibraryItem>?,
var series:MutableList<LibrarySeriesItem>?
) : LibraryItemWrapper(id) {
@get:JsonIgnore
val title get() = name
@get:JsonIgnore
val bookCount get() = if (numBooks != null) numBooks else libraryItems!!.size
@JsonIgnore
fun getPortraitUri(): Uri {
if (imagePath == null) {
return Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.md_account_outline)
}
return Uri.parse("${DeviceManager.serverAddress}/api/authors/$id/image?token=${DeviceManager.token}")
}
@JsonIgnore
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
val extras = Bundle()
val mediaId = "__LIBRARY__${libraryId}__AUTHOR__${id}"
return MediaDescriptionCompat.Builder()
.setMediaId(mediaId)
.setTitle(title)
.setIconUri(getPortraitUri())
.setSubtitle("${bookCount} books")
.setExtras(extras)
.build()
}
}

View file

@ -0,0 +1,40 @@
package com.audiobookshelf.app.data
import android.content.Context
import android.os.Bundle
import android.support.v4.media.MediaDescriptionCompat
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
class LibraryCollection(
id:String,
var libraryId:String,
var name:String,
//var userId:String?,
var description:String?,
var books:MutableList<LibraryItem>?,
) : LibraryItemWrapper(id) {
@get:JsonIgnore
val title get() = name
@get:JsonIgnore
val bookCount get() = if (books != null) books!!.size else 0
@get:JsonIgnore
val audiobookCount get() = books?.filter { book -> (book.media as Book).getAudioTracks().isNotEmpty() }?.size ?: 0
@JsonIgnore
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
val extras = Bundle()
val mediaId = "__LIBRARY__${libraryId}__COLLECTION__${id}"
return MediaDescriptionCompat.Builder()
.setMediaId(mediaId)
.setTitle(title)
//.setIconUri(getCoverUri())
.setSubtitle("${bookCount} books")
.setExtras(extras)
.build()
}
}

View file

@ -32,10 +32,17 @@ class LibraryItem(
var media:MediaType,
var libraryFiles:MutableList<LibraryFile>?,
var userMediaProgress:MediaProgress?, // Only included when requesting library item with progress (for downloads)
var collapsedSeries: CollapsedSeries?,
var localLibraryItemId:String? // For Android Auto
) : LibraryItemWrapper(id) {
@get:JsonIgnore
val title get() = media.metadata.title
val title: String
get() {
if (collapsedSeries != null) {
return collapsedSeries!!.title
}
return media.metadata.title
}
@get:JsonIgnore
val authorName get() = media.metadata.getAuthorDisplayName()
@ -58,49 +65,76 @@ class LibraryItem(
}
@JsonIgnore
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context, authorId: String?): MediaDescriptionCompat {
val extras = Bundle()
if (localLibraryItemId != null) {
extras.putLong(
MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
MediaDescriptionCompat.STATUS_DOWNLOADED
)
}
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
if (collapsedSeries == null) {
if (localLibraryItemId != null) {
extras.putLong(
MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
MediaDescriptionCompat.STATUS_DOWNLOADED
)
}
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
)
}
if (media.metadata.explicit) {
extras.putLong(
MediaConstants.METADATA_KEY_IS_EXPLICIT,
MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
)
}
} else if (mediaType != "podcast") {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
)
}
if (media.metadata.explicit) {
extras.putLong(MediaConstants.METADATA_KEY_IS_EXPLICIT, MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT)
val mediaId = if (localLibraryItemId != null) {
localLibraryItemId
} else if (collapsedSeries != null) {
if (authorId != null) {
"__LIBRARY__${libraryId}__AUTHOR_SERIES__${authorId}__${collapsedSeries!!.id}"
} else {
"__LIBRARY__${libraryId}__SERIES__${collapsedSeries!!.id}"
}
} else {
id
}
var subtitle = authorName
if (collapsedSeries != null) {
subtitle = "${collapsedSeries!!.numBooks} books"
}
val mediaId = localLibraryItemId ?: id
return MediaDescriptionCompat.Builder()
.setMediaId(mediaId)
.setTitle(title)
.setIconUri(getCoverUri())
.setSubtitle(authorName)
.setSubtitle(subtitle)
.setExtras(extras)
.build()
}
@JsonIgnore
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
/*
This is needed so Android auto library hierarchy for author series can be implemented
*/
return getMediaDescription(progress, ctx, null)
}
}

View file

@ -0,0 +1,51 @@
package com.audiobookshelf.app.data
import android.content.Context
import android.os.Bundle
import android.support.v4.media.MediaDescriptionCompat
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
class LibrarySeriesItem(
id:String,
var libraryId:String,
var name:String,
var description:String?,
var addedAt:Long,
var updatedAt:Long,
var books:MutableList<LibraryItem>?,
var localLibraryItemId:String? // For Android Auto
) : LibraryItemWrapper(id) {
@get:JsonIgnore
val title get() = name
@get:JsonIgnore
val audiobookCount: Int
get() {
if (books == null) return 0
val booksWithAudio = books?.filter { b -> (b.media as Book).numTracks != 0 }
return booksWithAudio?.size ?: 0
}
@JsonIgnore
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
val extras = Bundle()
if (localLibraryItemId != null) {
extras.putLong(
MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
MediaDescriptionCompat.STATUS_DOWNLOADED
)
}
val mediaId = "__LIBRARY__${libraryId}__SERIES__${id}"
return MediaDescriptionCompat.Builder()
.setMediaId(mediaId)
.setTitle(title)
//.setIconUri(getCoverUri())
.setSubtitle("$audiobookCount books")
.setExtras(extras)
.build()
}
}

View file

@ -23,6 +23,13 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
private var selectedLibraryItems = mutableListOf<LibraryItem>()
private var selectedLibraryId = ""
private var cachedLibraryAuthors : MutableMap<String, MutableMap<String, LibraryAuthorItem>> = hashMapOf()
private var cachedLibraryAuthorItems : MutableMap<String, MutableMap<String, List<LibraryItem>>> = hashMapOf()
private var cachedLibraryAuthorSeriesItems : MutableMap<String, MutableMap<String, List<LibraryItem>>> = hashMapOf()
private var cachedLibrarySeries : MutableMap<String, List<LibrarySeriesItem>> = hashMapOf()
private var cachedLibrarySeriesItem : MutableMap<String, MutableMap<String, List<LibraryItem>>> = hashMapOf()
private var cachedLibraryCollections : MutableMap<String, MutableMap<String, LibraryCollection>> = hashMapOf()
private var selectedPodcast:Podcast? = null
private var selectedLibraryItemId:String? = null
private var podcastEpisodeLibraryItemMap = mutableMapOf<String, LibraryItemWithEpisode>()
@ -142,6 +149,229 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
}
}
/**
* Returns series with audio books from selected library.
* If data is not found from local cache then it will be fetched from server
*/
fun loadLibrarySeriesWithAudio(libraryId:String, cb: (List<LibrarySeriesItem>) -> Unit) {
// Check "cache" first
if (cachedLibrarySeries.containsKey(libraryId)) {
Log.d(tag, "Series with audio found from cache | Library $libraryId ")
cb(cachedLibrarySeries[libraryId] as List<LibrarySeriesItem>)
} else {
apiHandler.getLibrarySeries(libraryId) { seriesItems ->
Log.d(tag, "Series with audio loaded from server | Library $libraryId")
val seriesItemsWithAudio = seriesItems.filter { si -> si.audiobookCount > 0 }
cachedLibrarySeries[libraryId] = seriesItemsWithAudio
cb(seriesItemsWithAudio)
}
}
}
/**
* Returns series with audiobooks from selected library using filter for paging.
* If data is not found from local cache then it will be fetched from server
*/
fun loadLibrarySeriesWithAudio(libraryId:String, seriesFilter:String, cb: (List<LibrarySeriesItem>) -> Unit) {
// Check "cache" first
if (!cachedLibrarySeries.containsKey(libraryId)) {
loadLibrarySeriesWithAudio(libraryId) {}
} else {
Log.d(tag, "Series with audio found from cache | Library $libraryId ")
}
val seriesWithBooks = cachedLibrarySeries[libraryId]!!.filter { ls -> ls.title.uppercase().startsWith(seriesFilter) }.toList()
cb(seriesWithBooks)
}
/**
* Returns books for series from library.
* If data is not found from local cache then it will be fetched from server
*/
fun loadLibrarySeriesItemsWithAudio(libraryId:String, seriesId:String, cb: (List<LibraryItem>) -> Unit) {
// Check "cache" first
if (!cachedLibrarySeriesItem.containsKey(libraryId)) {
cachedLibrarySeriesItem[libraryId] = hashMapOf()
}
if (cachedLibrarySeriesItem[libraryId]!!.containsKey(seriesId)) {
Log.d(tag, "Items for series $seriesId found from cache | Library $libraryId")
cachedLibrarySeriesItem[libraryId]!![seriesId]?.let { cb(it) }
} else {
apiHandler.getLibrarySeriesItems(libraryId, seriesId) { libraryItems ->
Log.d(tag, "Items for series $seriesId loaded from server | Library $libraryId")
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
cachedLibrarySeriesItem[libraryId]!![seriesId] = libraryItemsWithAudio
libraryItemsWithAudio.forEach { libraryItem ->
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
}
cb(libraryItemsWithAudio)
}
}
}
/**
* Returns authors with books from library.
* If data is not found from local cache then it will be fetched from server
*/
fun loadAuthorsWithBooks(libraryId:String, cb: (List<LibraryAuthorItem>) -> Unit) {
// Check "cache" first
if (cachedLibraryAuthors.containsKey(libraryId)) {
Log.d(tag, "Authors with books found from cache | Library $libraryId ")
cb(cachedLibraryAuthors[libraryId]!!.values.toList())
} else {
// Fetch data from server and add it to local "cache"
apiHandler.getLibraryAuthors(libraryId) { authorItems ->
Log.d(tag, "Authors with books loaded from server | Library $libraryId ")
// TO-DO: This check won't ensure that there is audiobooks. Current API won't offer ability to do so
val authorItemsWithBooks = authorItems.filter { li -> li.bookCount != null && li.bookCount!! > 0 }
// Ensure that there is map for library
cachedLibraryAuthors[libraryId] = mutableMapOf()
// Cache authors
authorItemsWithBooks.forEach {
if (!cachedLibraryAuthors[libraryId]!!.containsKey(it.id)) {
cachedLibraryAuthors[libraryId]!![it.id] = it
}
}
cb(authorItemsWithBooks)
}
}
}
/**
* Returns authors with books from selected library using filter for paging.
* If data is not found from local cache then it will be fetched from server
*/
fun loadAuthorsWithBooks(libraryId:String, authorFilter: String, cb: (List<LibraryAuthorItem>) -> Unit) {
// Check "cache" first
if (cachedLibraryAuthors.containsKey(libraryId)) {
Log.d(tag, "Authors with books found from cache | Library $libraryId ")
} else {
loadAuthorsWithBooks(libraryId) {}
}
val authorsWithBooks = cachedLibraryAuthors[libraryId]!!.values.filter { lai -> lai.name.uppercase().startsWith(authorFilter) }.toList()
cb(authorsWithBooks)
}
/**
* Returns audiobooks for author from library
* If data is not found from local cache then it will be fetched from server
*/
fun loadAuthorBooksWithAudio(libraryId:String, authorId:String, cb: (List<LibraryItem>) -> Unit) {
// Ensure that there is map for library
if (!cachedLibraryAuthorItems.containsKey(libraryId)) {
cachedLibraryAuthorItems[libraryId] = mutableMapOf()
}
// Check "cache" first
if (cachedLibraryAuthorItems[libraryId]!!.containsKey(authorId)) {
Log.d(tag, "Items for author $authorId found from cache | Library $libraryId")
cachedLibraryAuthorItems[libraryId]!![authorId]?.let { cb(it) }
} else {
apiHandler.getLibraryItemsFromAuthor(libraryId, authorId) { libraryItems ->
Log.d(tag, "Items for author $authorId loaded from server | Library $libraryId")
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
cachedLibraryAuthorItems[libraryId]!![authorId] = libraryItemsWithAudio
libraryItemsWithAudio.forEach { libraryItem ->
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
}
cb(libraryItemsWithAudio)
}
}
}
/**
* Returns audiobooks for author from specified series within library
* If data is not found from local cache then it will be fetched from server
*/
fun loadAuthorSeriesBooksWithAudio(libraryId:String, authorId:String, seriesId: String, cb: (List<LibraryItem>) -> Unit) {
val authorSeriesKey = "$authorId|$seriesId"
// Ensure that there is map for library
if (!cachedLibraryAuthorSeriesItems.containsKey(libraryId)) {
cachedLibraryAuthorSeriesItems[libraryId] = mutableMapOf()
}
// Check "cache" first
if (cachedLibraryAuthorSeriesItems[libraryId]!!.containsKey(authorSeriesKey)) {
Log.d(tag, "Items for series $seriesId with author $authorId found from cache | Library $libraryId")
cachedLibraryAuthorSeriesItems[libraryId]!![authorSeriesKey]?.let { cb(it) }
} else {
apiHandler.getLibrarySeriesItems(libraryId, seriesId) { libraryItems ->
Log.d(tag, "Items for series $seriesId with author $authorId loaded from server | Library $libraryId")
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
if (!cachedLibraryAuthors[libraryId]!!.containsKey(authorId)) {
Log.d(tag, "Author data is missing")
}
val authorName = cachedLibraryAuthors[libraryId]!![authorId]?.name ?: ""
Log.d(tag, "Using author name: $authorName")
val libraryItemsFromAuthorWithAudio = libraryItemsWithAudio.filter { li -> li.authorName.indexOf(authorName, ignoreCase = true) >= 0 }
cachedLibraryAuthorSeriesItems[libraryId]!![authorId] = libraryItemsFromAuthorWithAudio
libraryItemsFromAuthorWithAudio.forEach { libraryItem ->
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
}
cb(libraryItemsFromAuthorWithAudio)
}
}
}
/**
* Returns collections with audiobooks from library
* If data is not found from local cache then it will be fetched from server
*/
fun loadLibraryCollectionsWithAudio(libraryId:String, cb: (List<LibraryCollection>) -> Unit) {
if (cachedLibraryCollections.containsKey(libraryId)) {
Log.d(tag, "Collections with books found from cache | Library $libraryId ")
cb(cachedLibraryCollections[libraryId]!!.values.toList())
} else {
apiHandler.getLibraryCollections(libraryId) { libraryCollections ->
Log.d(tag, "Collections with books loaded from server | Library $libraryId ")
val libraryCollectionsWithAudio = libraryCollections.filter { lc -> lc.audiobookCount > 0 }
// Cache collections
cachedLibraryCollections[libraryId] = hashMapOf()
libraryCollectionsWithAudio.forEach {
if (!cachedLibraryCollections[libraryId]!!.containsKey(it.id)) {
cachedLibraryCollections[libraryId]!![it.id] = it
}
}
cb(libraryCollectionsWithAudio)
}
}
}
/**
* Returns audiobooks for collection from library
* If data is not found from local cache then it will be fetched from server
*/
fun loadLibraryCollectionBooksWithAudio(libraryId: String, collectionId: String, cb: (List<LibraryItem>) -> Unit) {
if (!cachedLibraryCollections.containsKey(libraryId)) {
loadLibraryCollectionsWithAudio(libraryId) {}
}
Log.d(tag, "Trying to find collection $collectionId items from from cache | Library $libraryId ")
if ( cachedLibraryCollections[libraryId]!!.containsKey(collectionId)) {
val libraryCollectionBookswithAudio = cachedLibraryCollections[libraryId]!![collectionId]?.books
libraryCollectionBookswithAudio?.forEach { libraryItem ->
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
}
cb(libraryCollectionBookswithAudio as List<LibraryItem>)
}
}
private fun loadLibraryItem(libraryItemId:String, cb: (LibraryItemWrapper?) -> Unit) {
if (libraryItemId.startsWith("local")) {
cb(DeviceManager.dbManager.getLocalLibraryItem(libraryItemId))

View file

@ -1029,29 +1029,35 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
val localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
val localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast")
val localBrowseItems:MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
val localBrowseItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
localBooks.forEach { localLibraryItem ->
if (localLibraryItem.media.getAudioTracks().isNotEmpty()) {
val progress = DeviceManager.dbManager.getLocalMediaProgress(localLibraryItem.id)
val description = localLibraryItem.getMediaDescription(progress, ctx)
localBrowseItems += MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
localBrowseItems += MediaBrowserCompat.MediaItem(
description,
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
)
}
}
localPodcasts.forEach { localLibraryItem ->
val mediaDescription = localLibraryItem.getMediaDescription(null, ctx)
localBrowseItems += MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
localBrowseItems += MediaBrowserCompat.MediaItem(
mediaDescription,
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
}
result.sendResult(localBrowseItems)
} else if (parentMediaId == CONTINUE_ROOT) {
val localBrowseItems:MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
val localBrowseItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
mediaManager.serverItemsInProgress.forEach { itemInProgress ->
val progress: MediaProgressWrapper?
val mediaDescription:MediaDescriptionCompat
val mediaDescription: MediaDescriptionCompat
if (itemInProgress.episode != null) {
if (itemInProgress.isLocal) {
progress = DeviceManager.dbManager.getLocalMediaProgress("${itemInProgress.libraryItemWrapper.id}-${itemInProgress.episode.id}")
@ -1093,20 +1099,224 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
}
} else if (mediaManager.getIsLibrary(parentMediaId)) { // Load library items for library
Log.d(tag, "Loading items for library $parentMediaId")
mediaManager.loadLibraryItemsWithAudio(parentMediaId) { libraryItems ->
val children = libraryItems.map { libraryItem ->
if (libraryItem.mediaType == "podcast") { // Podcasts are browseable
val mediaDescription = libraryItem.getMediaDescription(null, ctx)
MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
val children = mutableListOf(
MediaBrowserCompat.MediaItem(
MediaDescriptionCompat.Builder()
.setTitle("Library")
.setMediaId("__LIBRARY__${parentMediaId}__AUTHORS")
.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
),
MediaBrowserCompat.MediaItem(
MediaDescriptionCompat.Builder()
.setTitle("Series")
.setMediaId("__LIBRARY__${parentMediaId}__SERIES_LIST")
.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
),
MediaBrowserCompat.MediaItem(
MediaDescriptionCompat.Builder()
.setTitle("Collections")
.setMediaId("__LIBRARY__${parentMediaId}__COLLECTIONS")
.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
)
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
} else if (parentMediaId.startsWith("__LIBRARY__")) {
Log.d(tag, "Browsing library $parentMediaId")
val mediaIdParts = parentMediaId.split("__")
/*
MediaIdParts for Library
1: LIBRARY
2: mediaId for library
3: Browsing style (AUTHORS, AUTHOR, AUTHOR_SERIES, SERIES_LIST, SERIES, COLLECTION, COLLECTIONS)
4:
- Paging: SERIES_LIST, AUTHORS
- SeriesId: SERIES
- AuthorId: AUTHOR, AUTHOR_SERIES
- CollectionId: COLLECTIONS
5: SeriesId: AUTHOR_SERIES
*/
if (!mediaManager.getIsLibrary(mediaIdParts[2])) {
Log.d(tag, "${mediaIdParts[2]} is not library")
result.sendResult(null)
return
}
Log.d(tag, "$mediaIdParts")
if (mediaIdParts[3] == "SERIES_LIST" && mediaIdParts.size == 5) {
Log.d(tag, "Loading series from library ${mediaIdParts[2]} with paging ${mediaIdParts[4]}")
mediaManager.loadLibrarySeriesWithAudio(mediaIdParts[2], mediaIdParts[4]) { seriesItems ->
Log.d(tag, "Received ${seriesItems.size} series")
if (seriesItems.size > 500) {
val seriesLetters = seriesItems.groupingBy { iwb -> iwb.title.substring(0, mediaIdParts[4].length + 1).uppercase() }.eachCount()
val children = seriesLetters.map { (seriesLetter, seriesCount) ->
MediaBrowserCompat.MediaItem(
MediaDescriptionCompat.Builder()
.setTitle(seriesLetter)
.setMediaId("${parentMediaId}${seriesLetter.last()}")
.setSubtitle("$seriesCount series")
.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
} else {
val children = seriesItems.map { seriesItem ->
val description = seriesItem.getMediaDescription(null, ctx)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
}
}else if (mediaIdParts[3] == "SERIES_LIST") {
Log.d(tag, "Loading series from library ${mediaIdParts[2]}")
mediaManager.loadLibrarySeriesWithAudio(mediaIdParts[2]) { seriesItems ->
Log.d(tag, "Received ${seriesItems.size} series")
if (seriesItems.size > 1000) {
val seriesLetters = seriesItems.groupingBy { iwb -> iwb.title.first().uppercaseChar() }.eachCount()
val children = seriesLetters.map { (seriesLetter, seriesCount) ->
MediaBrowserCompat.MediaItem(
MediaDescriptionCompat.Builder()
.setTitle(seriesLetter.toString())
.setSubtitle("$seriesCount series")
.setMediaId("${parentMediaId}__${seriesLetter}")
.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
} else {
val children = seriesItems.map { seriesItem ->
val description = seriesItem.getMediaDescription(null, ctx)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
}
} else if (mediaIdParts[3] == "SERIES") {
Log.d(tag, "Loading items for serie ${mediaIdParts[4]} from library ${mediaIdParts[2]}")
mediaManager.loadLibrarySeriesItemsWithAudio(
mediaIdParts[2],
mediaIdParts[4]
) { libraryItems ->
Log.d(tag, "Received ${libraryItems.size} library items")
val children = libraryItems.map { libraryItem ->
val progress =
mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
libraryItem.localLibraryItemId = localLibraryItem?.id
val description = libraryItem.getMediaDescription(progress, ctx)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
} else if (mediaIdParts[3] == "AUTHORS" && mediaIdParts.size == 5) {
Log.d(tag, "Loading authors from library ${mediaIdParts[2]} with paging ${mediaIdParts[4]}")
mediaManager.loadAuthorsWithBooks(mediaIdParts[2], mediaIdParts[4]) { authorItems ->
Log.d(tag, "Received ${authorItems.size} authors")
if (authorItems.size > 100) {
val authorLetters = authorItems.groupingBy { iwb -> iwb.name.substring(0, mediaIdParts[4].length + 1).uppercase() }.eachCount()
val children = authorLetters.map { (authorLetter, authorCount) ->
MediaBrowserCompat.MediaItem(
MediaDescriptionCompat.Builder()
.setTitle(authorLetter)
.setMediaId("${parentMediaId}${authorLetter.last()}")
.setSubtitle("$authorCount authors")
.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
} else {
val children = authorItems.map { authorItem ->
val description = authorItem.getMediaDescription(null, ctx)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
}
} else if (mediaIdParts[3] == "AUTHORS") {
Log.d(tag, "Loading authors from library ${mediaIdParts[2]}")
mediaManager.loadAuthorsWithBooks(mediaIdParts[2]) { authorItems ->
Log.d(tag, "Received ${authorItems.size} authors")
if (authorItems.size > 1000) {
val authorLetters = authorItems.groupingBy { iwb -> iwb.name.first().uppercaseChar() }.eachCount()
val children = authorLetters.map { (authorLetter, authorCount) ->
MediaBrowserCompat.MediaItem(
MediaDescriptionCompat.Builder()
.setTitle(authorLetter.toString())
.setSubtitle("$authorCount authors")
.setMediaId("${parentMediaId}__${authorLetter}")
.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
} else {
val children = authorItems.map { authorItem ->
val description = authorItem.getMediaDescription(null, ctx)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
}
} else if (mediaIdParts[3] == "AUTHOR") {
mediaManager.loadAuthorBooksWithAudio(mediaIdParts[2], mediaIdParts[4]) { libraryItems ->
val children = libraryItems.map { libraryItem ->
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
libraryItem.localLibraryItemId = localLibraryItem?.id
if (libraryItem.collapsedSeries != null) {
val description = libraryItem.getMediaDescription(progress, ctx, mediaIdParts[4])
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
} else {
val description = libraryItem.getMediaDescription(progress, ctx)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
}
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
} else if (mediaIdParts[3] == "AUTHOR_SERIES") {
mediaManager.loadAuthorSeriesBooksWithAudio(mediaIdParts[2], mediaIdParts[4], mediaIdParts[5]) { libraryItems ->
val children = libraryItems.map { libraryItem ->
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
libraryItem.localLibraryItemId = localLibraryItem?.id
val description = libraryItem.getMediaDescription(progress, ctx)
if (libraryItem.collapsedSeries != null) {
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
} else {
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
}
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
} else if (mediaIdParts[3] == "COLLECTIONS") {
Log.d(tag, "Loading collections from library ${mediaIdParts[2]}")
mediaManager.loadLibraryCollectionsWithAudio(mediaIdParts[2]) { collectionItems ->
Log.d(tag, "Received ${collectionItems.size} collections")
val children = collectionItems.map { collectionItem ->
val description = collectionItem.getMediaDescription(null, ctx)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
} else if (mediaIdParts[3] == "COLLECTION") {
Log.d(tag, "Loading collection ${mediaIdParts[4]} books from library ${mediaIdParts[2]}")
mediaManager.loadLibraryCollectionBooksWithAudio(mediaIdParts[2], mediaIdParts[4]) { libraryItems ->
Log.d(tag, "Received ${libraryItems.size} collections")
val children = libraryItems.map { libraryItem ->
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
libraryItem.localLibraryItemId = localLibraryItem?.id
val description = libraryItem.getMediaDescription(progress, ctx)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
} else {
result.sendResult(null)
}
} else {
Log.d(tag, "Loading podcast episodes for podcast $parentMediaId")

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.provider.Settings
import android.util.Base64
import android.util.Log
import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.device.DeviceManager
@ -189,6 +190,90 @@ class ApiHandler(var ctx:Context) {
}
}
fun getLibrarySeries(libraryId:String, cb: (List<LibrarySeriesItem>) -> Unit) {
Log.d(tag, "Getting series")
getRequest("/api/libraries/$libraryId/series?minified=1&sort=name&limit=10000", null, null) {
val items = mutableListOf<LibrarySeriesItem>()
if (it.has("results")) {
val array = it.getJSONArray("results")
for (i in 0 until array.length()) {
val item = jacksonMapper.readValue<LibrarySeriesItem>(array.get(i).toString())
items.add(item)
}
}
cb(items)
}
}
fun getLibrarySeriesItems(libraryId:String, seriesId:String, cb: (List<LibraryItem>) -> Unit) {
Log.d(tag, "Getting items for series")
val seriesIdBase64 = Base64.encodeToString(seriesId.toByteArray(), Base64.DEFAULT)
getRequest("/api/libraries/$libraryId/items?minified=1&sort=media.metadata.title&filter=series.${seriesIdBase64}&limit=1000", null, null) {
val items = mutableListOf<LibraryItem>()
if (it.has("results")) {
val array = it.getJSONArray("results")
for (i in 0 until array.length()) {
val item = jacksonMapper.readValue<LibraryItem>(array.get(i).toString())
items.add(item)
}
}
cb(items)
}
}
fun getLibraryAuthors(libraryId:String, cb: (List<LibraryAuthorItem>) -> Unit) {
Log.d(tag, "Getting series")
getRequest("/api/libraries/$libraryId/authors", null, null) {
val items = mutableListOf<LibraryAuthorItem>()
if (it.has("authors")) {
val array = it.getJSONArray("authors")
for (i in 0 until array.length()) {
val item = jacksonMapper.readValue<LibraryAuthorItem>(array.get(i).toString())
items.add(item)
}
}else{
Log.e(tag, "No results")
}
cb(items)
}
}
fun getLibraryItemsFromAuthor(libraryId:String, authorId:String, cb: (List<LibraryItem>) -> Unit) {
Log.d(tag, "Getting author items")
val authorIdBase64 = Base64.encodeToString(authorId.toByteArray(), Base64.DEFAULT)
getRequest("/api/libraries/$libraryId/items?limit=1000&minified=1&filter=authors.${authorIdBase64}&sort=media.metadata.title&collapseseries=1", null, null) {
val items = mutableListOf<LibraryItem>()
if (it.has("results")) {
val array = it.getJSONArray("results")
for (i in 0 until array.length()) {
val item = jacksonMapper.readValue<LibraryItem>(array.get(i).toString())
if (item.collapsedSeries != null) {
item.collapsedSeries?.libraryId = libraryId
}
items.add(item)
}
}else{
Log.e(tag, "No results")
}
cb(items)
}
}
fun getLibraryCollections(libraryId:String, cb: (List<LibraryCollection>) -> Unit) {
Log.d(tag, "Getting collections")
getRequest("/api/libraries/$libraryId/collections?minified=1&sort=name&limit=1000", null, null) {
val items = mutableListOf<LibraryCollection>()
if (it.has("results")) {
val array = it.getJSONArray("results")
for (i in 0 until array.length()) {
val item = jacksonMapper.readValue<LibraryCollection>(array.get(i).toString())
items.add(item)
}
}
cb(items)
}
}
fun getAllItemsInProgress(cb: (List<ItemInProgress>) -> Unit) {
getRequest("/api/me/items-in-progress", null, null) {
val items = mutableListOf<ItemInProgress>()

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1 @@
<!-- drawable/account_outline.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#FFFFFF" android:pathData="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z" /></vector>