music: make grouping mechanism consistent

Actually bother to make the way music is grouped consistent, based on:
- The first track for albums
- The earliest album for artists
- The first song for genres
This commit is contained in:
Alexander Capehart 2023-06-07 15:15:51 -06:00
parent 927c4a056e
commit 8edfcd22c7
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
2 changed files with 100 additions and 34 deletions

View file

@ -21,8 +21,8 @@ package org.oxycblt.auxio.music.device
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import java.util.LinkedList
import javax.inject.Inject import javax.inject.Inject
import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
@ -33,6 +33,7 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.fs.contentResolverSafe import org.oxycblt.auxio.music.fs.contentResolverSafe
import org.oxycblt.auxio.music.fs.useQuery import org.oxycblt.auxio.music.fs.useQuery
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
/** /**
* Organized music library information obtained from device storage. * Organized music library information obtained from device storage.
@ -159,19 +160,44 @@ private class DeviceLibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings
private fun buildSongs(rawSongs: List<RawSong>, settings: MusicSettings): List<SongImpl> { private fun buildSongs(rawSongs: List<RawSong>, settings: MusicSettings): List<SongImpl> {
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
val songs = val uidSet = LinkedHashSet<Music.UID>(rawSongs.size)
Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING) val songs = LinkedList<SongImpl>()
.songs(rawSongs.map { SongImpl(it, settings) }.distinctBy { it.uid }) for (rawSong in rawSongs) {
val song = SongImpl(rawSong, settings)
if (uidSet.add(song.uid)) {
songs.add(song)
} else {
logW("Duplicate song found: $song")
}
}
logD("Successfully built ${songs.size} songs in ${System.currentTimeMillis() - start}ms") logD("Successfully built ${songs.size} songs in ${System.currentTimeMillis() - start}ms")
return songs return songs
} }
private fun buildAlbums(songs: List<SongImpl>, settings: MusicSettings): List<AlbumImpl> { private fun buildAlbums(songs: List<SongImpl>, settings: MusicSettings): List<AlbumImpl> {
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
val albumGrouping = mutableMapOf<RawAlbum.Key, Grouping<RawAlbum, SongImpl, SongImpl>>()
for (song in songs) {
val key = RawAlbum.Key(song.rawAlbum)
val body = albumGrouping[key]
if (body != null) {
body.music.add(song)
val dominantSong = body.dominantRaw.derived
val dominates =
song.track != null &&
(dominantSong.track == null || song.track < dominantSong.track)
if (dominates) {
body.dominantRaw = DominantRaw(song.rawAlbum, song)
}
} else {
albumGrouping[key] = Grouping(DominantRaw(song.rawAlbum, song), mutableListOf(song))
}
}
// 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.key } val albums =
val albums = songsByAlbum.map { AlbumImpl(it.key.value, settings, it.value) } albumGrouping.values.map { AlbumImpl(it.dominantRaw.inner, settings, it.music) }
logD("Successfully built ${albums.size} albums in ${System.currentTimeMillis() - start}ms") logD("Successfully built ${albums.size} albums in ${System.currentTimeMillis() - start}ms")
return albums return albums
} }
@ -185,22 +211,44 @@ private class DeviceLibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings
// 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.
// Songs and albums are grouped by artist and album artist respectively. // Songs and albums are grouped by artist and album artist respectively.
val musicByArtist = mutableMapOf<RawArtist.Key, MutableList<Music>>() val artistGrouping = mutableMapOf<RawArtist.Key, Grouping<RawArtist, AlbumImpl, Music>>()
for (song in songs) { for (song in songs) {
for (rawArtist in song.rawArtists) { for (rawArtist in song.rawArtists) {
musicByArtist.getOrPut(rawArtist.key) { mutableListOf() }.add(song) val key = RawArtist.Key(rawArtist)
val body = artistGrouping[key]
if (body != null) {
body.music.add(song)
} else {
artistGrouping[key] =
Grouping(DominantRaw(rawArtist, albums.first()), mutableListOf(song))
}
} }
} }
for (album in albums) { for (album in albums) {
for (rawArtist in album.rawArtists) { for (rawArtist in album.rawArtists) {
musicByArtist.getOrPut(rawArtist.key) { mutableListOf() }.add(album) val key = RawArtist.Key(rawArtist)
val body = artistGrouping[key]
if (body != null) {
body.music.add(album)
val dominantAlbum = body.dominantRaw.derived
val dominates =
album.dates != null &&
(dominantAlbum.dates == null || album.dates < dominantAlbum.dates)
if (dominates) {
body.dominantRaw = DominantRaw(rawArtist, album)
}
} else {
artistGrouping[key] =
Grouping(DominantRaw(rawArtist, album), mutableListOf(album))
}
} }
} }
// Convert the combined mapping into artist instances. // Convert the combined mapping into artist instances.
val artists = musicByArtist.map { ArtistImpl(it.key.value, settings, it.value) } val artists =
artistGrouping.values.map { ArtistImpl(it.dominantRaw.inner, settings, it.music) }
logD( logD(
"Successfully built ${artists.size} artists in ${System.currentTimeMillis() - start}ms") "Successfully built ${artists.size} artists in ${System.currentTimeMillis() - start}ms")
return artists return artists
@ -210,16 +258,34 @@ private class DeviceLibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
// 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.Key, MutableList<SongImpl>>() val songsByGenre = mutableMapOf<RawGenre.Key, Grouping<RawGenre, SongImpl, SongImpl>>()
for (song in songs) { for (song in songs) {
for (rawGenre in song.rawGenres) { for (rawGenre in song.rawGenres) {
songsByGenre.getOrPut(rawGenre.key) { mutableListOf() }.add(song) val key = RawGenre.Key(rawGenre)
val body = songsByGenre[key]
if (body != null) {
body.music.add(song)
val dominantSong = body.dominantRaw.derived
if (song.date != null && song.name < dominantSong.name) {
body.dominantRaw = DominantRaw(rawGenre, song)
}
} else {
songsByGenre[key] = Grouping(DominantRaw(rawGenre, song), mutableListOf(song))
}
} }
} }
// Convert the mapping into genre instances. // Convert the mapping into genre instances.
val genres = songsByGenre.map { GenreImpl(it.key.value, settings, it.value) } val genres =
songsByGenre.map { GenreImpl(it.value.dominantRaw.inner, settings, it.value.music) }
logD("Successfully built ${genres.size} genres in ${System.currentTimeMillis() - start}ms") logD("Successfully built ${genres.size} genres in ${System.currentTimeMillis() - start}ms")
return genres return genres
} }
data class DominantRaw<R, M : Music>(val inner: R, val derived: M)
data class Grouping<R, D : Music, M : Music>(
var dominantRaw: DominantRaw<R, D>,
val music: MutableList<M>
)
} }

View file

@ -116,7 +116,7 @@ data class RawAlbum(
val key = Key(this) val key = Key(this)
/** Exposed information that denotes [RawAlbum] uniqueness. */ /** Exposed information that denotes [RawAlbum] uniqueness. */
data class Key(val value: RawAlbum) { data class Key(private val inner: RawAlbum) {
// Albums are grouped as follows: // Albums are grouped as follows:
// - If we have a MusicBrainz ID, only group by it. This allows different Albums with the // - If we have a MusicBrainz ID, only group by it. This allows different Albums with the
// same name to be differentiated, which is common in large libraries. // same name to be differentiated, which is common in large libraries.
@ -126,19 +126,19 @@ data class RawAlbum(
// Cache the hash-code for HashMap efficiency. // Cache the hash-code for HashMap efficiency.
private val hashCode = private val hashCode =
value.musicBrainzId?.hashCode() inner.musicBrainzId?.hashCode()
?: (31 * value.name.lowercase().hashCode() + value.rawArtists.hashCode()) ?: (31 * inner.name.lowercase().hashCode() + inner.rawArtists.hashCode())
override fun hashCode() = hashCode override fun hashCode() = hashCode
override fun equals(other: Any?) = override fun equals(other: Any?) =
other is Key && other is Key &&
when { when {
value.musicBrainzId != null && other.value.musicBrainzId != null -> inner.musicBrainzId != null && other.inner.musicBrainzId != null ->
value.musicBrainzId == other.value.musicBrainzId inner.musicBrainzId == other.inner.musicBrainzId
value.musicBrainzId == null && other.value.musicBrainzId == null -> inner.musicBrainzId == null && other.inner.musicBrainzId == null ->
other.value.name.equals(other.value.name, true) && inner.name.equals(other.inner.name, true) &&
other.value.rawArtists == other.value.rawArtists inner.rawArtists == other.inner.rawArtists
else -> false else -> false
} }
} }
@ -164,7 +164,7 @@ data class RawArtist(
* Allows [RawArtist]s to be compared by "fundamental" information that is unlikely to change on * Allows [RawArtist]s to be compared by "fundamental" information that is unlikely to change on
* an item-by-item * an item-by-item
*/ */
data class Key(val value: RawArtist) { data class Key(private val inner: RawArtist) {
// Artists are grouped as follows: // Artists are grouped as follows:
// - If we have a MusicBrainz ID, only group by it. This allows different Artists with the // - If we have a MusicBrainz ID, only group by it. This allows different Artists with the
// same name to be differentiated, which is common in large libraries. // same name to be differentiated, which is common in large libraries.
@ -172,7 +172,7 @@ data class RawArtist(
// grouping to be case-insensitive. // grouping to be case-insensitive.
// Cache the hashCode for HashMap efficiency. // Cache the hashCode for HashMap efficiency.
private val hashCode = value.musicBrainzId?.hashCode() ?: value.name?.lowercase().hashCode() private val hashCode = inner.musicBrainzId?.hashCode() ?: inner.name?.lowercase().hashCode()
// Compare names and MusicBrainz IDs in order to differentiate artists with the // Compare names and MusicBrainz IDs in order to differentiate artists with the
// same name in large libraries. // same name in large libraries.
@ -182,13 +182,13 @@ data class RawArtist(
override fun equals(other: Any?) = override fun equals(other: Any?) =
other is Key && other is Key &&
when { when {
value.musicBrainzId != null && other.value.musicBrainzId != null -> inner.musicBrainzId != null && other.inner.musicBrainzId != null ->
value.musicBrainzId == other.value.musicBrainzId inner.musicBrainzId == other.inner.musicBrainzId
value.musicBrainzId == null && other.value.musicBrainzId == null -> inner.musicBrainzId == null && other.inner.musicBrainzId == null ->
when { when {
value.name != null && other.value.name != null -> inner.name != null && other.inner.name != null ->
value.name.equals(other.value.name, true) inner.name.equals(other.inner.name, true)
value.name == null && other.value.name == null -> true inner.name == null && other.inner.name == null -> true
else -> false else -> false
} }
else -> false else -> false
@ -207,9 +207,9 @@ data class RawGenre(
) { ) {
val key = Key(this) val key = Key(this)
data class Key(val value: RawGenre) { data class Key(private val inner: RawGenre) {
// Cache the hashCode for HashMap efficiency. // Cache the hashCode for HashMap efficiency.
private val hashCode = value.name?.lowercase().hashCode() private val hashCode = inner.name?.lowercase().hashCode()
// Only group by the lowercase genre name. This allows Genre grouping to be // Only group by the lowercase genre name. This allows Genre grouping to be
// case-insensitive, which may be helpful in some libraries with different ways of // case-insensitive, which may be helpful in some libraries with different ways of
@ -219,9 +219,9 @@ data class RawGenre(
override fun equals(other: Any?) = override fun equals(other: Any?) =
other is Key && other is Key &&
when { when {
value.name != null && other.value.name != null -> inner.name != null && other.inner.name != null ->
value.name.equals(other.value.name, true) inner.name.equals(other.inner.name, true)
value.name == null && other.value.name == null -> true inner.name == null && other.inner.name == null -> true
else -> false else -> false
} }
} }