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 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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue