mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-08 21:08:43 +02:00
Enchancements for Android Auto library
- Hide libraries without audiobooks - Sort books in series by sequence value - Added option for selecting ASC or DESC sorting for series - Order authors alphabetically
This commit is contained in:
@@ -214,7 +214,9 @@ class BookMetadata(
|
||||
var authorName:String?,
|
||||
var authorNameLF:String?,
|
||||
var narratorName:String?,
|
||||
var seriesName:String?
|
||||
var seriesName:String?,
|
||||
@JsonFormat(with=[JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY])
|
||||
var series:List<SeriesType>?
|
||||
) : MediaTypeMetadata(title, explicit) {
|
||||
@JsonIgnore
|
||||
override fun getAuthorDisplayName():String { return authorName ?: "Unknown" }
|
||||
@@ -342,7 +344,8 @@ data class Library(
|
||||
var name:String,
|
||||
var folders:MutableList<Folder>,
|
||||
var icon:String,
|
||||
var mediaType:String
|
||||
var mediaType:String,
|
||||
var stats: LibraryStats?
|
||||
) {
|
||||
@JsonIgnore
|
||||
fun getMediaMetadata(): MediaMetadataCompat {
|
||||
@@ -354,6 +357,20 @@ data class Library(
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class LibraryStats(
|
||||
var totalItems: Int,
|
||||
var totalAuthors: Int,
|
||||
var numAudioTracks: Int
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class SeriesType(
|
||||
var id: String,
|
||||
var name: String,
|
||||
var sequence: String?
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class Folder(
|
||||
var id:String,
|
||||
|
||||
@@ -28,6 +28,10 @@ enum class StreamingUsingCellularSetting {
|
||||
ASK, ALWAYS, NEVER
|
||||
}
|
||||
|
||||
enum class AndroidAutoBrowseSeriesSequenceOrderSetting {
|
||||
ASC, DESC
|
||||
}
|
||||
|
||||
data class ServerConnectionConfig(
|
||||
var id:String,
|
||||
var index:Int,
|
||||
@@ -136,7 +140,8 @@ data class DeviceSettings(
|
||||
var streamingUsingCellular: StreamingUsingCellularSetting,
|
||||
var androidAutoBrowseForceGrouping: Boolean,
|
||||
var androidAutoBrowseTopLevelLimitForGrouping: Int,
|
||||
var androidAutoBrowseLimitForGrouping: Int
|
||||
var androidAutoBrowseLimitForGrouping: Int,
|
||||
var androidAutoBrowseSeriesSequenceOrder: AndroidAutoBrowseSeriesSequenceOrderSetting
|
||||
) {
|
||||
companion object {
|
||||
// Static method to get default device settings
|
||||
@@ -165,7 +170,8 @@ data class DeviceSettings(
|
||||
streamingUsingCellular = StreamingUsingCellularSetting.ALWAYS,
|
||||
androidAutoBrowseForceGrouping = false,
|
||||
androidAutoBrowseTopLevelLimitForGrouping = 100,
|
||||
androidAutoBrowseLimitForGrouping = 50
|
||||
androidAutoBrowseLimitForGrouping = 50,
|
||||
androidAutoBrowseSeriesSequenceOrder = AndroidAutoBrowseSeriesSequenceOrderSetting.ASC
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,8 +64,27 @@ class LibraryItem(
|
||||
}
|
||||
}
|
||||
|
||||
@get:JsonIgnore
|
||||
val seriesSequence: String
|
||||
get() {
|
||||
if (mediaType != "podcast") {
|
||||
return ((media as Book).metadata as BookMetadata).series?.get(0)?.sequence.orEmpty()
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
@get:JsonIgnore
|
||||
val seriesSequenceParts: List<String>
|
||||
get() {
|
||||
if (seriesSequence.isEmpty()) {
|
||||
return listOf("")
|
||||
}
|
||||
return seriesSequence.split(".", limit = 2)
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context, authorId: String?): MediaDescriptionCompat {
|
||||
fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context, authorId: String?, showSeriesNumber: Boolean?): MediaDescriptionCompat {
|
||||
val extras = Bundle()
|
||||
|
||||
if (collapsedSeries == null) {
|
||||
@@ -121,20 +140,29 @@ class LibraryItem(
|
||||
if (collapsedSeries != null) {
|
||||
subtitle = "${collapsedSeries!!.numBooks} books"
|
||||
}
|
||||
var itemTitle = title
|
||||
if (showSeriesNumber == true && seriesSequence != "") {
|
||||
itemTitle = "$seriesSequence. $itemTitle"
|
||||
}
|
||||
return MediaDescriptionCompat.Builder()
|
||||
.setMediaId(mediaId)
|
||||
.setTitle(title)
|
||||
.setTitle(itemTitle)
|
||||
.setIconUri(getCoverUri())
|
||||
.setSubtitle(subtitle)
|
||||
.setExtras(extras)
|
||||
.build()
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context, authorId: String?): MediaDescriptionCompat {
|
||||
return getMediaDescription(progress, ctx, authorId, null)
|
||||
}
|
||||
|
||||
@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)
|
||||
return getMediaDescription(progress, ctx, null, null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ data class LocalMediaItem(
|
||||
@JsonIgnore
|
||||
fun getMediaMetadata():MediaTypeMetadata {
|
||||
return if (mediaType == "book") {
|
||||
BookMetadata(name,null, mutableListOf(), mutableListOf(), mutableListOf(),null,null,null,null,null,null,null,false,null,null,null,null)
|
||||
BookMetadata(name,null, mutableListOf(), mutableListOf(), mutableListOf(),null,null,null,null,null,null,null,false,null,null,null, null, null)
|
||||
} else {
|
||||
PodcastMetadata(name,null,null, mutableListOf(), false)
|
||||
}
|
||||
|
||||
@@ -71,6 +71,9 @@ object DeviceManager {
|
||||
if (deviceData.deviceSettings?.androidAutoBrowseLimitForGrouping == null) {
|
||||
deviceData.deviceSettings?.androidAutoBrowseLimitForGrouping = 50
|
||||
}
|
||||
if (deviceData.deviceSettings?.androidAutoBrowseSeriesSequenceOrder == null) {
|
||||
deviceData.deviceSettings?.androidAutoBrowseSeriesSequenceOrder = AndroidAutoBrowseSeriesSequenceOrderSetting.ASC
|
||||
}
|
||||
}
|
||||
|
||||
fun getBase64Id(id:String):String {
|
||||
|
||||
@@ -46,6 +46,10 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
||||
return serverLibraries.find { it.id == id } != null
|
||||
}
|
||||
|
||||
fun getLibrary(id:String) : Library? {
|
||||
return serverLibraries.find { it.id == id }
|
||||
}
|
||||
|
||||
fun getSavedPlaybackRate():Float {
|
||||
if (userSettingsPlaybackRate != null) {
|
||||
return userSettingsPlaybackRate ?: 1f
|
||||
@@ -185,6 +189,14 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
||||
cb(seriesWithBooks)
|
||||
}
|
||||
|
||||
fun sortSeriesBooks(seriesBooks: List<LibraryItem>) : List<LibraryItem> {
|
||||
val sortingLogic = compareBy<LibraryItem> { it.seriesSequenceParts[0].length }
|
||||
.thenBy { it.seriesSequenceParts[0].ifEmpty { "" } }
|
||||
.thenBy { it.seriesSequenceParts.getOrElse(1) { "" }.length }
|
||||
.thenBy { it.seriesSequenceParts.getOrElse(1) { "" } }
|
||||
return seriesBooks.sortedWith(sortingLogic)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns books for series from library.
|
||||
* If data is not found from local cache then it will be fetched from server
|
||||
@@ -202,14 +214,15 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
||||
Log.d(tag, "Items for series $seriesId loaded from server | Library $libraryId")
|
||||
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
|
||||
|
||||
cachedLibrarySeriesItem[libraryId]!![seriesId] = libraryItemsWithAudio
|
||||
val sortedLibraryItemsWithAudio = sortSeriesBooks(libraryItemsWithAudio)
|
||||
cachedLibrarySeriesItem[libraryId]!![seriesId] = sortedLibraryItemsWithAudio
|
||||
|
||||
libraryItemsWithAudio.forEach { libraryItem ->
|
||||
sortedLibraryItemsWithAudio.forEach { libraryItem ->
|
||||
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
||||
serverLibraryItems.add(libraryItem)
|
||||
}
|
||||
}
|
||||
cb(libraryItemsWithAudio)
|
||||
cb(sortedLibraryItemsWithAudio)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,8 +241,8 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
||||
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 }
|
||||
|
||||
var authorItemsWithBooks = authorItems.filter { li -> li.bookCount != null && li.bookCount!! > 0 }
|
||||
authorItemsWithBooks = authorItemsWithBooks.sortedBy { it.name }
|
||||
// Ensure that there is map for library
|
||||
cachedLibraryAuthors[libraryId] = mutableMapOf()
|
||||
// Cache authors
|
||||
@@ -314,15 +327,16 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
||||
Log.d(tag, "Using author name: $authorName")
|
||||
val libraryItemsFromAuthorWithAudio = libraryItemsWithAudio.filter { li -> li.authorName.indexOf(authorName, ignoreCase = true) >= 0 }
|
||||
|
||||
cachedLibraryAuthorSeriesItems[libraryId]!![authorId] = libraryItemsFromAuthorWithAudio
|
||||
val sortedLibraryItemsWithAudio = sortSeriesBooks(libraryItemsFromAuthorWithAudio)
|
||||
cachedLibraryAuthorSeriesItems[libraryId]!![authorId] = sortedLibraryItemsWithAudio
|
||||
|
||||
libraryItemsFromAuthorWithAudio.forEach { libraryItem ->
|
||||
sortedLibraryItemsWithAudio.forEach { libraryItem ->
|
||||
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
||||
serverLibraryItems.add(libraryItem)
|
||||
}
|
||||
}
|
||||
|
||||
cb(libraryItemsFromAuthorWithAudio)
|
||||
cb(sortedLibraryItemsWithAudio)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -443,9 +457,15 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
||||
if (serverLibraries.isNotEmpty()) {
|
||||
cb(serverLibraries)
|
||||
} else {
|
||||
apiHandler.getLibraries {
|
||||
serverLibraries = it
|
||||
cb(it)
|
||||
apiHandler.getLibraries { loadedLibraries ->
|
||||
serverLibraries = loadedLibraries.map { library ->
|
||||
apiHandler.getLibraryStats(library.id) { libraryStats ->
|
||||
Log.d(tag, "Library stats for library ${library.id} | $libraryStats")
|
||||
library.stats = libraryStats
|
||||
}
|
||||
library
|
||||
}
|
||||
cb(serverLibraries)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ class BrowseTree(
|
||||
rootList += librariesMetadata
|
||||
|
||||
libraries.forEach { library ->
|
||||
if (library.stats?.numAudioTracks == 0) return@forEach
|
||||
val libraryMediaMetadata = library.getMediaMetadata()
|
||||
val children = mediaIdToChildren[LIBRARIES_ROOT] ?: mutableListOf()
|
||||
children += libraryMediaMetadata
|
||||
|
||||
+49
-27
@@ -1099,30 +1099,44 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
}
|
||||
} else if (mediaManager.getIsLibrary(parentMediaId)) { // Load library items for library
|
||||
Log.d(tag, "Loading items for library $parentMediaId")
|
||||
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
|
||||
val selectedLibrary = mediaManager.getLibrary(parentMediaId)
|
||||
if (selectedLibrary?.mediaType == "podcast") { // Podcasts are browseable
|
||||
mediaManager.loadLibraryItemsWithAudio(parentMediaId) { libraryItems ->
|
||||
val children = libraryItems.map { libraryItem ->
|
||||
val mediaDescription = libraryItem.getMediaDescription(null, ctx)
|
||||
MediaBrowserCompat.MediaItem(
|
||||
mediaDescription,
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
} else {
|
||||
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>?)
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
} else if (parentMediaId.startsWith("__LIBRARY__")) {
|
||||
Log.d(tag, "Browsing library $parentMediaId")
|
||||
val mediaIdParts = parentMediaId.split("__")
|
||||
@@ -1201,12 +1215,16 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
mediaIdParts[4]
|
||||
) { libraryItems ->
|
||||
Log.d(tag, "Received ${libraryItems.size} library items")
|
||||
val children = libraryItems.map { libraryItem ->
|
||||
var items = libraryItems
|
||||
if (DeviceManager.deviceData.deviceSettings!!.androidAutoBrowseSeriesSequenceOrder === AndroidAutoBrowseSeriesSequenceOrderSetting.DESC) {
|
||||
items = libraryItems.reversed()
|
||||
}
|
||||
val children = items.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)
|
||||
val description = libraryItem.getMediaDescription(progress, ctx, null, true)
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
@@ -1279,11 +1297,15 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
}
|
||||
} else if (mediaIdParts[3] == "AUTHOR_SERIES") {
|
||||
mediaManager.loadAuthorSeriesBooksWithAudio(mediaIdParts[2], mediaIdParts[4], mediaIdParts[5]) { libraryItems ->
|
||||
val children = libraryItems.map { libraryItem ->
|
||||
var items = libraryItems
|
||||
if (DeviceManager.deviceData.deviceSettings!!.androidAutoBrowseSeriesSequenceOrder === AndroidAutoBrowseSeriesSequenceOrderSetting.DESC) {
|
||||
items = libraryItems.reversed()
|
||||
}
|
||||
val children = items.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)
|
||||
val description = libraryItem.getMediaDescription(progress, ctx, null, true)
|
||||
if (libraryItem.collapsedSeries != null) {
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
} else {
|
||||
|
||||
@@ -150,6 +150,18 @@ class ApiHandler(var ctx:Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibraryStats(libraryItemId:String, cb: (LibraryStats?) -> Unit) {
|
||||
getRequest("/api/libraries/$libraryItemId/stats", null, null) {
|
||||
if (it.has("error")) {
|
||||
Log.e(tag, it.getString("error") ?: "getLibraryStats Failed")
|
||||
cb(null)
|
||||
} else {
|
||||
val libraryStats = jacksonMapper.readValue<LibraryStats>(it.toString())
|
||||
cb(libraryStats)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibraryItem(libraryItemId:String, cb: (LibraryItem?) -> Unit) {
|
||||
getRequest("/api/items/$libraryItemId?expanded=1", null, null) {
|
||||
if (it.has("error")) {
|
||||
|
||||
Reference in New Issue
Block a user