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 ALAC files
- Song properties view now shows tags
- Added option to control whether articles like "the" are ignored when sorting
#### What's Improved
- 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
/** A [String] of characters representing the desired characters to denote multi-value tags. */
var multiValueSeparators: String
/** Whether to trim english articles with song sort names. */
val automaticSortNames: Boolean
/** The [Sort] mode used in [Song] lists. */
var songSort: Sort
/** 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
get() =
Sort.fromIntCode(
@ -203,11 +208,14 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext context: Context
}
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) {
getString(R.string.set_key_exclude_non_music),
getString(R.string.set_key_music_dirs),
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()
}
}

View file

@ -88,9 +88,9 @@ interface Library {
private class LibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings) : Library {
override val songs = buildSongs(rawSongs, settings)
override val albums = buildAlbums(songs)
override val artists = buildArtists(songs, albums)
override val genres = buildGenres(songs)
override val albums = buildAlbums(songs, settings)
override val artists = buildArtists(songs, albums, settings)
override val genres = buildGenres(songs, settings)
// Use a mapping to make finding information based on it's UID much faster.
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].
* @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
* 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.
* @param songs The [Song]s to build [Album]s from. These will be linked with their respective
* [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
* 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
// grouped songs to Album values. Album.Raw will handle the actual grouping rules.
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")
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
* one or more [Artist] instances. These will be linked with their respective [Artist]s when
* 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
* 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,
// different multi-artist combinations are not treated as different artists.
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.
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")
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
* one or more [Genre] instances. These will be linked with their respective [Genre]s when
* created.
* @param settings [MusicSettings] to obtain user parsing configuration.
* @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,
// different multi-genre combinations are not treated as different genres.
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.
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")
return genres
}

View file

@ -47,7 +47,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* Library-backed implementation of [Song].
* @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)
*/
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 rawSortName = rawSong.sortName
override val collationKey = makeCollationKey(this)
override val collationKey = makeCollationKey(musicSettings)
override fun resolveName(context: Context) = rawName
override val track = rawSong.track
@ -219,11 +219,16 @@ class SongImpl(rawSong: RawSong, musicSettings: MusicSettings) : Song {
/**
* Library-backed implementation of [Album].
* @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
* [Album].
* @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 =
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
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 rawSortName = rawAlbum.sortName
override val collationKey = makeCollationKey(this)
override val collationKey = makeCollationKey(musicSettings)
override fun resolveName(context: Context) = rawName
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].
* @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
* through artist or album artist tags. Providing [Song]s to the artist is optional. These instances
* will be linked to this [Artist].
* @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 =
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
rawArtist.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ARTISTS, it) }
?: Music.UID.auxio(MusicMode.ARTISTS) { update(rawArtist.name) }
override val rawName = rawArtist.name
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 val songs: List<Song>
@ -390,13 +400,20 @@ class ArtistImpl(private val rawArtist: RawArtist, songAlbums: List<Music>) : Ar
}
/**
* 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)
*/
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 rawName = rawGenre.name
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 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].
* 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].
*/
private fun makeCollationKey(music: Music): CollationKey? {
val sortName =
(music.rawSortName ?: music.rawName)?.run {
when {
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
private fun Music.makeCollationKey(musicSettings: MusicSettings): CollationKey? {
var sortName = (rawSortName ?: rawName) ?: return null
if (musicSettings.automaticSortNames) {
sortName =
sortName.run {
when {
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)
}

View file

@ -18,6 +18,7 @@
<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_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_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_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_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_desc">Only show artists that are directly credited on an album (works best on well-tagged libraries)</string>
<string name="set_images">Images</string>

View file

@ -20,6 +20,12 @@
app:summary="@string/set_separators_desc"
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
app:defaultValue="false"
app:key="@string/set_key_hide_collaborators"