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:
parent
d4f74784ba
commit
9f8ce49d70
3 changed files with 38 additions and 27 deletions
|
@ -8,6 +8,7 @@ at the cost of longer loading times
|
||||||
- Added support for sort tags [#172, dependent on this feature]
|
- 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 support for date tags, including more fine-grained dates [#159, dependent on this feature]
|
||||||
- Added Last Added sorting
|
- Added Last Added sorting
|
||||||
|
- Search now takes sort tags and file names in account [#184]
|
||||||
|
|
||||||
## 2.5.0
|
## 2.5.0
|
||||||
|
|
||||||
|
|
|
@ -292,18 +292,18 @@ data class Genre(override val rawName: String?, override val songs: List<Song>)
|
||||||
/**
|
/**
|
||||||
* An ISO-8601/RFC 3339 Date.
|
* An ISO-8601/RFC 3339 Date.
|
||||||
*
|
*
|
||||||
* Unlike a typical Date within the standard library, this class is simply a 1:1 mapping between
|
* Unlike a typical Date within the standard library, this class is simply a 1:1 mapping between the
|
||||||
* the tag date format of ID3v2 and (presumably) the Vorbis date format, implementing only format
|
* tag date format of ID3v2 and (presumably) the Vorbis date format, implementing only format
|
||||||
* validation and excluding advanced or locale-specific date functionality..
|
* 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
|
* The reasoning behind Date is that Auxio cannot trust any kind of metadata date to actually make
|
||||||
* make sense in a calendar, due to bad tagging, locale-specific issues, or simply from the
|
* sense in a calendar, due to bad tagging, locale-specific issues, or simply from the limited
|
||||||
* limited nature of tag formats. Thus, it's better to use an analogous data structure that
|
* nature of tag formats. Thus, it's better to use an analogous data structure that will not mangle
|
||||||
* will not mangle or reject valid-ish dates.
|
* or reject valid-ish dates.
|
||||||
*
|
*
|
||||||
* Date instances are immutable and their internal implementation is hidden. To instantiate one,
|
* Date instances are immutable and their internal implementation is hidden. To instantiate one, use
|
||||||
* use [fromYear] or [parseTimestamp]. The string representation of a Date is RFC 3339, with
|
* [fromYear] or [parseTimestamp]. The string representation of a Date is RFC 3339, with granular
|
||||||
* granular position depending on the presence of particular tokens.
|
* position depending on the presence of particular tokens.
|
||||||
*
|
*
|
||||||
* Please, *Do not use this for anything important related to time.* I cannot stress this enough.
|
* 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.
|
* This class will blow up if you try to do that.
|
||||||
|
|
|
@ -29,12 +29,15 @@ import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
import org.oxycblt.auxio.ui.DisplayMode
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.ui.recycler.Header
|
import org.oxycblt.auxio.ui.recycler.Header
|
||||||
import org.oxycblt.auxio.ui.recycler.Item
|
import org.oxycblt.auxio.ui.recycler.Item
|
||||||
|
import org.oxycblt.auxio.util.TaskGuard
|
||||||
import org.oxycblt.auxio.util.application
|
import org.oxycblt.auxio.util.application
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
@ -56,11 +59,13 @@ class SearchViewModel(application: Application) :
|
||||||
get() = settings.searchFilterMode
|
get() = settings.searchFilterMode
|
||||||
|
|
||||||
private var lastQuery: String? = null
|
private var lastQuery: String? = null
|
||||||
|
private var guard = TaskGuard()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use [query] to perform a search of the music library. Will push results to [searchResults].
|
* Use [query] to perform a search of the music library. Will push results to [searchResults].
|
||||||
*/
|
*/
|
||||||
fun search(query: String?) {
|
fun search(query: String?) {
|
||||||
|
val handle = guard.newHandle()
|
||||||
lastQuery = query
|
lastQuery = query
|
||||||
|
|
||||||
val library = musicStore.library
|
val library = musicStore.library
|
||||||
|
@ -80,33 +85,34 @@ class SearchViewModel(application: Application) :
|
||||||
// Note: a filter mode of null means to not filter at all.
|
// Note: a filter mode of null means to not filter at all.
|
||||||
|
|
||||||
if (filterMode == null || filterMode == DisplayMode.SHOW_ARTISTS) {
|
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.add(Header(-1, R.string.lbl_artists))
|
||||||
results.addAll(sort.artists(artists))
|
results.addAll(sort.artists(artists))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterMode == null || filterMode == DisplayMode.SHOW_ALBUMS) {
|
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.add(Header(-2, R.string.lbl_albums))
|
||||||
results.addAll(sort.albums(albums))
|
results.addAll(sort.albums(albums))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterMode == null || filterMode == DisplayMode.SHOW_GENRES) {
|
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.add(Header(-3, R.string.lbl_genres))
|
||||||
results.addAll(sort.genres(genres))
|
results.addAll(sort.genres(genres))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterMode == null || filterMode == DisplayMode.SHOW_SONGS) {
|
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.add(Header(-4, R.string.lbl_songs))
|
||||||
results.addAll(sort.songs(songs))
|
results.addAll(sort.songs(songs))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
guard.yield(handle)
|
||||||
_searchResults.value = results
|
_searchResults.value = results
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -131,20 +137,24 @@ class SearchViewModel(application: Application) :
|
||||||
search(lastQuery)
|
search(lastQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Searches the song list by the normalized name, then the sort name, and then the file name. */
|
||||||
* Shortcut that will run a ignoreCase filter on a list and only return a value if the resulting
|
private fun List<Song>.filterSongsBy(value: String) =
|
||||||
* list is empty.
|
baseFilterBy(value) { it.path.name.contains(value) }.ifEmpty { null }
|
||||||
*/
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
private fun Music.resolveNameNormalized(context: Context): String {
|
||||||
|
|
Loading…
Reference in a new issue