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.net.Uri
import android.provider.OpenableColumns
import java.util.LinkedList
import javax.inject.Inject
import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
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.useQuery
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
/**
* 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> {
val start = System.currentTimeMillis()
val songs =
Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
.songs(rawSongs.map { SongImpl(it, settings) }.distinctBy { it.uid })
val uidSet = LinkedHashSet<Music.UID>(rawSongs.size)
val songs = LinkedList<SongImpl>()
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")
return songs
}
private fun buildAlbums(songs: List<SongImpl>, settings: MusicSettings): List<AlbumImpl> {
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
// grouped songs to Album values. Album.Raw will handle the actual grouping rules.
val songsByAlbum = songs.groupBy { it.rawAlbum.key }
val albums = songsByAlbum.map { AlbumImpl(it.key.value, settings, it.value) }
val albums =
albumGrouping.values.map { AlbumImpl(it.dominantRaw.inner, settings, it.music) }
logD("Successfully built ${albums.size} albums in ${System.currentTimeMillis() - start}ms")
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,
// different multi-artist combinations are not treated as different artists.
// 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 (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 (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.
val artists = musicByArtist.map { ArtistImpl(it.key.value, settings, it.value) }
val artists =
artistGrouping.values.map { ArtistImpl(it.dominantRaw.inner, settings, it.music) }
logD(
"Successfully built ${artists.size} artists in ${System.currentTimeMillis() - start}ms")
return artists
@ -210,16 +258,34 @@ private class DeviceLibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings
val start = System.currentTimeMillis()
// 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.Key, MutableList<SongImpl>>()
val songsByGenre = mutableMapOf<RawGenre.Key, Grouping<RawGenre, SongImpl, SongImpl>>()
for (song in songs) {
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.
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")
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)
/** Exposed information that denotes [RawAlbum] uniqueness. */
data class Key(val value: RawAlbum) {
data class Key(private val inner: RawAlbum) {
// Albums are grouped as follows:
// - 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.
@ -126,19 +126,19 @@ data class RawAlbum(
// Cache the hash-code for HashMap efficiency.
private val hashCode =
value.musicBrainzId?.hashCode()
?: (31 * value.name.lowercase().hashCode() + value.rawArtists.hashCode())
inner.musicBrainzId?.hashCode()
?: (31 * inner.name.lowercase().hashCode() + inner.rawArtists.hashCode())
override fun hashCode() = hashCode
override fun equals(other: Any?) =
other is Key &&
when {
value.musicBrainzId != null && other.value.musicBrainzId != null ->
value.musicBrainzId == other.value.musicBrainzId
value.musicBrainzId == null && other.value.musicBrainzId == null ->
other.value.name.equals(other.value.name, true) &&
other.value.rawArtists == other.value.rawArtists
inner.musicBrainzId != null && other.inner.musicBrainzId != null ->
inner.musicBrainzId == other.inner.musicBrainzId
inner.musicBrainzId == null && other.inner.musicBrainzId == null ->
inner.name.equals(other.inner.name, true) &&
inner.rawArtists == other.inner.rawArtists
else -> false
}
}
@ -164,7 +164,7 @@ data class RawArtist(
* Allows [RawArtist]s to be compared by "fundamental" information that is unlikely to change on
* an item-by-item
*/
data class Key(val value: RawArtist) {
data class Key(private val inner: RawArtist) {
// Artists are grouped as follows:
// - 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.
@ -172,7 +172,7 @@ data class RawArtist(
// grouping to be case-insensitive.
// 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
// same name in large libraries.
@ -182,13 +182,13 @@ data class RawArtist(
override fun equals(other: Any?) =
other is Key &&
when {
value.musicBrainzId != null && other.value.musicBrainzId != null ->
value.musicBrainzId == other.value.musicBrainzId
value.musicBrainzId == null && other.value.musicBrainzId == null ->
inner.musicBrainzId != null && other.inner.musicBrainzId != null ->
inner.musicBrainzId == other.inner.musicBrainzId
inner.musicBrainzId == null && other.inner.musicBrainzId == null ->
when {
value.name != null && other.value.name != null ->
value.name.equals(other.value.name, true)
value.name == null && other.value.name == null -> true
inner.name != null && other.inner.name != null ->
inner.name.equals(other.inner.name, true)
inner.name == null && other.inner.name == null -> true
else -> false
}
else -> false
@ -207,9 +207,9 @@ data class RawGenre(
) {
val key = Key(this)
data class Key(val value: RawGenre) {
data class Key(private val inner: RawGenre) {
// 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
// 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?) =
other is Key &&
when {
value.name != null && other.value.name != null ->
value.name.equals(other.value.name, true)
value.name == null && other.value.name == null -> true
inner.name != null && other.inner.name != null ->
inner.name.equals(other.inner.name, true)
inner.name == null && other.inner.name == null -> true
else -> false
}
}