diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt index 814c15818..6d2c66899 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt @@ -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, settings: MusicSettings private fun buildSongs(rawSongs: List, settings: MusicSettings): List { 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(rawSongs.size) + val songs = LinkedList() + 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, settings: MusicSettings): List { val start = System.currentTimeMillis() + val albumGrouping = mutableMapOf>() + 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, 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>() + val artistGrouping = mutableMapOf>() 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, 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>() + val songsByGenre = mutableMapOf>() 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(val inner: R, val derived: M) + + data class Grouping( + var dominantRaw: DominantRaw, + val music: MutableList + ) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt index 2a198c687..46c84fc51 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt @@ -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 } }