search: add sort and file name to comparison

Make the search algorithm take in account the raw sort name and file
name when searching.

This allows the user to search for a particular song without typing in
a unicode/non-ideal title, instead typing in a latinized sort name or
suitable file name.

This does make Auxio's search a bit more fuzzy, but it still gets the
job done.

Resolves #184.
Related to #172.
This commit is contained in:
OxygenCobalt 2022-07-15 11:29:18 -06:00
parent d4f74784ba
commit 9f8ce49d70
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
3 changed files with 38 additions and 27 deletions

View file

@ -8,6 +8,7 @@ at the cost of longer loading times
- Added support for sort tags [#172, dependent on this feature]
- Added support for date tags, including more fine-grained dates [#159, dependent on this feature]
- Added Last Added sorting
- Search now takes sort tags and file names in account [#184]
## 2.5.0

View file

@ -292,18 +292,18 @@ data class Genre(override val rawName: String?, override val songs: List<Song>)
/**
* An ISO-8601/RFC 3339 Date.
*
* Unlike a typical Date within the standard library, this class is simply a 1:1 mapping between
* the tag date format of ID3v2 and (presumably) the Vorbis date format, implementing only format
* Unlike a typical Date within the standard library, this class is simply a 1:1 mapping between the
* tag date format of ID3v2 and (presumably) the Vorbis date format, implementing only format
* validation and excluding advanced or locale-specific date functionality..
*
* The reasoning behind Date is that Auxio cannot trust any kind of metadata date to actually
* make sense in a calendar, due to bad tagging, locale-specific issues, or simply from the
* limited nature of tag formats. Thus, it's better to use an analogous data structure that
* will not mangle or reject valid-ish dates.
*
* Date instances are immutable and their internal implementation is hidden. To instantiate one,
* use [fromYear] or [parseTimestamp]. The string representation of a Date is RFC 3339, with
* granular position depending on the presence of particular tokens.
* The reasoning behind Date is that Auxio cannot trust any kind of metadata date to actually make
* sense in a calendar, due to bad tagging, locale-specific issues, or simply from the limited
* nature of tag formats. Thus, it's better to use an analogous data structure that will not mangle
* or reject valid-ish dates.
*
* Date instances are immutable and their internal implementation is hidden. To instantiate one, use
* [fromYear] or [parseTimestamp]. The string representation of a Date is RFC 3339, with granular
* position depending on the presence of particular tokens.
*
* Please, *Do not use this for anything important related to time.* I cannot stress this enough.
* This class will blow up if you try to do that.

View file

@ -29,12 +29,15 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.recycler.Header
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.TaskGuard
import org.oxycblt.auxio.util.application
import org.oxycblt.auxio.util.logD
@ -56,11 +59,13 @@ class SearchViewModel(application: Application) :
get() = settings.searchFilterMode
private var lastQuery: String? = null
private var guard = TaskGuard()
/**
* Use [query] to perform a search of the music library. Will push results to [searchResults].
*/
fun search(query: String?) {
val handle = guard.newHandle()
lastQuery = query
val library = musicStore.library
@ -80,33 +85,34 @@ class SearchViewModel(application: Application) :
// Note: a filter mode of null means to not filter at all.
if (filterMode == null || filterMode == DisplayMode.SHOW_ARTISTS) {
library.artists.filterByOrNull(query)?.let { artists ->
library.artists.filterParentsBy(query)?.let { artists ->
results.add(Header(-1, R.string.lbl_artists))
results.addAll(sort.artists(artists))
}
}
if (filterMode == null || filterMode == DisplayMode.SHOW_ALBUMS) {
library.albums.filterByOrNull(query)?.let { albums ->
library.albums.filterParentsBy(query)?.let { albums ->
results.add(Header(-2, R.string.lbl_albums))
results.addAll(sort.albums(albums))
}
}
if (filterMode == null || filterMode == DisplayMode.SHOW_GENRES) {
library.genres.filterByOrNull(query)?.let { genres ->
library.genres.filterParentsBy(query)?.let { genres ->
results.add(Header(-3, R.string.lbl_genres))
results.addAll(sort.genres(genres))
}
}
if (filterMode == null || filterMode == DisplayMode.SHOW_SONGS) {
library.songs.filterByOrNull(query)?.let { songs ->
library.songs.filterSongsBy(query)?.let { songs ->
results.add(Header(-4, R.string.lbl_songs))
results.addAll(sort.songs(songs))
}
}
guard.yield(handle)
_searchResults.value = results
}
}
@ -131,20 +137,24 @@ class SearchViewModel(application: Application) :
search(lastQuery)
}
/**
* Shortcut that will run a ignoreCase filter on a list and only return a value if the resulting
* list is empty.
*/
private fun <T : Music> List<T>.filterByOrNull(value: String): List<T>? {
val filtered = filter {
// Compare normalized names, which are names with unicode characters that are
// normalized to their non-unicode forms. This is just for quality-of-life,
// and I hope it doesn't bork search functionality for other languages.
it.resolveNameNormalized(application).contains(value, ignoreCase = true) ||
it.resolveNameNormalized(application).contains(value, ignoreCase = true)
}
/** Searches the song list by the normalized name, then the sort name, and then the file name. */
private fun List<Song>.filterSongsBy(value: String) =
baseFilterBy(value) { it.path.name.contains(value) }.ifEmpty { null }
return filtered.ifEmpty { null }
/** Searches a list of parents by the normalized name, and then the sort name. */
private fun <T : MusicParent> List<T>.filterParentsBy(value: String) =
baseFilterBy(value) { false }.ifEmpty { null }
private inline fun <T : Music> List<T>.baseFilterBy(
value: String,
additional: (T) -> Boolean
) = filter {
// The basic comparison is first by the *normalized* name, as that allows a non-unicode
// search to match with some unicode characters. If that fails, we if there is a sort name
// we can leverage, as those are often used to make non-unicode variants of unicode titles.
it.resolveNameNormalized(application).contains(value, ignoreCase = true) ||
it.rawSortName?.contains(value, ignoreCase = true) == true ||
additional(it)
}
private fun Music.resolveNameNormalized(context: Context): String {