diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dbcec439..978ceb9a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt index c954d1696..252693747 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt @@ -42,6 +42,8 @@ interface MusicSettings : Settings { 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() } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/model/Library.kt b/app/src/main/java/org/oxycblt/auxio/music/model/Library.kt index 4d9241bad..b952efba8 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/model/Library.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/model/Library.kt @@ -88,9 +88,9 @@ interface Library { private class LibraryImpl(rawSongs: List, 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, 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, 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): List { + private fun buildAlbums(songs: List, settings: MusicSettings): List { // 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, 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, albums: List): List { + private fun buildArtists( + songs: List, + albums: List, + settings: MusicSettings + ): List { // 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>() @@ -182,7 +188,7 @@ private class LibraryImpl(rawSongs: List, 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, 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): List { + private fun buildGenres(songs: List, settings: MusicSettings): List { // 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>() @@ -205,7 +212,7 @@ private class LibraryImpl(rawSongs: List, 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 } diff --git a/app/src/main/java/org/oxycblt/auxio/music/model/MusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/model/MusicImpl.kt index a39b474d9..1bf87f5bf 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/model/MusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/model/MusicImpl.kt @@ -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) : Album { +class AlbumImpl( + private val rawAlbum: RawAlbum, + musicSettings: MusicSettings, + override val songs: List +) : 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) : 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) : 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) : Artist { +class ArtistImpl( + private val rawArtist: RawArtist, + musicSettings: MusicSettings, + songAlbums: List +) : 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 @@ -390,13 +400,20 @@ class ArtistImpl(private val rawArtist: RawArtist, songAlbums: List) : 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) : Genre { +class GenreImpl( + private val rawGenre: RawGenre, + musicSettings: MusicSettings, + override val songs: List +) : 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 @@ -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) } diff --git a/app/src/main/res/values/settings.xml b/app/src/main/res/values/settings.xml index 2fb5975e8..5971baebb 100644 --- a/app/src/main/res/values/settings.xml +++ b/app/src/main/res/values/settings.xml @@ -18,6 +18,7 @@ auxio_include_dirs auxio_exclude_non_music auxio_separators + auxio_auto_sort_names auxio_headset_autoplay auxio_replay_gain diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a8e92346e..41d015a28 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -212,6 +212,8 @@ Plus (+) Ampersand (&) 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 (\\). + Ignore articles when sorting + Ignore words like \"the\" when sorting by name (works best with english-language music) Hide collaborators Only show artists that are directly credited on an album (works best on well-tagged libraries) Images diff --git a/app/src/main/res/xml/preferences_music.xml b/app/src/main/res/xml/preferences_music.xml index f45a960e6..decbc4090 100644 --- a/app/src/main/res/xml/preferences_music.xml +++ b/app/src/main/res/xml/preferences_music.xml @@ -20,6 +20,12 @@ app:summary="@string/set_separators_desc" app:title="@string/set_separators" /> + +