music: update search results as well

This commit is contained in:
Alexander Capehart 2024-04-14 12:15:16 -06:00
parent 583e984c70
commit 02b7acd1c5
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
4 changed files with 56 additions and 40 deletions

View file

@ -20,6 +20,7 @@ package org.oxycblt.auxio.music.service
import android.content.Context import android.content.Context
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.session.MediaSession.ControllerInfo
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.min import kotlin.math.min
@ -51,11 +52,14 @@ constructor(
) : MusicRepository.UpdateListener { ) : MusicRepository.UpdateListener {
private val browserJob = Job() private val browserJob = Job()
private val searchScope = CoroutineScope(browserJob + Dispatchers.Default) private val searchScope = CoroutineScope(browserJob + Dispatchers.Default)
private val searchSubscribers = mutableMapOf<ControllerInfo, String>()
private val searchResults = mutableMapOf<String, Deferred<SearchEngine.Items>>() private val searchResults = mutableMapOf<String, Deferred<SearchEngine.Items>>()
private var invalidator: Invalidator? = null private var invalidator: Invalidator? = null
interface Invalidator { interface Invalidator {
fun invalidate(ids: List<String>) fun invalidate(ids: List<String>)
fun invalidate(controller: ControllerInfo, query: String, itemCount: Int)
} }
fun attach(invalidator: Invalidator) { fun attach(invalidator: Invalidator) {
@ -93,9 +97,16 @@ constructor(
if (invalidateSearch) { if (invalidateSearch) {
for (entry in searchResults.entries) { for (entry in searchResults.entries) {
entry.value.cancel() searchResults[entry.key]?.cancel()
} }
searchResults.clear() searchResults.clear()
for (entry in searchSubscribers.entries) {
if (searchResults[entry.value] != null) {
continue
}
searchResults[entry.value] = searchTo(entry.value)
}
} }
} }
@ -190,7 +201,8 @@ constructor(
is Genre -> { is Genre -> {
val artists = GENRE_ARTISTS_SORT.artists(item.artists) val artists = GENRE_ARTISTS_SORT.artists(item.artists)
val songs = listSettings.genreSongSort.songs(item.songs) val songs = listSettings.genreSongSort.songs(item.songs)
artists.map { it.toMediaItem(context) } + songs.map { it.toMediaItem(context, null) } artists.map { it.toMediaItem(context) } +
songs.map { it.toMediaItem(context, null) }
} }
is Playlist -> { is Playlist -> {
item.songs.map { it.toMediaItem(context, item) } item.songs.map { it.toMediaItem(context, item) }
@ -200,20 +212,17 @@ constructor(
} }
} }
suspend fun prepareSearch(query: String): Int { suspend fun prepareSearch(query: String, controller: ControllerInfo) {
val deviceLibrary = musicRepository.deviceLibrary searchSubscribers[controller] = query
val userLibrary = musicRepository.userLibrary val existing = searchResults[query]
if (deviceLibrary == null || userLibrary == null) { if (existing == null) {
return 0 val new = searchTo(query)
searchResults[query] = new
new.await()
} else {
val items = existing.await()
invalidator?.invalidate(controller, query, items.count())
} }
if (query.isEmpty()) {
return 0
}
val deferred = searchTo(query, deviceLibrary, userLibrary)
searchResults[query] = deferred
return deferred.await().count()
} }
suspend fun getSearchResult( suspend fun getSearchResult(
@ -221,22 +230,8 @@ constructor(
page: Int, page: Int,
pageSize: Int, pageSize: Int,
): List<MediaItem>? { ): List<MediaItem>? {
val deviceLibrary = musicRepository.deviceLibrary val deferred = searchResults[query] ?: searchTo(query).also { searchResults[query] = it }
val userLibrary = musicRepository.userLibrary return deferred.await().concat().paginate(page, pageSize)
if (deviceLibrary == null || userLibrary == null) {
return listOf()
}
if (query.isEmpty()) {
return listOf()
}
val existing = searchResults[query]
if (existing != null) {
return existing.await().concat().paginate(page, pageSize)
}
return searchTo(query, deviceLibrary, userLibrary).await().concat().paginate(page, pageSize)
} }
private fun SearchEngine.Items.concat(): MutableList<MediaItem> { private fun SearchEngine.Items.concat(): MutableList<MediaItem> {
@ -279,8 +274,13 @@ constructor(
return count return count
} }
private fun searchTo(query: String, deviceLibrary: DeviceLibrary, userLibrary: UserLibrary) = private fun searchTo(query: String) =
searchScope.async { searchScope.async {
if (query.isEmpty()) {
return@async SearchEngine.Items()
}
val deviceLibrary = musicRepository.deviceLibrary ?: return@async SearchEngine.Items()
val userLibrary = musicRepository.userLibrary ?: return@async SearchEngine.Items()
val items = val items =
SearchEngine.Items( SearchEngine.Items(
deviceLibrary.songs, deviceLibrary.songs,
@ -288,7 +288,13 @@ constructor(
deviceLibrary.artists, deviceLibrary.artists,
deviceLibrary.genres, deviceLibrary.genres,
userLibrary.playlists) userLibrary.playlists)
searchEngine.search(items, query) val results = searchEngine.search(items, query)
for (entry in searchSubscribers.entries) {
if (entry.value == query) {
invalidator?.invalidate(entry.key, query, results.count())
}
}
results
} }
private fun List<MediaItem>.paginate(page: Int, pageSize: Int): List<MediaItem>? { private fun List<MediaItem>.paginate(page: Int, pageSize: Int): List<MediaItem>? {

View file

@ -224,8 +224,8 @@ constructor(
): ListenableFuture<LibraryResult<Void>> = ): ListenableFuture<LibraryResult<Void>> =
waitScope waitScope
.async { .async {
val count = mediaItemBrowser.prepareSearch(query) mediaItemBrowser.prepareSearch(query, browser)
session.notifySearchResultChanged(browser, query, count, params) // Invalidator will send the notify result
LibraryResult.ofVoid() LibraryResult.ofVoid()
} }
.asListenableFuture() .asListenableFuture()
@ -260,4 +260,12 @@ constructor(
mediaSession.notifyChildrenChanged(id, Int.MAX_VALUE, null) mediaSession.notifyChildrenChanged(id, Int.MAX_VALUE, null)
} }
} }
override fun invalidate(
controller: MediaSession.ControllerInfo,
query: String,
itemCount: Int
) {
mediaSession.notifySearchResultChanged(controller, query, itemCount, null)
}
} }

View file

@ -56,11 +56,11 @@ interface SearchEngine {
* @param playlists A list of [Playlist], null if empty. * @param playlists A list of [Playlist], null if empty.
*/ */
data class Items( data class Items(
val songs: Collection<Song>?, val songs: Collection<Song>? = null,
val albums: Collection<Album>?, val albums: Collection<Album>? = null,
val artists: Collection<Artist>?, val artists: Collection<Artist>? = null,
val genres: Collection<Genre>?, val genres: Collection<Genre>? = null,
val playlists: Collection<Playlist>? val playlists: Collection<Playlist>? = null
) )
} }

View file

@ -0,0 +1,2 @@
package org.oxycblt.auxio.tasker