service: add search functionality

I cannot tell if this actually works yet.
This commit is contained in:
Alexander Capehart 2024-04-09 22:28:32 -06:00
parent e9a4b99aa5
commit 48275c4698
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47

View file

@ -109,6 +109,7 @@ import org.oxycblt.auxio.playback.state.RawQueue
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.playback.state.ShuffleMode import org.oxycblt.auxio.playback.state.ShuffleMode
import org.oxycblt.auxio.playback.state.StateAck import org.oxycblt.auxio.playback.state.StateAck
import org.oxycblt.auxio.search.SearchEngine
import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -157,6 +158,11 @@ class AuxioService :
@Inject lateinit var widgetComponent: WidgetComponent @Inject lateinit var widgetComponent: WidgetComponent
@Inject lateinit var bitmapLoader: NeoBitmapLoader @Inject lateinit var bitmapLoader: NeoBitmapLoader
@Inject lateinit var searchEngine: SearchEngine
private var searchResultsCache = mutableMapOf<String, SearchEngine.Items>()
private var searchScope = CoroutineScope(serviceJob + Dispatchers.Default)
private var searchJob: Job? = null
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -692,10 +698,15 @@ class AuxioService :
} }
override fun onMusicChanges(changes: MusicRepository.Changes) { override fun onMusicChanges(changes: MusicRepository.Changes) {
if (changes.deviceLibrary && musicRepository.deviceLibrary != null) { if (changes.deviceLibrary) {
// We now have a library, see if we have anything we need to do. if (musicRepository.deviceLibrary != null) {
logD("Library obtained, requesting action") // We now have a library, see if we have anything we need to do.
playbackManager.requestAction(this) logD("Library obtained, requesting action")
playbackManager.requestAction(this)
}
// Invalidate anything we searched prior.
searchResultsCache.clear()
searchJob?.cancel()
} }
} }
@ -720,7 +731,6 @@ class AuxioService :
session: MediaSession, session: MediaSession,
controller: MediaSession.ControllerInfo controller: MediaSession.ControllerInfo
): ConnectionResult { ): ConnectionResult {
logD(Exception().stackTraceToString())
val sessionCommands = val sessionCommands =
ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon() ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon()
.add(SessionCommand(ACTION_INC_REPEAT_MODE, Bundle.EMPTY)) .add(SessionCommand(ACTION_INC_REPEAT_MODE, Bundle.EMPTY))
@ -738,9 +748,7 @@ class AuxioService :
browser: MediaSession.ControllerInfo, browser: MediaSession.ControllerInfo,
params: LibraryParams? params: LibraryParams?
): ListenableFuture<LibraryResult<MediaItem>> { ): ListenableFuture<LibraryResult<MediaItem>> {
val result = val result = LibraryResult.ofItem(ExternalUID.Category.ROOT.toMediaItem(this), params)
LibraryResult.ofItem(
ExternalUID.Category.ROOT.toMediaItem(this), LibraryParams.Builder().build())
return Futures.immediateFuture(result) return Futures.immediateFuture(result)
} }
@ -764,15 +772,18 @@ class AuxioService :
val deviceLibrary = musicRepository.deviceLibrary val deviceLibrary = musicRepository.deviceLibrary
val userLibrary = musicRepository.userLibrary val userLibrary = musicRepository.userLibrary
if (deviceLibrary == null || userLibrary == null) { if (deviceLibrary == null || userLibrary == null) {
return Futures.immediateFuture( return Futures.immediateFuture(LibraryResult.ofItemList(emptyList(), params))
LibraryResult.ofItemList(emptyList(), LibraryParams.Builder().build()))
} }
val items = val items =
getMediaItemList(parentId, deviceLibrary, userLibrary) getMediaItemList(parentId, deviceLibrary, userLibrary)
?: return Futures.immediateFuture( ?: return Futures.immediateFuture(
LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)) LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE))
val result = LibraryResult.ofItemList(items, LibraryParams.Builder().build()) val paginatedItems =
items.paginate(page, pageSize)
?: return Futures.immediateFuture(
LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE))
val result = LibraryResult.ofItemList(paginatedItems, params)
return Futures.immediateFuture(result) return Futures.immediateFuture(result)
} }
@ -833,6 +844,128 @@ class AuxioService :
} }
} }
override fun onSearch(
session: MediaLibrarySession,
browser: MediaSession.ControllerInfo,
query: String,
params: LibraryParams?
): ListenableFuture<LibraryResult<Void>> {
val deviceLibrary = musicRepository.deviceLibrary
val userLibrary = musicRepository.userLibrary
if (deviceLibrary == null || userLibrary == null) {
return Futures.immediateFuture(
LibraryResult.ofError(LibraryResult.RESULT_ERROR_INVALID_STATE))
}
if (query.isEmpty()) {
return Futures.immediateFuture(LibraryResult.ofVoid())
}
val future = SettableFuture.create<LibraryResult<Void>>()
searchTo(query, deviceLibrary, userLibrary) { future.set(LibraryResult.ofVoid()) }
return Futures.immediateFuture(LibraryResult.ofVoid())
}
override fun onGetSearchResult(
session: MediaLibrarySession,
browser: MediaSession.ControllerInfo,
query: String,
page: Int,
pageSize: Int,
params: LibraryParams?
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
val deviceLibrary = musicRepository.deviceLibrary
val userLibrary = musicRepository.userLibrary
if (deviceLibrary == null || userLibrary == null) {
return Futures.immediateFuture(
LibraryResult.ofError(LibraryResult.RESULT_ERROR_INVALID_STATE))
}
if (query.isEmpty()) {
return Futures.immediateFuture(LibraryResult.ofItemList(emptyList(), params))
}
val items = searchResultsCache[query]
if (items != null) {
val concatenatedItems = items.concat()
val paginatedItems =
concatenatedItems.paginate(page, pageSize)
?: return Futures.immediateFuture(
LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE))
val result = LibraryResult.ofItemList(paginatedItems, params)
return Futures.immediateFuture(result)
}
val future = SettableFuture.create<LibraryResult<ImmutableList<MediaItem>>>()
searchTo(query, deviceLibrary, userLibrary) {
val concatenatedItems = it.concat()
val paginatedItems = concatenatedItems.paginate(page, pageSize) ?: return@searchTo
val result = LibraryResult.ofItemList(paginatedItems, params)
future.set(result)
}
return future
}
private fun SearchEngine.Items.concat(): MutableList<MediaItem> {
val music = mutableListOf<MediaItem>()
if (songs != null) {
music.addAll(songs.map { it.toMediaItem(this@AuxioService, null) })
}
if (albums != null) {
music.addAll(albums.map { it.toMediaItem(this@AuxioService, null) })
}
if (artists != null) {
music.addAll(artists.map { it.toMediaItem(this@AuxioService, null) })
}
if (genres != null) {
music.addAll(genres.map { it.toMediaItem(this@AuxioService) })
}
if (playlists != null) {
music.addAll(playlists.map { it.toMediaItem(this@AuxioService) })
}
return music
}
private fun searchTo(
query: String,
deviceLibrary: DeviceLibrary,
userLibrary: UserLibrary,
cb: (SearchEngine.Items) -> Unit
) {
// TODO: Queue up searches rather than clobbering the last one
searchJob?.cancel()
searchJob =
searchScope.launch {
val items =
SearchEngine.Items(
deviceLibrary.songs,
deviceLibrary.albums,
deviceLibrary.artists,
deviceLibrary.genres,
userLibrary.playlists)
val results = searchEngine.search(items, query)
searchResultsCache[query] = results
cb(results)
}
}
private fun List<MediaItem>.paginate(page: Int, pageSize: Int): List<MediaItem>? {
if (page == Int.MAX_VALUE) {
// I think if someone requests this page it more or less implies that I should
// return all of the pages.
return this
}
val start = page * pageSize
val end = (page + 1) * pageSize
if (start !in indices || end - 1 !in indices) {
// Assume that everything out of bounds is a weird magic value implying that it
// actually wants all of the pages. This will not backfire at all.
return this
}
return subList(page * pageSize, (page + 1) * pageSize).toMutableList()
}
override fun onSetMediaItems( override fun onSetMediaItems(
mediaSession: MediaSession, mediaSession: MediaSession,
controller: MediaSession.ControllerInfo, controller: MediaSession.ControllerInfo,
@ -840,14 +973,11 @@ class AuxioService :
startIndex: Int, startIndex: Int,
startPositionMs: Long startPositionMs: Long
): ListenableFuture<MediaSession.MediaItemsWithStartPosition> { ): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
val deviceLibrary =
musicRepository.deviceLibrary
?: return Futures.immediateFailedFuture(Exception("Invalid state"))
val result = val result =
if (mediaItems.size > 1) { if (mediaItems.size > 1) {
playMediaItemSelection(mediaItems, startIndex, deviceLibrary) playMediaItemSelection(mediaItems, startIndex)
} else { } else {
playSingleMediaItem(mediaItems.first(), deviceLibrary) playSingleMediaItem(mediaItems.first())
} }
return if (result) { return if (result) {
// This will not actually do anything to the player, I patched that out // This will not actually do anything to the player, I patched that out
@ -858,11 +988,8 @@ class AuxioService :
} }
} }
private fun playMediaItemSelection( private fun playMediaItemSelection(mediaItems: List<MediaItem>, startIndex: Int): Boolean {
mediaItems: List<MediaItem>, val deviceLibrary = musicRepository.deviceLibrary ?: return false
startIndex: Int,
deviceLibrary: DeviceLibrary
): Boolean {
val targetSong = mediaItems.getOrNull(startIndex)?.toSong(deviceLibrary) val targetSong = mediaItems.getOrNull(startIndex)?.toSong(deviceLibrary)
val songs = mediaItems.mapNotNull { it.toSong(deviceLibrary) } val songs = mediaItems.mapNotNull { it.toSong(deviceLibrary) }
var index = startIndex var index = startIndex
@ -875,7 +1002,7 @@ class AuxioService :
return true return true
} }
private fun playSingleMediaItem(mediaItem: MediaItem, deviceLibrary: DeviceLibrary): Boolean { private fun playSingleMediaItem(mediaItem: MediaItem): Boolean {
val uid = ExternalUID.fromString(mediaItem.mediaId) ?: return false val uid = ExternalUID.fromString(mediaItem.mediaId) ?: return false
val music: Music val music: Music
var parent: MusicParent? = null var parent: MusicParent? = null