mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-28 22:27:14 +02:00
Android Auto improvements
General: - New top menu item Recent is added - Library caches are cleared when switching server Search: - Is done using server API - Latest search is cache to prevent need to make new request when returning from browsable item. - Results are grouped by book, series, author and split by library - Only searches libraries with audio content Library personalized shelves: - Recent books, series, authors, podcasts and episodes shelves are listed under Recent top menu - Discovery shelves can be found under library many from corresponding library
This commit is contained in:
@@ -348,9 +348,13 @@ data class Library(
|
|||||||
var stats: LibraryStats?
|
var stats: LibraryStats?
|
||||||
) {
|
) {
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getMediaMetadata(): MediaMetadataCompat {
|
fun getMediaMetadata(targetType: String? = null): MediaMetadataCompat {
|
||||||
|
var mediaId = id
|
||||||
|
if (targetType !== null) {
|
||||||
|
mediaId = "__RECENTLY__$id"
|
||||||
|
}
|
||||||
return MediaMetadataCompat.Builder().apply {
|
return MediaMetadataCompat.Builder().apply {
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId)
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, name)
|
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, name)
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, name)
|
putString(MediaMetadataCompat.METADATA_KEY_TITLE, name)
|
||||||
}.build()
|
}.build()
|
||||||
@@ -423,3 +427,64 @@ data class LibraryItemSearchResultType(
|
|||||||
var series:List<LibraryItemSearchResultSeriesItemType>?,
|
var series:List<LibraryItemSearchResultSeriesItemType>?,
|
||||||
var authors:List<LibraryAuthorItem>?
|
var authors:List<LibraryAuthorItem>?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonTypeInfo(
|
||||||
|
use=JsonTypeInfo.Id.NAME,
|
||||||
|
property = "type",
|
||||||
|
include = JsonTypeInfo.As.PROPERTY,
|
||||||
|
visible = true
|
||||||
|
)
|
||||||
|
@JsonSubTypes(
|
||||||
|
JsonSubTypes.Type(LibraryShelfBookEntity::class, name = "book"),
|
||||||
|
JsonSubTypes.Type(LibraryShelfSeriesEntity::class, name = "series"),
|
||||||
|
JsonSubTypes.Type(LibraryShelfAuthorEntity::class, name = "authors"),
|
||||||
|
JsonSubTypes.Type(LibraryShelfEpisodeEntity::class, name = "episode"),
|
||||||
|
JsonSubTypes.Type(LibraryShelfPodcastEntity::class, name = "podcast")
|
||||||
|
)
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
sealed class LibraryShelfType(
|
||||||
|
open val id: String,
|
||||||
|
open val label: String,
|
||||||
|
open val total: Int,
|
||||||
|
open val type: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LibraryShelfBookEntity(
|
||||||
|
override val id: String,
|
||||||
|
override val label: String,
|
||||||
|
override val total: Int,
|
||||||
|
override val type: String,
|
||||||
|
val entities: List<LibraryItem>?
|
||||||
|
) : LibraryShelfType(id, label, total, type)
|
||||||
|
|
||||||
|
data class LibraryShelfSeriesEntity(
|
||||||
|
override val id: String,
|
||||||
|
override val label: String,
|
||||||
|
override val total: Int,
|
||||||
|
override val type: String,
|
||||||
|
val entities: List<LibrarySeriesItem>?
|
||||||
|
) : LibraryShelfType(id, label, total, type)
|
||||||
|
|
||||||
|
data class LibraryShelfAuthorEntity(
|
||||||
|
override val id: String,
|
||||||
|
override val label: String,
|
||||||
|
override val total: Int,
|
||||||
|
override val type: String,
|
||||||
|
val entities: List<LibraryAuthorItem>?
|
||||||
|
) : LibraryShelfType(id, label, total, type)
|
||||||
|
|
||||||
|
data class LibraryShelfEpisodeEntity(
|
||||||
|
override val id: String,
|
||||||
|
override val label: String,
|
||||||
|
override val total: Int,
|
||||||
|
override val type: String,
|
||||||
|
val entities: List<LibraryItem>?
|
||||||
|
) : LibraryShelfType(id, label, total, type)
|
||||||
|
|
||||||
|
data class LibraryShelfPodcastEntity(
|
||||||
|
override val id: String,
|
||||||
|
override val label: String,
|
||||||
|
override val total: Int,
|
||||||
|
override val type: String,
|
||||||
|
val entities: List<LibraryItem>?
|
||||||
|
) : LibraryShelfType(id, label, total, type)
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ class LibraryItem(
|
|||||||
var libraryFiles:MutableList<LibraryFile>?,
|
var libraryFiles:MutableList<LibraryFile>?,
|
||||||
var userMediaProgress:MediaProgress?, // Only included when requesting library item with progress (for downloads)
|
var userMediaProgress:MediaProgress?, // Only included when requesting library item with progress (for downloads)
|
||||||
var collapsedSeries: CollapsedSeries?,
|
var collapsedSeries: CollapsedSeries?,
|
||||||
var localLibraryItemId:String? // For Android Auto
|
var localLibraryItemId:String?, // For Android Auto
|
||||||
|
val recentEpisode: PodcastEpisode? // Podcast episode shelf uses this
|
||||||
) : LibraryItemWrapper(id) {
|
) : LibraryItemWrapper(id) {
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val title: String
|
val title: String
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
val tag = "MediaManager"
|
val tag = "MediaManager"
|
||||||
|
|
||||||
private var serverLibraryItems = mutableListOf<LibraryItem>() // Store all items here
|
private var serverLibraryItems = mutableListOf<LibraryItem>() // Store all items here
|
||||||
private var selectedLibraryItems = mutableListOf<LibraryItem>()
|
|
||||||
private var selectedLibraryId = ""
|
|
||||||
|
|
||||||
private var cachedLibraryAuthors : MutableMap<String, MutableMap<String, LibraryAuthorItem>> = hashMapOf()
|
private var cachedLibraryAuthors : MutableMap<String, MutableMap<String, LibraryAuthorItem>> = hashMapOf()
|
||||||
private var cachedLibraryAuthorItems : MutableMap<String, MutableMap<String, List<LibraryItem>>> = hashMapOf()
|
private var cachedLibraryAuthorItems : MutableMap<String, MutableMap<String, List<LibraryItem>>> = hashMapOf()
|
||||||
@@ -29,11 +27,14 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
private var cachedLibrarySeries : MutableMap<String, List<LibrarySeriesItem>> = hashMapOf()
|
private var cachedLibrarySeries : MutableMap<String, List<LibrarySeriesItem>> = hashMapOf()
|
||||||
private var cachedLibrarySeriesItem : MutableMap<String, MutableMap<String, List<LibraryItem>>> = hashMapOf()
|
private var cachedLibrarySeriesItem : MutableMap<String, MutableMap<String, List<LibraryItem>>> = hashMapOf()
|
||||||
private var cachedLibraryCollections : MutableMap<String, MutableMap<String, LibraryCollection>> = hashMapOf()
|
private var cachedLibraryCollections : MutableMap<String, MutableMap<String, LibraryCollection>> = hashMapOf()
|
||||||
|
private var cachedLibraryRecentShelfs : MutableMap<String, MutableList<LibraryShelfType>> = hashMapOf()
|
||||||
|
private var cachedLibraryDiscovery : MutableMap<String, MutableList<LibraryItem>> = hashMapOf()
|
||||||
|
private var cachedLibraryPodcasts : MutableMap<String, MutableMap<String, LibraryItem>> = hashMapOf()
|
||||||
|
private var isLibraryPodcastsCached : MutableMap<String, Boolean> = hashMapOf()
|
||||||
|
|
||||||
private var selectedPodcast:Podcast? = null
|
private var selectedPodcast:Podcast? = null
|
||||||
private var selectedLibraryItemId:String? = null
|
private var selectedLibraryItemId:String? = null
|
||||||
private var podcastEpisodeLibraryItemMap = mutableMapOf<String, LibraryItemWithEpisode>()
|
private var podcastEpisodeLibraryItemMap = mutableMapOf<String, LibraryItemWithEpisode>()
|
||||||
private var serverLibraryCategories = listOf<LibraryCategory>()
|
|
||||||
private var serverConfigIdUsed:String? = null
|
private var serverConfigIdUsed:String? = null
|
||||||
private var serverConfigLastPing:Long = 0L
|
private var serverConfigLastPing:Long = 0L
|
||||||
var serverUserMediaProgress:MutableList<MediaProgress> = mutableListOf()
|
var serverUserMediaProgress:MutableList<MediaProgress> = mutableListOf()
|
||||||
@@ -46,10 +47,27 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
return serverLibraries.find { it.id == id } != null
|
return serverLibraries.find { it.id == id } != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getHasDiscovery(libraryId: String) : Boolean {
|
||||||
|
if (cachedLibraryDiscovery.containsKey(libraryId)) {
|
||||||
|
if (cachedLibraryDiscovery[libraryId]!!.isNotEmpty()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
populatePersonalizedDataForLibrary(libraryId){}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
fun getLibrary(id:String) : Library? {
|
fun getLibrary(id:String) : Library? {
|
||||||
return serverLibraries.find { it.id == id }
|
return serverLibraries.find { it.id == id }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun addServerLibrary(libraryItem: LibraryItem) {
|
||||||
|
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
||||||
|
serverLibraryItems.add(libraryItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getSavedPlaybackRate():Float {
|
fun getSavedPlaybackRate():Float {
|
||||||
if (userSettingsPlaybackRate != null) {
|
if (userSettingsPlaybackRate != null) {
|
||||||
return userSettingsPlaybackRate ?: 1f
|
return userSettingsPlaybackRate ?: 1f
|
||||||
@@ -109,11 +127,18 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
|
|
||||||
if (!DeviceManager.isConnectedToServer || !DeviceManager.checkConnectivity(ctx) || serverConnConfig == null || serverConnConfig.id !== serverConfigIdUsed) {
|
if (!DeviceManager.isConnectedToServer || !DeviceManager.checkConnectivity(ctx) || serverConnConfig == null || serverConnConfig.id !== serverConfigIdUsed) {
|
||||||
podcastEpisodeLibraryItemMap = mutableMapOf()
|
podcastEpisodeLibraryItemMap = mutableMapOf()
|
||||||
serverLibraryCategories = listOf()
|
|
||||||
serverLibraries = listOf()
|
serverLibraries = listOf()
|
||||||
serverLibraryItems = mutableListOf()
|
serverLibraryItems = mutableListOf()
|
||||||
selectedLibraryItems = mutableListOf()
|
cachedLibraryAuthors = hashMapOf()
|
||||||
selectedLibraryId = ""
|
cachedLibraryAuthorItems = hashMapOf()
|
||||||
|
cachedLibraryAuthorSeriesItems = hashMapOf()
|
||||||
|
cachedLibrarySeries = hashMapOf()
|
||||||
|
cachedLibrarySeriesItem = hashMapOf()
|
||||||
|
cachedLibraryCollections = hashMapOf()
|
||||||
|
cachedLibraryRecentShelfs = hashMapOf()
|
||||||
|
cachedLibraryDiscovery = hashMapOf()
|
||||||
|
cachedLibraryPodcasts = hashMapOf()
|
||||||
|
isLibraryPodcastsCached = hashMapOf()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,24 +156,112 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadLibraryItemsWithAudio(libraryId:String, cb: (List<LibraryItem>) -> Unit) {
|
fun populatePersonalizedDataForAllLibraries(cb: () -> Unit ) {
|
||||||
if (selectedLibraryItems.isNotEmpty() && selectedLibraryId == libraryId) {
|
serverLibraries.forEach {
|
||||||
cb(selectedLibraryItems)
|
Log.d(tag, "Loading personalization for library ${it.name} - ${it.id} - ${it.mediaType}")
|
||||||
|
populatePersonalizedDataForLibrary(it.id) {
|
||||||
|
Log.d(tag, "Loaded personalization for library ${it.name} - ${it.id} - ${it.mediaType}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populatePersonalizedDataForLibrary(libraryId: String, cb: () -> Unit) {
|
||||||
|
apiHandler.getLibraryPersonalized(libraryId) { shelfs ->
|
||||||
|
Log.d(tag, "populatePersonalizedDataForLibrary $libraryId")
|
||||||
|
if (shelfs === null) return@getLibraryPersonalized
|
||||||
|
shelfs.map { shelf ->
|
||||||
|
Log.d(tag, "$shelf")
|
||||||
|
if (shelf.type == "book") {
|
||||||
|
if (shelf.id == "continue-listening") return@map
|
||||||
|
else if (shelf.id == "listen-again") return@map
|
||||||
|
else if (shelf.id == "recently-added") {
|
||||||
|
if (!cachedLibraryRecentShelfs.containsKey(libraryId)) {
|
||||||
|
cachedLibraryRecentShelfs[libraryId] = mutableListOf()
|
||||||
|
}
|
||||||
|
cachedLibraryRecentShelfs[libraryId]!!.add(shelf)
|
||||||
|
}
|
||||||
|
else if (shelf.id == "discover") {
|
||||||
|
if (!cachedLibraryDiscovery.containsKey(libraryId)) {
|
||||||
|
cachedLibraryDiscovery[libraryId] = mutableListOf()
|
||||||
|
}
|
||||||
|
(shelf as LibraryShelfBookEntity).entities?.map {
|
||||||
|
cachedLibraryDiscovery[libraryId]!!.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (shelf.id == "continue-reading") return@map
|
||||||
|
else if (shelf.id == "continue-series") return@map
|
||||||
|
shelf as LibraryShelfBookEntity
|
||||||
|
} else if (shelf.type == "series") {
|
||||||
|
if (shelf.id == "recent-series") {
|
||||||
|
if (!cachedLibraryRecentShelfs.containsKey(libraryId)) {
|
||||||
|
cachedLibraryRecentShelfs[libraryId] = mutableListOf()
|
||||||
|
}
|
||||||
|
cachedLibraryRecentShelfs[libraryId]!!.add(shelf)
|
||||||
|
}
|
||||||
|
} else if (shelf.type == "episode") {
|
||||||
|
if (shelf.id == "continue-listening") return@map
|
||||||
|
else if (shelf.id == "listen-again") return@map
|
||||||
|
else if (shelf.id == "newest-episodes") {
|
||||||
|
if (!cachedLibraryRecentShelfs.containsKey(libraryId)) {
|
||||||
|
cachedLibraryRecentShelfs[libraryId] = mutableListOf()
|
||||||
|
}
|
||||||
|
cachedLibraryRecentShelfs[libraryId]!!.add(shelf)
|
||||||
|
|
||||||
|
(shelf as LibraryShelfEpisodeEntity).entities?.forEach { libraryItem ->
|
||||||
|
loadPodcastItem(libraryItem.libraryId, libraryItem.id) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (shelf.type == "podcast") {
|
||||||
|
if (shelf.id == "recently-added"){
|
||||||
|
if (!cachedLibraryRecentShelfs.containsKey(libraryId)) {
|
||||||
|
cachedLibraryRecentShelfs[libraryId] = mutableListOf()
|
||||||
|
}
|
||||||
|
cachedLibraryRecentShelfs[libraryId]!!.add(shelf)
|
||||||
|
}
|
||||||
|
else if (shelf.id == "discover"){
|
||||||
|
return@map
|
||||||
|
}
|
||||||
|
} else if (shelf.type =="authors") {
|
||||||
|
if (shelf.id == "newest-authors") {
|
||||||
|
if (!cachedLibraryRecentShelfs.containsKey(libraryId)) {
|
||||||
|
cachedLibraryRecentShelfs[libraryId] = mutableListOf()
|
||||||
|
}
|
||||||
|
cachedLibraryRecentShelfs[libraryId]!!.add(shelf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
Log.d(tag, "populatePersonalizedDataForLibrary $libraryId DONE")
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadLibraryPodcasts(libraryId:String, cb: (List<LibraryItem>?) -> Unit) {
|
||||||
|
// Without this there is possibility that only recent podcasts get loaded
|
||||||
|
// Loading recent podcasts will also create cachedLibraryPodcasts entry for library
|
||||||
|
if (!isLibraryPodcastsCached.containsKey(libraryId)) {
|
||||||
|
isLibraryPodcastsCached[libraryId] = false
|
||||||
|
}
|
||||||
|
// Ensure that there is map for library
|
||||||
|
if (!cachedLibraryPodcasts.containsKey(libraryId)) {
|
||||||
|
cachedLibraryPodcasts[libraryId] = mutableMapOf()
|
||||||
|
}
|
||||||
|
if (isLibraryPodcastsCached.getOrElse(libraryId) {false}) {
|
||||||
|
Log.d(tag, "loadLibraryPodcasts: Found from cache: $libraryId")
|
||||||
|
cb(cachedLibraryPodcasts[libraryId]?.values?.sortedBy { libraryItem -> (libraryItem.media as Podcast).metadata.title })
|
||||||
} else {
|
} else {
|
||||||
apiHandler.getLibraryItems(libraryId) { libraryItems ->
|
apiHandler.getLibraryItems(libraryId) { libraryItems ->
|
||||||
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
|
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
|
||||||
if (libraryItemsWithAudio.isNotEmpty()) {
|
|
||||||
selectedLibraryId = libraryId
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedLibraryItems = mutableListOf()
|
|
||||||
libraryItemsWithAudio.forEach { libraryItem ->
|
libraryItemsWithAudio.forEach { libraryItem ->
|
||||||
selectedLibraryItems.add(libraryItem)
|
cachedLibraryPodcasts[libraryId]?.set(libraryItem.id, libraryItem)
|
||||||
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
||||||
serverLibraryItems.add(libraryItem)
|
serverLibraryItems.add(libraryItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cb(libraryItemsWithAudio)
|
isLibraryPodcastsCached[libraryId] = true
|
||||||
|
Log.d(tag, "loadLibraryPodcasts: loaded from server: $libraryId")
|
||||||
|
cb(libraryItemsWithAudio.sortedBy { libraryItem -> (libraryItem.media as Podcast).metadata.title })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -386,6 +499,62 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun loadLibraryDiscoveryBooksWithAudio(libraryId: String, cb: (List<LibraryItem>) -> Unit) {
|
||||||
|
if (!cachedLibraryDiscovery.containsKey(libraryId)) {
|
||||||
|
cb(listOf())
|
||||||
|
}
|
||||||
|
val libraryItemsWithAudio = cachedLibraryDiscovery[libraryId]!!.filter { li -> li.checkHasTracks() }
|
||||||
|
libraryItemsWithAudio.forEach { libraryItem -> addServerLibrary(libraryItem) }
|
||||||
|
cb(libraryItemsWithAudio)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLibraryRecentShelfs(libraryId: String, cb: (List<LibraryShelfType>) -> Unit) {
|
||||||
|
if (!cachedLibraryRecentShelfs.containsKey(libraryId)) {
|
||||||
|
cb(listOf())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cb(cachedLibraryRecentShelfs[libraryId] as List<LibraryShelfType>)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLibraryRecentShelfByType(libraryId: String, type:String, cb: (LibraryShelfType?) -> Unit) {
|
||||||
|
Log.d(tag, "getLibraryRecentShelfByType: $libraryId | $type")
|
||||||
|
if (!cachedLibraryRecentShelfs.containsKey(libraryId)) {
|
||||||
|
cb(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (shelf in cachedLibraryRecentShelfs[libraryId]!!) {
|
||||||
|
if (shelf.type == type.lowercase()) {
|
||||||
|
cb(shelf)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cb(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadPodcastItem(libraryId: String, libraryItemId: String, cb: (LibraryItem?) -> Unit) {
|
||||||
|
// Ensure that there is map for library
|
||||||
|
if (!cachedLibraryPodcasts.containsKey(libraryId)) {
|
||||||
|
cachedLibraryPodcasts[libraryId] = mutableMapOf()
|
||||||
|
}
|
||||||
|
if (cachedLibraryPodcasts[libraryId]!!.containsKey(libraryItemId)) {
|
||||||
|
Log.d(tag, "Podcast found from cache | Library $libraryItemId ")
|
||||||
|
cb(cachedLibraryPodcasts[libraryId]?.get(libraryItemId))
|
||||||
|
} else {
|
||||||
|
Log.d(tag, "loadPodcastItem: $libraryItemId")
|
||||||
|
apiHandler.getLibraryItem(libraryItemId) { libraryItem ->
|
||||||
|
if (libraryItem !== null) {
|
||||||
|
Log.d(tag, "loadPodcastItem: Got library item $libraryItem")
|
||||||
|
val podcast = libraryItem.media as Podcast
|
||||||
|
podcast.episodes?.forEach { podcastEpisode ->
|
||||||
|
podcastEpisodeLibraryItemMap[podcastEpisode.id] = LibraryItemWithEpisode(libraryItem, podcastEpisode)
|
||||||
|
}
|
||||||
|
cachedLibraryPodcasts[libraryId]?.set(libraryItemId, libraryItem)
|
||||||
|
cb(libraryItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadLibraryItem(libraryItemId:String, cb: (LibraryItemWrapper?) -> Unit) {
|
private fun loadLibraryItem(libraryItemId:String, cb: (LibraryItemWrapper?) -> Unit) {
|
||||||
if (libraryItemId.startsWith("local")) {
|
if (libraryItemId.startsWith("local")) {
|
||||||
cb(DeviceManager.dbManager.getLocalLibraryItem(libraryItemId))
|
cb(DeviceManager.dbManager.getLocalLibraryItem(libraryItemId))
|
||||||
@@ -619,10 +788,56 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun searchLocalCache(libraryId: String, queryString: String) : LibraryItemSearchResultType? {
|
suspend fun doSearch(libraryId: String, queryString: String) : Map<String, List<MediaBrowserCompat.MediaItem>> {
|
||||||
return suspendCoroutine {
|
return suspendCoroutine {
|
||||||
apiHandler.getSearchResults(libraryId, queryString) { results ->
|
apiHandler.getSearchResults(libraryId, queryString) { searchResult ->
|
||||||
it.resume(results)
|
Log.d(tag, "searchLocalCache: $searchResult")
|
||||||
|
// Nothing found from server
|
||||||
|
if (searchResult === null) {
|
||||||
|
it.resume(mapOf())
|
||||||
|
return@getSearchResults
|
||||||
|
}
|
||||||
|
|
||||||
|
var foundItems: MutableMap<String, List<MediaBrowserCompat.MediaItem>> = mutableMapOf()
|
||||||
|
|
||||||
|
val serverLibrary = serverLibraries.find { sl -> sl.id == libraryId }
|
||||||
|
|
||||||
|
// Books
|
||||||
|
if (searchResult.book !== null && searchResult.book!!.isNotEmpty()) {
|
||||||
|
Log.d(tag, "searchLocalCache: found ${searchResult.book!!.size} books")
|
||||||
|
val children = searchResult.book!!.map { bookResult ->
|
||||||
|
val libraryItem = bookResult.libraryItem
|
||||||
|
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
||||||
|
serverLibraryItems.add(libraryItem)
|
||||||
|
}
|
||||||
|
val progress = serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
|
||||||
|
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
|
||||||
|
libraryItem.localLibraryItemId = localLibraryItem?.id
|
||||||
|
val description = libraryItem.getMediaDescription(progress, ctx, null, null, "Books (${serverLibrary?.name})")
|
||||||
|
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||||
|
}
|
||||||
|
foundItems["book"] = children
|
||||||
|
}
|
||||||
|
if (searchResult.series !== null && searchResult.series!!.isNotEmpty()) {
|
||||||
|
Log.d(tag, "onSearch: found ${searchResult.series!!.size} series")
|
||||||
|
val children = searchResult.series!!.map { seriesResult ->
|
||||||
|
val seriesItem = seriesResult.series
|
||||||
|
seriesItem.books = seriesResult.books as MutableList<LibraryItem>
|
||||||
|
val description = seriesItem.getMediaDescription(null, ctx, "Series (${serverLibrary?.name})")
|
||||||
|
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||||
|
}
|
||||||
|
foundItems["series"] = children
|
||||||
|
}
|
||||||
|
if (searchResult.authors !== null && searchResult.authors!!.isNotEmpty()) {
|
||||||
|
Log.d(tag, "onSearch: found ${searchResult.authors!!.size} authors")
|
||||||
|
val children = searchResult.authors!!.map { authorItem ->
|
||||||
|
val description = authorItem.getMediaDescription(null, ctx, "Authors (${serverLibrary?.name})")
|
||||||
|
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||||
|
}
|
||||||
|
foundItems["authors"] = children
|
||||||
|
}
|
||||||
|
|
||||||
|
it.resume(foundItems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,10 +32,16 @@ class BrowseTree(
|
|||||||
|
|
||||||
val continueListeningMetadata = 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, "Continue")
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.exo_icon_localaudio).toString())
|
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.exo_icon_localaudio).toString())
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
|
val recentMetadata = MediaMetadataCompat.Builder().apply {
|
||||||
|
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, RECENTLY_ROOT)
|
||||||
|
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Recent")
|
||||||
|
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.md_clock_outline).toString())
|
||||||
|
}.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")
|
||||||
@@ -53,14 +59,24 @@ class BrowseTree(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (libraries.isNotEmpty()) {
|
if (libraries.isNotEmpty()) {
|
||||||
|
rootList += recentMetadata
|
||||||
rootList += librariesMetadata
|
rootList += librariesMetadata
|
||||||
|
|
||||||
libraries.forEach { library ->
|
libraries.forEach { library ->
|
||||||
|
// Skip libraries without audio content
|
||||||
if (library.stats?.numAudioTracks == 0) return@forEach
|
if (library.stats?.numAudioTracks == 0) return@forEach
|
||||||
|
|
||||||
|
// Generate library list items for Libraries menu
|
||||||
val libraryMediaMetadata = library.getMediaMetadata()
|
val libraryMediaMetadata = library.getMediaMetadata()
|
||||||
val children = mediaIdToChildren[LIBRARIES_ROOT] ?: mutableListOf()
|
val children = mediaIdToChildren[LIBRARIES_ROOT] ?: mutableListOf()
|
||||||
children += libraryMediaMetadata
|
children += libraryMediaMetadata
|
||||||
mediaIdToChildren[LIBRARIES_ROOT] = children
|
mediaIdToChildren[LIBRARIES_ROOT] = children
|
||||||
|
|
||||||
|
// Generate library list items for Recent menu
|
||||||
|
val recentlyMediaMetadata = library.getMediaMetadata("recently")
|
||||||
|
val childrenRecently = mediaIdToChildren[RECENTLY_ROOT] ?: mutableListOf()
|
||||||
|
childrenRecently += recentlyMediaMetadata
|
||||||
|
mediaIdToChildren[RECENTLY_ROOT] = childrenRecently
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,3 +92,4 @@ const val AUTO_BROWSE_ROOT = "/"
|
|||||||
const val CONTINUE_ROOT = "__CONTINUE__"
|
const val CONTINUE_ROOT = "__CONTINUE__"
|
||||||
const val DOWNLOADS_ROOT = "__DOWNLOADS__"
|
const val DOWNLOADS_ROOT = "__DOWNLOADS__"
|
||||||
const val LIBRARIES_ROOT = "__LIBRARIES__"
|
const val LIBRARIES_ROOT = "__LIBRARIES__"
|
||||||
|
const val RECENTLY_ROOT = "__RECENTLY__"
|
||||||
|
|||||||
+215
-55
@@ -120,6 +120,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
private var mShakeDetector: ShakeDetector? = null
|
private var mShakeDetector: ShakeDetector? = null
|
||||||
private var shakeSensorUnregisterTask:TimerTask? = null
|
private var shakeSensorUnregisterTask:TimerTask? = null
|
||||||
|
|
||||||
|
// These are used to prevent things running multiple times or simultaneously
|
||||||
|
private var isLoadingAndroidAutoItems:Boolean = false
|
||||||
|
|
||||||
|
// Cache latest search so it wont trigger again when returning from series for example
|
||||||
|
private var cachedSearch : String = ""
|
||||||
|
private var cachedSearchResults : MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Service related stuff
|
Service related stuff
|
||||||
*/
|
*/
|
||||||
@@ -977,6 +984,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
private val AUTO_MEDIA_ROOT = "/"
|
private val AUTO_MEDIA_ROOT = "/"
|
||||||
private val LIBRARIES_ROOT = "__LIBRARIES__"
|
private val LIBRARIES_ROOT = "__LIBRARIES__"
|
||||||
|
private val RECENTLY_ROOT = "__RECENTLY__"
|
||||||
private val DOWNLOADS_ROOT = "__DOWNLOADS__"
|
private val DOWNLOADS_ROOT = "__DOWNLOADS__"
|
||||||
private val CONTINUE_ROOT = "__CONTINUE__"
|
private val CONTINUE_ROOT = "__CONTINUE__"
|
||||||
private lateinit var browseTree:BrowseTree
|
private lateinit var browseTree:BrowseTree
|
||||||
@@ -1088,22 +1096,37 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
localBrowseItems += MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
localBrowseItems += MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||||
}
|
}
|
||||||
result.sendResult(localBrowseItems)
|
result.sendResult(localBrowseItems)
|
||||||
} else if (parentMediaId == LIBRARIES_ROOT || parentMediaId == AUTO_MEDIA_ROOT) {
|
} else if (parentMediaId == AUTO_MEDIA_ROOT) {
|
||||||
mediaManager.loadAndroidAutoItems {
|
Log.d(tag, "Trying to initialize browseTree.")
|
||||||
browseTree = BrowseTree(this, mediaManager.serverItemsInProgress, mediaManager.serverLibraries)
|
if (!this::browseTree.isInitialized) {
|
||||||
|
isLoadingAndroidAutoItems = true
|
||||||
|
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}")
|
Log.d(tag, "Found top menu item: ${item.description.title}")
|
||||||
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||||
|
}
|
||||||
|
Log.d(tag, "browseTree initialize and android auto loaded")
|
||||||
|
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||||
|
isLoadingAndroidAutoItems = false
|
||||||
|
Log.d(tag, "Starting personalization fetch")
|
||||||
|
mediaManager.populatePersonalizedDataForAllLibraries {}
|
||||||
}
|
}
|
||||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
|
||||||
}
|
}
|
||||||
|
} else if (parentMediaId == LIBRARIES_ROOT || parentMediaId == RECENTLY_ROOT) {
|
||||||
|
while (!this::browseTree.isInitialized) {}
|
||||||
|
val children = browseTree[parentMediaId]?.map { item ->
|
||||||
|
Log.d(tag, "[MENU: $parentMediaId] Showing list item ${item.description.title}")
|
||||||
|
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||||
|
}
|
||||||
|
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||||
} else if (mediaManager.getIsLibrary(parentMediaId)) { // Load library items for library
|
} else if (mediaManager.getIsLibrary(parentMediaId)) { // Load library items for library
|
||||||
Log.d(tag, "Loading items for library $parentMediaId")
|
Log.d(tag, "Loading items for library $parentMediaId")
|
||||||
val selectedLibrary = mediaManager.getLibrary(parentMediaId)
|
val selectedLibrary = mediaManager.getLibrary(parentMediaId)
|
||||||
if (selectedLibrary?.mediaType == "podcast") { // Podcasts are browseable
|
if (selectedLibrary?.mediaType == "podcast") { // Podcasts are browseable
|
||||||
mediaManager.loadLibraryItemsWithAudio(parentMediaId) { libraryItems ->
|
mediaManager.loadLibraryPodcasts(parentMediaId) { libraryItems ->
|
||||||
val children = libraryItems.map { libraryItem ->
|
val children = libraryItems?.map { libraryItem ->
|
||||||
val mediaDescription = libraryItem.getMediaDescription(null, ctx)
|
val mediaDescription = libraryItem.getMediaDescription(null, ctx)
|
||||||
MediaBrowserCompat.MediaItem(
|
MediaBrowserCompat.MediaItem(
|
||||||
mediaDescription,
|
mediaDescription,
|
||||||
@@ -1116,7 +1139,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
val children = mutableListOf(
|
val children = mutableListOf(
|
||||||
MediaBrowserCompat.MediaItem(
|
MediaBrowserCompat.MediaItem(
|
||||||
MediaDescriptionCompat.Builder()
|
MediaDescriptionCompat.Builder()
|
||||||
.setTitle("Library")
|
.setTitle("Authors")
|
||||||
.setMediaId("__LIBRARY__${parentMediaId}__AUTHORS")
|
.setMediaId("__LIBRARY__${parentMediaId}__AUTHORS")
|
||||||
.build(),
|
.build(),
|
||||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||||
@@ -1136,8 +1159,149 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if (mediaManager.getHasDiscovery(parentMediaId)) {
|
||||||
|
children.add(
|
||||||
|
MediaBrowserCompat.MediaItem(
|
||||||
|
MediaDescriptionCompat.Builder()
|
||||||
|
.setTitle("Discovery")
|
||||||
|
.setMediaId("__LIBRARY__${parentMediaId}__DISCOVERY")
|
||||||
|
.build(),
|
||||||
|
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||||
}
|
}
|
||||||
|
} else if (parentMediaId.startsWith(RECENTLY_ROOT)) {
|
||||||
|
Log.d(tag, "Browsing recently $parentMediaId")
|
||||||
|
val mediaIdParts = parentMediaId.split("__")
|
||||||
|
if (!mediaManager.getIsLibrary(mediaIdParts[2])) {
|
||||||
|
Log.d(tag, "${mediaIdParts[2]} is not library")
|
||||||
|
result.sendResult(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Log.d(tag, "Mediaparts: ${mediaIdParts.size} | $mediaIdParts")
|
||||||
|
if(mediaIdParts.size == 3) {
|
||||||
|
mediaManager.getLibraryRecentShelfs(mediaIdParts[2]) { availableShelfs ->
|
||||||
|
Log.d(tag, "Found ${availableShelfs.size} shelfs")
|
||||||
|
val children : MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||||
|
for (shelf in availableShelfs) {
|
||||||
|
if (shelf.type == "book") {
|
||||||
|
children.add(
|
||||||
|
MediaBrowserCompat.MediaItem(
|
||||||
|
MediaDescriptionCompat.Builder()
|
||||||
|
.setTitle("Books")
|
||||||
|
.setMediaId("${parentMediaId}__BOOK")
|
||||||
|
.build(),
|
||||||
|
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else if (shelf.type == "series") {
|
||||||
|
children.add(
|
||||||
|
MediaBrowserCompat.MediaItem(
|
||||||
|
MediaDescriptionCompat.Builder()
|
||||||
|
.setTitle("Series")
|
||||||
|
.setMediaId("${parentMediaId}__SERIES")
|
||||||
|
.build(),
|
||||||
|
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else if (shelf.type == "episode") {
|
||||||
|
children.add(
|
||||||
|
MediaBrowserCompat.MediaItem(
|
||||||
|
MediaDescriptionCompat.Builder()
|
||||||
|
.setTitle("Episodes")
|
||||||
|
.setMediaId("${parentMediaId}__EPISODE")
|
||||||
|
.build(),
|
||||||
|
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else if (shelf.type == "podcast") {
|
||||||
|
children.add(
|
||||||
|
MediaBrowserCompat.MediaItem(
|
||||||
|
MediaDescriptionCompat.Builder()
|
||||||
|
.setTitle("Podcast")
|
||||||
|
.setMediaId("${parentMediaId}__PODCAST")
|
||||||
|
.build(),
|
||||||
|
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else if (shelf.type == "authors") {
|
||||||
|
children.add(
|
||||||
|
MediaBrowserCompat.MediaItem(
|
||||||
|
MediaDescriptionCompat.Builder()
|
||||||
|
.setTitle("Authors")
|
||||||
|
.setMediaId("${parentMediaId}__AUTHORS")
|
||||||
|
.build(),
|
||||||
|
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||||
|
}
|
||||||
|
} else if (mediaIdParts.size == 4) {
|
||||||
|
mediaManager.getLibraryRecentShelfByType(mediaIdParts[2], mediaIdParts[3]) { shelf ->
|
||||||
|
if (shelf === null) {
|
||||||
|
result.sendResult(mutableListOf())
|
||||||
|
}else {
|
||||||
|
if (shelf.type == "book") {
|
||||||
|
val children = (shelf as LibraryShelfBookEntity).entities?.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, null, false)
|
||||||
|
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||||
|
}
|
||||||
|
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||||
|
}else if (shelf.type == "episode") {
|
||||||
|
val episodesWithRecentEpisode = (shelf as LibraryShelfEpisodeEntity).entities?.filter { libraryItem -> libraryItem.recentEpisode !== null }
|
||||||
|
val children = episodesWithRecentEpisode?.map { libraryItem ->
|
||||||
|
val podcast = libraryItem.media as Podcast
|
||||||
|
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.libraryId && it.episodeId == libraryItem.recentEpisode?.id }
|
||||||
|
|
||||||
|
// to show download icon
|
||||||
|
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.recentEpisode!!.id)
|
||||||
|
localLibraryItem?.let { lli ->
|
||||||
|
val localEpisode = (lli.media as Podcast).episodes?.find { it.serverEpisodeId == libraryItem.recentEpisode.id }
|
||||||
|
libraryItem.recentEpisode.localEpisodeId = localEpisode?.id
|
||||||
|
}
|
||||||
|
|
||||||
|
val description = libraryItem.recentEpisode.getMediaDescription(libraryItem, progress, ctx)
|
||||||
|
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||||
|
}
|
||||||
|
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||||
|
}else if (shelf.type == "podcast") {
|
||||||
|
val children = (shelf as LibraryShelfPodcastEntity).entities?.map { libraryItem ->
|
||||||
|
val mediaDescription = libraryItem.getMediaDescription(null, ctx)
|
||||||
|
MediaBrowserCompat.MediaItem(
|
||||||
|
mediaDescription,
|
||||||
|
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||||
|
}
|
||||||
|
else if (shelf.type == "series") {
|
||||||
|
val children = (shelf as LibraryShelfSeriesEntity).entities?.map { librarySeriesItem ->
|
||||||
|
val description = librarySeriesItem.getMediaDescription(null, ctx)
|
||||||
|
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||||
|
}
|
||||||
|
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||||
|
}
|
||||||
|
else if (shelf.type == "authors") {
|
||||||
|
val children = (shelf as LibraryShelfAuthorEntity).entities?.map { authorItem ->
|
||||||
|
val description = authorItem.getMediaDescription(null, ctx)
|
||||||
|
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||||
|
}
|
||||||
|
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result.sendResult(mutableListOf())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (parentMediaId.startsWith("__LIBRARY__")) {
|
} else if (parentMediaId.startsWith("__LIBRARY__")) {
|
||||||
Log.d(tag, "Browsing library $parentMediaId")
|
Log.d(tag, "Browsing library $parentMediaId")
|
||||||
val mediaIdParts = parentMediaId.split("__")
|
val mediaIdParts = parentMediaId.split("__")
|
||||||
@@ -1145,7 +1309,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
MediaIdParts for Library
|
MediaIdParts for Library
|
||||||
1: LIBRARY
|
1: LIBRARY
|
||||||
2: mediaId for library
|
2: mediaId for library
|
||||||
3: Browsing style (AUTHORS, AUTHOR, AUTHOR_SERIES, SERIES_LIST, SERIES, COLLECTION, COLLECTIONS)
|
3: Browsing style (AUTHORS, AUTHOR, AUTHOR_SERIES, SERIES_LIST, SERIES, COLLECTION, COLLECTIONS, DISCOVERY)
|
||||||
4:
|
4:
|
||||||
- Paging: SERIES_LIST, AUTHORS
|
- Paging: SERIES_LIST, AUTHORS
|
||||||
- SeriesId: SERIES
|
- SeriesId: SERIES
|
||||||
@@ -1338,7 +1502,20 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||||
}
|
}
|
||||||
} else {
|
} else if (mediaIdParts[3] == "DISCOVERY") {
|
||||||
|
Log.d(tag, "Loading discovery from library ${mediaIdParts[2]}")
|
||||||
|
mediaManager.loadLibraryDiscoveryBooksWithAudio(mediaIdParts[2]) { libraryItems ->
|
||||||
|
Log.d(tag, "Received ${libraryItems.size} libraryItems for discovery")
|
||||||
|
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 {
|
||||||
result.sendResult(null)
|
result.sendResult(null)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1351,54 +1528,34 @@ 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()
|
||||||
Log.d(tag, "Search bundle: $extras")
|
if (cachedSearch != query) {
|
||||||
var foundBooks: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
Log.d(tag, "Search bundle: $extras")
|
||||||
var foundPodcasts: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
var foundBooks: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||||
var foundSeries: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
var foundPodcasts: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||||
var foundAuthors: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
var foundSeries: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||||
|
var foundAuthors: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||||
|
|
||||||
mediaManager.serverLibraries.forEach { serverLibrary ->
|
mediaManager.serverLibraries.forEach { serverLibrary ->
|
||||||
runBlocking{
|
runBlocking {
|
||||||
val searchResult = mediaManager.searchLocalCache(serverLibrary.id, query)
|
// Skip searching library if it doesn't have any audio files
|
||||||
Log.d(tag, "onSearch: SearchResult: $searchResult")
|
if (serverLibrary.stats?.numAudioTracks == 0) return@runBlocking
|
||||||
if (searchResult === null) return@runBlocking
|
val searchResult = mediaManager.doSearch(serverLibrary.id, query)
|
||||||
if (searchResult.book !== null && searchResult.book!!.isNotEmpty()) {
|
for (resultData in searchResult.entries.iterator()) {
|
||||||
Log.d(tag, "onSearch: found ${searchResult.book!!.size} books")
|
when (resultData.key) {
|
||||||
val children = searchResult.book!!.map { bookResult ->
|
"book" -> foundBooks.addAll(resultData.value)
|
||||||
val libraryItem = bookResult.libraryItem
|
"series" -> foundSeries.addAll(resultData.value)
|
||||||
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
|
"authors" -> foundAuthors.addAll(resultData.value)
|
||||||
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
|
"podcast" -> foundPodcasts.addAll(resultData.value)
|
||||||
libraryItem.localLibraryItemId = localLibraryItem?.id
|
}
|
||||||
val description = libraryItem.getMediaDescription(progress, ctx, null, null, "Books (${serverLibrary.name})")
|
|
||||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
|
||||||
}
|
}
|
||||||
foundBooks.addAll(children)
|
|
||||||
}
|
}
|
||||||
if (searchResult.series !== null && searchResult.series!!.isNotEmpty()) {
|
|
||||||
Log.d(tag, "onSearch: found ${searchResult.series!!.size} series")
|
|
||||||
val children = searchResult.series!!.map { seriesResult ->
|
|
||||||
val seriesItem = seriesResult.series
|
|
||||||
seriesItem.books = seriesResult.books as MutableList<LibraryItem>
|
|
||||||
val description = seriesItem.getMediaDescription(null, ctx, "Series (${serverLibrary.name})")
|
|
||||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
|
||||||
}
|
|
||||||
foundSeries.addAll(children)
|
|
||||||
}
|
|
||||||
if (searchResult.authors !== null && searchResult.authors!!.isNotEmpty()) {
|
|
||||||
Log.d(tag, "onSearch: found ${searchResult.authors!!.size} authors")
|
|
||||||
val children = searchResult.authors!!.map { authorItem ->
|
|
||||||
val description = authorItem.getMediaDescription(null, ctx, "Authors (${serverLibrary.name})")
|
|
||||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
|
||||||
}
|
|
||||||
foundAuthors.addAll(children)
|
|
||||||
}
|
|
||||||
Log.d(tag, "onSearch: Library ${serverLibrary.id} processed")
|
|
||||||
}
|
}
|
||||||
Log.d(tag, "onSearch: Library ${serverLibrary.id} scanned")
|
foundBooks.addAll(foundSeries)
|
||||||
|
foundBooks.addAll(foundAuthors)
|
||||||
|
cachedSearchResults = foundBooks
|
||||||
}
|
}
|
||||||
foundBooks.addAll(foundSeries)
|
result.sendResult(cachedSearchResults)
|
||||||
foundBooks.addAll(foundAuthors)
|
cachedSearch = query
|
||||||
result.sendResult(foundBooks as MutableList<MediaBrowserCompat.MediaItem>?)
|
|
||||||
Log.d(tag, "onSearch: Done")
|
Log.d(tag, "onSearch: Done")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1462,6 +1619,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
hasNetworkConnectivity = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
hasNetworkConnectivity = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
Log.i(tag, "Network capabilities changed. hasNetworkConnectivity=$hasNetworkConnectivity | isUnmeteredNetwork=$isUnmeteredNetwork")
|
Log.i(tag, "Network capabilities changed. hasNetworkConnectivity=$hasNetworkConnectivity | isUnmeteredNetwork=$isUnmeteredNetwork")
|
||||||
clientEventEmitter?.onNetworkMeteredChanged(isUnmeteredNetwork)
|
clientEventEmitter?.onNetworkMeteredChanged(isUnmeteredNetwork)
|
||||||
|
if (hasNetworkConnectivity) {
|
||||||
|
// TODO: Trigger android auto loading if it is not loaded previously
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -162,6 +162,23 @@ class ApiHandler(var ctx:Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getLibraryPersonalized(libraryItemId:String, cb: (List<LibraryShelfType>?) -> Unit) {
|
||||||
|
getRequest("/api/libraries/$libraryItemId/personalized", null, null) {
|
||||||
|
if (it.has("error")) {
|
||||||
|
Log.e(tag, it.getString("error") ?: "getLibraryStats Failed")
|
||||||
|
cb(null)
|
||||||
|
} else {
|
||||||
|
val items = mutableListOf<LibraryShelfType>()
|
||||||
|
val array = it.getJSONArray("value")
|
||||||
|
for (i in 0 until array.length()) {
|
||||||
|
val item = jacksonMapper.readValue<LibraryShelfType>(array.get(i).toString())
|
||||||
|
items.add(item)
|
||||||
|
}
|
||||||
|
cb(items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getLibraryItem(libraryItemId:String, cb: (LibraryItem?) -> Unit) {
|
fun getLibraryItem(libraryItemId:String, cb: (LibraryItem?) -> Unit) {
|
||||||
getRequest("/api/items/$libraryItemId?expanded=1", null, null) {
|
getRequest("/api/items/$libraryItemId?expanded=1", null, null) {
|
||||||
if (it.has("error")) {
|
if (it.has("error")) {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<!-- drawable/clock_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,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.5 2,12A10,10 0 0,1 12,2M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z" /></vector>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 798 B |
Binary file not shown.
|
After Width: | Height: | Size: 532 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<!-- drawable/clock_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,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.5 2,12A10,10 0 0,1 12,2M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z" /></vector>
|
||||||
Reference in New Issue
Block a user