music: merge duplicate albums [#66]

Move all duplicate checking to the album creation stage, adding a new
check for duplicate albums.

Album names can be similarly duplicated as artist names, most often
when one has an album consisting of multiple differing file formats.
This commit fixes that by grouping albums up by their ID as usual,
but then merging together albums that have the same (lowercase) album
name and (lowercase) artist name.
This commit is contained in:
OxygenCobalt 2022-02-03 20:05:20 -07:00
parent 433d623f14
commit 6e00fd1129
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
2 changed files with 66 additions and 38 deletions

View file

@ -143,10 +143,8 @@ data class Album(
} }
/** /**
* The data object for an *album* artist. Inherits [MusicParent]. This differs from the actual * The [MusicParent] for an *album* artist. This reflects a group of songs with the same(ish)
* performers. * album artist or artist field, not the individual performers of an artist.
* @property albums The list of all [Album]s in this artist
* @property songs The list of all [Song]s in this artist
*/ */
data class Artist( data class Artist(
override val id: Long, override val id: Long,
@ -166,7 +164,6 @@ data class Artist(
/** /**
* The data object for a genre. Inherits [MusicParent] * The data object for a genre. Inherits [MusicParent]
* @property songs The list of all [Song]s in this genre.
*/ */
data class Genre( data class Genre(
override val id: Long, override val id: Long,

View file

@ -142,6 +142,8 @@ class MusicLoader {
val album = cursor.getString(albumIndex) val album = cursor.getString(albumIndex)
val albumId = cursor.getLong(albumIdIndex) val albumId = cursor.getLong(albumIdIndex)
// If the artist field is <unknown>, make it null. This makes handling the
// insanity of the artist field easier later on.
val artist = cursor.getString(artistIndex).let { val artist = cursor.getString(artistIndex).let {
if (it != MediaStore.UNKNOWN_STRING) it else null if (it != MediaStore.UNKNOWN_STRING) it else null
} }
@ -154,8 +156,16 @@ class MusicLoader {
songs.add( songs.add(
Song( Song(
id, title, fileName, album, albumId, artist, id,
albumArtist, year, track, duration title,
fileName,
album,
albumId,
artist,
albumArtist,
year,
track,
duration
) )
) )
} }
@ -170,27 +180,57 @@ class MusicLoader {
private fun buildAlbums(songs: List<Song>): List<Album> { private fun buildAlbums(songs: List<Song>): List<Album> {
// Group up songs by their album ids and then link them with their albums // Group up songs by their album ids and then link them with their albums
// TODO: Figure out how to group up songs by album in a way that does not accidentally
// split songs by album.
val albums = mutableListOf<Album>() val albums = mutableListOf<Album>()
val songsByAlbum = songs.groupBy { it.albumId } val songsByAlbum = songs.groupBy { it.albumId }
songsByAlbum.forEach { entry -> for (entry in songsByAlbum) {
val albumId = entry.key
val albumSongs = entry.value
// Use the song with the latest year as our metadata song. // Use the song with the latest year as our metadata song.
// This allows us to replicate the LAST_YEAR field, which is useful as it means that // This allows us to replicate the LAST_YEAR field, which is useful as it means that
// weird years like "0" wont show up if there are alternatives. // weird years like "0" wont show up if there are alternatives.
val song = requireNotNull(entry.value.maxByOrNull { it.year }) val templateSong = requireNotNull(albumSongs.maxByOrNull { it.year })
val albumName = templateSong.albumName
albums.add(
Album(
// When assigning an artist to an album, use the album artist first, then the // When assigning an artist to an album, use the album artist first, then the
// normal artist, and then the internal representation of an unknown artist name. // normal artist, and then the internal representation of an unknown artist name.
entry.key, song.albumName, val artistName = templateSong.albumArtistName
song.albumArtistName ?: song.artistName ?: MediaStore.UNKNOWN_STRING, ?: templateSong.artistName ?: MediaStore.UNKNOWN_STRING
song.year, entry.value
val albumYear = templateSong.year
// Search for duplicate albums first. This serves two purposes:
// 1. It collapses differently styled artists [ex. Rammstein vs. RAMMSTEIN] into
// a single grouped artist
// 2. It also unifies albums that exist across several file formats [excluding
// when the titles are mangled by MediaStore insanity]
val previousAlbumIndex = albums.indexOfFirst { album ->
album.name.lowercase() == albumName.lowercase() &&
album.artistName.lowercase() == artistName.lowercase()
}
if (previousAlbumIndex > -1) {
val previousAlbum = albums[previousAlbumIndex]
albums[previousAlbumIndex] = Album(
previousAlbum.id,
previousAlbum.name,
previousAlbum.artistName,
previousAlbum.year,
previousAlbum.songs + albumSongs
)
} else {
albums.add(
Album(
albumId,
albumName,
artistName,
albumYear,
albumSongs
) )
) )
} }
}
albums.removeAll { it.songs.isEmpty() } albums.removeAll { it.songs.isEmpty() }
@ -207,27 +247,18 @@ class MusicLoader {
MediaStore.UNKNOWN_STRING -> context.getString(R.string.def_artist) MediaStore.UNKNOWN_STRING -> context.getString(R.string.def_artist)
else -> name else -> name
} }
val artistAlbums = entry.value.toMutableList() val artistAlbums = entry.value
// Music files from the same artist may format the artist differently, such as being // Due to the black magic we do to get a good artist field, the ID is unreliable.
// in uppercase/lowercase forms. If we have already built an artist that has a // Take a hash of the artist name instead.
// functionally identical name to this one, then simply merge the artists instead artists.add(
// of removing them. Artist(
val previousArtistIndex = artists.indexOfFirst { artist -> name.hashCode().toLong(),
artist.name.lowercase() == name.lowercase() name,
} resolvedName,
artistAlbums
// In most cases, MediaStore artist IDs are unreliable or omitted for speed. )
// Use the hashCode of the artist name as our ID and move on.
if (previousArtistIndex > -1) {
val previousArtist = artists[previousArtistIndex]
artists[previousArtistIndex] = Artist(
previousArtist.name.hashCode().toLong(), previousArtist.name,
previousArtist.resolvedName, previousArtist.albums + artistAlbums
) )
} else {
artists.add(Artist(name.hashCode().toLong(), name, resolvedName, artistAlbums))
}
} }
return artists return artists