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:
parent
927c4a056e
commit
8edfcd22c7
2 changed files with 100 additions and 34 deletions
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue