music: add option to disable article sort names

Add a setting to remove hard-coded sort names based on articles.

This feature is nice, but does not work with some non-english music.
Those individuals should have the ability to disable it.

The implementation honestly is not the greatest, primarily because it
does a 100% reload when it could just regenerate the library. Auxio's
current music model isn't really designed for that, so it will do this
until a need to that kind of "soft reload" really arises.

Resolves #359.
This commit is contained in:
Alexander Capehart 2023-02-20 18:30:43 -07:00
parent 2d9a5ad5cd
commit 45fe36bd09
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
7 changed files with 75 additions and 29 deletions

View file

@ -6,6 +6,7 @@
- Added support for disc subtitles - Added support for disc subtitles
- Added support for ALAC files - Added support for ALAC files
- Song properties view now shows tags - Song properties view now shows tags
- Added option to control whether articles like "the" are ignored when sorting
#### What's Improved #### What's Improved
- Will now accept zeroed track/disc numbers in the presence of non-zero total - Will now accept zeroed track/disc numbers in the presence of non-zero total

View file

@ -42,6 +42,8 @@ interface MusicSettings : Settings<MusicSettings.Listener> {
val shouldBeObserving: Boolean val shouldBeObserving: Boolean
/** A [String] of characters representing the desired characters to denote multi-value tags. */ /** A [String] of characters representing the desired characters to denote multi-value tags. */
var multiValueSeparators: String var multiValueSeparators: String
/** Whether to trim english articles with song sort names. */
val automaticSortNames: Boolean
/** The [Sort] mode used in [Song] lists. */ /** The [Sort] mode used in [Song] lists. */
var songSort: Sort var songSort: Sort
/** The [Sort] mode used in [Album] lists. */ /** The [Sort] mode used in [Album] lists. */
@ -106,6 +108,9 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext context: Context
} }
} }
override val automaticSortNames: Boolean
get() = sharedPreferences.getBoolean(getString(R.string.set_key_auto_sort_names), true)
override var songSort: Sort override var songSort: Sort
get() = get() =
Sort.fromIntCode( Sort.fromIntCode(
@ -203,11 +208,14 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext context: Context
} }
override fun onSettingChanged(key: String, listener: MusicSettings.Listener) { override fun onSettingChanged(key: String, listener: MusicSettings.Listener) {
// TODO: Differentiate "hard reloads" (Need the cache) and "Soft reloads"
// (just need to manipulate data)
when (key) { when (key) {
getString(R.string.set_key_exclude_non_music), getString(R.string.set_key_exclude_non_music),
getString(R.string.set_key_music_dirs), getString(R.string.set_key_music_dirs),
getString(R.string.set_key_music_dirs_include), getString(R.string.set_key_music_dirs_include),
getString(R.string.set_key_separators) -> listener.onIndexingSettingChanged() getString(R.string.set_key_separators),
getString(R.string.set_key_auto_sort_names) -> listener.onIndexingSettingChanged()
getString(R.string.set_key_observing) -> listener.onObservingChanged() getString(R.string.set_key_observing) -> listener.onObservingChanged()
} }
} }

View file

@ -88,9 +88,9 @@ interface Library {
private class LibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings) : Library { private class LibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings) : Library {
override val songs = buildSongs(rawSongs, settings) override val songs = buildSongs(rawSongs, settings)
override val albums = buildAlbums(songs) override val albums = buildAlbums(songs, settings)
override val artists = buildArtists(songs, albums) override val artists = buildArtists(songs, albums, settings)
override val genres = buildGenres(songs) override val genres = buildGenres(songs, settings)
// Use a mapping to make finding information based on it's UID much faster. // Use a mapping to make finding information based on it's UID much faster.
private val uidMap = buildMap { private val uidMap = buildMap {
@ -127,7 +127,7 @@ private class LibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings) : Li
/** /**
* Build a list [SongImpl]s from the given [RawSong]. * Build a list [SongImpl]s from the given [RawSong].
* @param rawSongs The [RawSong]s to build the [SongImpl]s from. * @param rawSongs The [RawSong]s to build the [SongImpl]s from.
* @param settings [MusicSettings] required to build [SongImpl]s. * @param settings [MusicSettings] to obtain user parsing configuration.
* @return A sorted list of [SongImpl]s derived from the [RawSong] that should be suitable for * @return A sorted list of [SongImpl]s derived from the [RawSong] that should be suitable for
* grouping. * grouping.
*/ */
@ -139,14 +139,15 @@ private class LibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings) : Li
* Build a list of [Album]s from the given [Song]s. * Build a list of [Album]s from the given [Song]s.
* @param songs The [Song]s to build [Album]s from. These will be linked with their respective * @param songs The [Song]s to build [Album]s from. These will be linked with their respective
* [Album]s when created. * [Album]s when created.
* @param settings [MusicSettings] to obtain user parsing configuration.
* @return A non-empty list of [Album]s. These [Album]s will be incomplete and must be linked * @return A non-empty list of [Album]s. These [Album]s will be incomplete and must be linked
* with parent [Artist] instances in order to be usable. * with parent [Artist] instances in order to be usable.
*/ */
private fun buildAlbums(songs: List<SongImpl>): List<AlbumImpl> { private fun buildAlbums(songs: List<SongImpl>, settings: MusicSettings): List<AlbumImpl> {
// Group songs by their singular raw album, then map the raw instances and their // Group songs by their singular raw album, then map the raw instances and their
// grouped songs to Album values. Album.Raw will handle the actual grouping rules. // grouped songs to Album values. Album.Raw will handle the actual grouping rules.
val songsByAlbum = songs.groupBy { it.rawAlbum } val songsByAlbum = songs.groupBy { it.rawAlbum }
val albums = songsByAlbum.map { AlbumImpl(it.key, it.value) } val albums = songsByAlbum.map { AlbumImpl(it.key, settings, it.value) }
logD("Successfully built ${albums.size} albums") logD("Successfully built ${albums.size} albums")
return albums return albums
} }
@ -161,10 +162,15 @@ private class LibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings) : Li
* @param albums The [Album]s to build [Artist]s from. One [Album] can result in the creation of * @param albums The [Album]s to build [Artist]s from. One [Album] can result in the creation of
* one or more [Artist] instances. These will be linked with their respective [Artist]s when * one or more [Artist] instances. These will be linked with their respective [Artist]s when
* created. * created.
* @param settings [MusicSettings] to obtain user parsing configuration.
* @return A non-empty list of [Artist]s. These [Artist]s will consist of the combined groupings * @return A non-empty list of [Artist]s. These [Artist]s will consist of the combined groupings
* of [Song]s and [Album]s. * of [Song]s and [Album]s.
*/ */
private fun buildArtists(songs: List<SongImpl>, albums: List<AlbumImpl>): List<ArtistImpl> { private fun buildArtists(
songs: List<SongImpl>,
albums: List<AlbumImpl>,
settings: MusicSettings
): List<ArtistImpl> {
// Add every raw artist credited to each Song/Album to the grouping. This way, // Add every raw artist credited to each Song/Album to the grouping. This way,
// different multi-artist combinations are not treated as different artists. // different multi-artist combinations are not treated as different artists.
val musicByArtist = mutableMapOf<RawArtist, MutableList<Music>>() val musicByArtist = mutableMapOf<RawArtist, MutableList<Music>>()
@ -182,7 +188,7 @@ private class LibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings) : Li
} }
// Convert the combined mapping into artist instances. // Convert the combined mapping into artist instances.
val artists = musicByArtist.map { ArtistImpl(it.key, it.value) } val artists = musicByArtist.map { ArtistImpl(it.key, settings, it.value) }
logD("Successfully built ${artists.size} artists") logD("Successfully built ${artists.size} artists")
return artists return artists
} }
@ -192,9 +198,10 @@ private class LibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings) : Li
* @param [songs] The [Song]s to build [Genre]s from. One [Song] can result in the creation of * @param [songs] The [Song]s to build [Genre]s from. One [Song] can result in the creation of
* one or more [Genre] instances. These will be linked with their respective [Genre]s when * one or more [Genre] instances. These will be linked with their respective [Genre]s when
* created. * created.
* @param settings [MusicSettings] to obtain user parsing configuration.
* @return A non-empty list of [Genre]s. * @return A non-empty list of [Genre]s.
*/ */
private fun buildGenres(songs: List<SongImpl>): List<GenreImpl> { private fun buildGenres(songs: List<SongImpl>, settings: MusicSettings): List<GenreImpl> {
// Add every raw genre credited to each Song to the grouping. This way, // Add every raw genre credited to each Song to the grouping. This way,
// different multi-genre combinations are not treated as different genres. // different multi-genre combinations are not treated as different genres.
val songsByGenre = mutableMapOf<RawGenre, MutableList<SongImpl>>() val songsByGenre = mutableMapOf<RawGenre, MutableList<SongImpl>>()
@ -205,7 +212,7 @@ private class LibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings) : Li
} }
// Convert the mapping into genre instances. // Convert the mapping into genre instances.
val genres = songsByGenre.map { GenreImpl(it.key, it.value) } val genres = songsByGenre.map { GenreImpl(it.key, settings, it.value) }
logD("Successfully built ${genres.size} genres") logD("Successfully built ${genres.size} genres")
return genres return genres
} }

View file

@ -47,7 +47,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
* Library-backed implementation of [Song]. * Library-backed implementation of [Song].
* @param rawSong The [RawSong] to derive the member data from. * @param rawSong The [RawSong] to derive the member data from.
* @param musicSettings [MusicSettings] to perform further user-configured parsing. * @param musicSettings [MusicSettings] to for user parsing configuration.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class SongImpl(rawSong: RawSong, musicSettings: MusicSettings) : Song { class SongImpl(rawSong: RawSong, musicSettings: MusicSettings) : Song {
@ -70,7 +70,7 @@ class SongImpl(rawSong: RawSong, musicSettings: MusicSettings) : Song {
} }
override val rawName = requireNotNull(rawSong.name) { "Invalid raw: No title" } override val rawName = requireNotNull(rawSong.name) { "Invalid raw: No title" }
override val rawSortName = rawSong.sortName override val rawSortName = rawSong.sortName
override val collationKey = makeCollationKey(this) override val collationKey = makeCollationKey(musicSettings)
override fun resolveName(context: Context) = rawName override fun resolveName(context: Context) = rawName
override val track = rawSong.track override val track = rawSong.track
@ -219,11 +219,16 @@ class SongImpl(rawSong: RawSong, musicSettings: MusicSettings) : Song {
/** /**
* Library-backed implementation of [Album]. * Library-backed implementation of [Album].
* @param rawAlbum The [RawAlbum] to derive the member data from. * @param rawAlbum The [RawAlbum] to derive the member data from.
* @param musicSettings [MusicSettings] to for user parsing configuration.
* @param songs The [Song]s that are a part of this [Album]. These items will be linked to this * @param songs The [Song]s that are a part of this [Album]. These items will be linked to this
* [Album]. * [Album].
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class AlbumImpl(val rawAlbum: RawAlbum, override val songs: List<SongImpl>) : Album { class AlbumImpl(
private val rawAlbum: RawAlbum,
musicSettings: MusicSettings,
override val songs: List<SongImpl>
) : Album {
override val uid = override val uid =
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID. // Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
rawAlbum.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ALBUMS, it) } rawAlbum.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ALBUMS, it) }
@ -236,7 +241,7 @@ class AlbumImpl(val rawAlbum: RawAlbum, override val songs: List<SongImpl>) : Al
} }
override val rawName = rawAlbum.name override val rawName = rawAlbum.name
override val rawSortName = rawAlbum.sortName override val rawSortName = rawAlbum.sortName
override val collationKey = makeCollationKey(this) override val collationKey = makeCollationKey(musicSettings)
override fun resolveName(context: Context) = rawName override fun resolveName(context: Context) = rawName
override val dates = Date.Range.from(songs.mapNotNull { it.date }) override val dates = Date.Range.from(songs.mapNotNull { it.date })
@ -309,19 +314,24 @@ class AlbumImpl(val rawAlbum: RawAlbum, override val songs: List<SongImpl>) : Al
/** /**
* Library-backed implementation of [Artist]. * Library-backed implementation of [Artist].
* @param rawArtist The [RawArtist] to derive the member data from. * @param rawArtist The [RawArtist] to derive the member data from.
* @param musicSettings [MusicSettings] to for user parsing configuration.
* @param songAlbums A list of the [Song]s and [Album]s that are a part of this [Artist] , either * @param songAlbums A list of the [Song]s and [Album]s that are a part of this [Artist] , either
* through artist or album artist tags. Providing [Song]s to the artist is optional. These instances * through artist or album artist tags. Providing [Song]s to the artist is optional. These instances
* will be linked to this [Artist]. * will be linked to this [Artist].
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class ArtistImpl(private val rawArtist: RawArtist, songAlbums: List<Music>) : Artist { class ArtistImpl(
private val rawArtist: RawArtist,
musicSettings: MusicSettings,
songAlbums: List<Music>
) : Artist {
override val uid = override val uid =
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID. // Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
rawArtist.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ARTISTS, it) } rawArtist.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ARTISTS, it) }
?: Music.UID.auxio(MusicMode.ARTISTS) { update(rawArtist.name) } ?: Music.UID.auxio(MusicMode.ARTISTS) { update(rawArtist.name) }
override val rawName = rawArtist.name override val rawName = rawArtist.name
override val rawSortName = rawArtist.sortName override val rawSortName = rawArtist.sortName
override val collationKey = makeCollationKey(this) override val collationKey = makeCollationKey(musicSettings)
override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_artist) override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_artist)
override val songs: List<Song> override val songs: List<Song>
@ -390,13 +400,20 @@ class ArtistImpl(private val rawArtist: RawArtist, songAlbums: List<Music>) : Ar
} }
/** /**
* Library-backed implementation of [Genre]. * Library-backed implementation of [Genre].
* @param rawGenre [RawGenre] to derive the member data from.
* @param musicSettings [MusicSettings] to for user parsing configuration.
* @param songs Child [SongImpl]s of this instance.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class GenreImpl(private val rawGenre: RawGenre, override val songs: List<SongImpl>) : Genre { class GenreImpl(
private val rawGenre: RawGenre,
musicSettings: MusicSettings,
override val songs: List<SongImpl>
) : Genre {
override val uid = Music.UID.auxio(MusicMode.GENRES) { update(rawGenre.name) } override val uid = Music.UID.auxio(MusicMode.GENRES) { update(rawGenre.name) }
override val rawName = rawGenre.name override val rawName = rawGenre.name
override val rawSortName = rawName override val rawSortName = rawName
override val collationKey = makeCollationKey(this) override val collationKey = makeCollationKey(musicSettings)
override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_genre) override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_genre)
override val albums: List<Album> override val albums: List<Album>
@ -503,19 +520,23 @@ private val COLLATOR: Collator = Collator.getInstance().apply { strength = Colla
/** /**
* Provided implementation to create a [CollationKey] in the way described by [Music.collationKey]. * Provided implementation to create a [CollationKey] in the way described by [Music.collationKey].
* This should be used in all overrides of all [CollationKey]. * This should be used in all overrides of all [CollationKey].
* @param music The [Music] to create the [CollationKey] for. * @param musicSettings [MusicSettings] required for user parsing configuration.
* @return A [CollationKey] that follows the specification described by [Music.collationKey]. * @return A [CollationKey] that follows the specification described by [Music.collationKey].
*/ */
private fun makeCollationKey(music: Music): CollationKey? { private fun Music.makeCollationKey(musicSettings: MusicSettings): CollationKey? {
val sortName = var sortName = (rawSortName ?: rawName) ?: return null
(music.rawSortName ?: music.rawName)?.run {
when { if (musicSettings.automaticSortNames) {
length > 5 && startsWith("the ", ignoreCase = true) -> substring(4) sortName =
length > 4 && startsWith("an ", ignoreCase = true) -> substring(3) sortName.run {
length > 3 && startsWith("a ", ignoreCase = true) -> substring(2) when {
else -> this length > 5 && startsWith("the ", ignoreCase = true) -> substring(4)
length > 4 && startsWith("an ", ignoreCase = true) -> substring(3)
length > 3 && startsWith("a ", ignoreCase = true) -> substring(2)
else -> this
}
} }
} }
return COLLATOR.getCollationKey(sortName) return COLLATOR.getCollationKey(sortName)
} }

View file

@ -18,6 +18,7 @@
<string name="set_key_music_dirs_include" translatable="false">auxio_include_dirs</string> <string name="set_key_music_dirs_include" translatable="false">auxio_include_dirs</string>
<string name="set_key_exclude_non_music" translatable="false">auxio_exclude_non_music</string> <string name="set_key_exclude_non_music" translatable="false">auxio_exclude_non_music</string>
<string name="set_key_separators" translatable="false">auxio_separators</string> <string name="set_key_separators" translatable="false">auxio_separators</string>
<string name="set_key_auto_sort_names" translatable="false">auxio_auto_sort_names</string>
<string name="set_key_headset_autoplay" translatable="false">auxio_headset_autoplay</string> <string name="set_key_headset_autoplay" translatable="false">auxio_headset_autoplay</string>
<string name="set_key_replay_gain" translatable="false">auxio_replay_gain</string> <string name="set_key_replay_gain" translatable="false">auxio_replay_gain</string>

View file

@ -212,6 +212,8 @@
<string name="set_separators_plus">Plus (+)</string> <string name="set_separators_plus">Plus (+)</string>
<string name="set_separators_and">Ampersand (&amp;)</string> <string name="set_separators_and">Ampersand (&amp;)</string>
<string name="set_separators_warning">Warning: Using this setting may result in some tags being incorrectly interpreted as having multiple values. You can resolve this by prefixing unwanted separator characters with a backslash (\\).</string> <string name="set_separators_warning">Warning: Using this setting may result in some tags being incorrectly interpreted as having multiple values. You can resolve this by prefixing unwanted separator characters with a backslash (\\).</string>
<string name="set_auto_sort_names">Ignore articles when sorting</string>
<string name="set_auto_sort_names_desc">Ignore words like \"the\" when sorting by name (works best with english-language music)</string>
<string name="set_hide_collaborators">Hide collaborators</string> <string name="set_hide_collaborators">Hide collaborators</string>
<string name="set_hide_collaborators_desc">Only show artists that are directly credited on an album (works best on well-tagged libraries)</string> <string name="set_hide_collaborators_desc">Only show artists that are directly credited on an album (works best on well-tagged libraries)</string>
<string name="set_images">Images</string> <string name="set_images">Images</string>

View file

@ -20,6 +20,12 @@
app:summary="@string/set_separators_desc" app:summary="@string/set_separators_desc"
app:title="@string/set_separators" /> app:title="@string/set_separators" />
<SwitchPreferenceCompat
app:defaultValue="true"
app:key="@string/set_key_auto_sort_names"
app:summary="@string/set_auto_sort_names_desc"
app:title="@string/set_auto_sort_names" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
app:defaultValue="false" app:defaultValue="false"
app:key="@string/set_key_hide_collaborators" app:key="@string/set_key_hide_collaborators"