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.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>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue