musikr: split albums w/full album artist coverage

This is like the old Auxio behavior, but should now trigger with only
full album artist coverage, rather than before where it would always
trigger and break apart sparsely tagged albums.

Still not a perfect heuristic, but it's the best one I can do.
This commit is contained in:
Alexander Capehart 2025-03-17 09:15:35 -06:00
parent eaba11fa44
commit b21b2e49d3
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
3 changed files with 41 additions and 8 deletions

View file

@ -23,6 +23,7 @@ import org.oxycblt.musikr.playlist.SongPointer
import org.oxycblt.musikr.playlist.interpret.PrePlaylist import org.oxycblt.musikr.playlist.interpret.PrePlaylist
import org.oxycblt.musikr.tag.interpret.PreAlbum import org.oxycblt.musikr.tag.interpret.PreAlbum
import org.oxycblt.musikr.tag.interpret.PreArtist import org.oxycblt.musikr.tag.interpret.PreArtist
import org.oxycblt.musikr.tag.interpret.PreArtistsFrom
import org.oxycblt.musikr.tag.interpret.PreGenre import org.oxycblt.musikr.tag.interpret.PreGenre
import org.oxycblt.musikr.tag.interpret.PreSong import org.oxycblt.musikr.tag.interpret.PreSong
import org.oxycblt.musikr.util.unlikelyToBeNull import org.oxycblt.musikr.util.unlikelyToBeNull
@ -75,7 +76,7 @@ private class MusicGraphBuilderImpl : MusicGraph.Builder {
// Albums themselves have their own parent artists that also need to be // Albums themselves have their own parent artists that also need to be
// linked up. // linked up.
val albumArtistVertices = val albumArtistVertices =
preSong.preAlbum.preArtists.map { preArtist -> preSong.preAlbum.preArtists.preArtists.map { preArtist ->
artistVertices.getOrPut(preArtist) { ArtistVertex(preArtist) } artistVertices.getOrPut(preArtist) { ArtistVertex(preArtist) }
} }
val albumVertex = AlbumVertex(preSong.preAlbum, albumArtistVertices.toMutableList()) val albumVertex = AlbumVertex(preSong.preAlbum, albumArtistVertices.toMutableList())
@ -288,7 +289,7 @@ private class MusicGraphBuilderImpl : MusicGraph.Builder {
return return
} }
// No full MBID coverage, discard the MBIDs from the graph. // No full MBID coverage, discard the MBIDs from the graph.
val strippedCluster = val strippedMbidCluster =
cluster.map { cluster.map {
val noMbidPreAlbum = it.preAlbum.copy(musicBrainzId = null) val noMbidPreAlbum = it.preAlbum.copy(musicBrainzId = null)
val simpleMbidVertex = val simpleMbidVertex =
@ -298,7 +299,31 @@ private class MusicGraphBuilderImpl : MusicGraph.Builder {
meldAlbumVertices(it, simpleMbidVertex) meldAlbumVertices(it, simpleMbidVertex)
simpleMbidVertex simpleMbidVertex
} }
simplifyAlbumClusterImpl(strippedCluster) val fullAlbumArtistCoverage =
strippedMbidCluster.all { it.preAlbum.preArtists is PreArtistsFrom.Album }
if (fullAlbumArtistCoverage) {
// All albums have album artists, we can reasonably cluster around artists
// rather than just name.
val albumArtistClusters =
strippedMbidCluster.groupBy { it.preAlbum.preArtists.preArtists }
for (albumArtistCluster in albumArtistClusters.values) {
simplifyAlbumClusterImpl(albumArtistCluster)
}
return
}
val strippedAlbumArtistCluster =
strippedMbidCluster.map {
val noAlbumArtistPreAlbum =
it.preAlbum.copy(
preArtists = PreArtistsFrom.Individual(it.preAlbum.preArtists.preArtists))
val simpleAlbumArtistVertex =
albumVertices.getOrPut(noAlbumArtistPreAlbum) {
AlbumVertex(noAlbumArtistPreAlbum, it.artistVertices.toMutableList())
}
meldAlbumVertices(it, simpleAlbumArtistVertex)
simpleAlbumArtistVertex
}
simplifyAlbumClusterImpl(strippedAlbumArtistCluster)
} }
private fun simplifyAlbumClusterImpl(cluster: Collection<AlbumVertex>) { private fun simplifyAlbumClusterImpl(cluster: Collection<AlbumVertex>) {

View file

@ -54,16 +54,24 @@ internal data class PreSong(
val preAlbum: PreAlbum, val preAlbum: PreAlbum,
val preArtists: List<PreArtist>, val preArtists: List<PreArtist>,
val preGenres: List<PreGenre> val preGenres: List<PreGenre>
) {} )
internal data class PreAlbum( internal data class PreAlbum(
val musicBrainzId: UUID?, val musicBrainzId: UUID?,
val name: Name, val name: Name,
val rawName: String?, val rawName: String?,
val releaseType: ReleaseType, val releaseType: ReleaseType,
val preArtists: List<PreArtist> val preArtists: PreArtistsFrom,
) )
internal sealed interface PreArtistsFrom {
val preArtists: List<PreArtist>
data class Individual(override val preArtists: List<PreArtist>) : PreArtistsFrom
data class Album(override val preArtists: List<PreArtist>) : PreArtistsFrom
}
internal data class PreArtist(val musicBrainzId: UUID?, val name: Name, val rawName: String?) internal data class PreArtist(val musicBrainzId: UUID?, val name: Name, val rawName: String?)
internal data class PreGenre( internal data class PreGenre(

View file

@ -165,9 +165,9 @@ private class TagInterpreterImpl(private val interpretation: Interpretation) : T
ReleaseType.parse(interpretation.separators.split(parsedTags.releaseTypes)) ReleaseType.parse(interpretation.separators.split(parsedTags.releaseTypes))
?: ReleaseType.Album(null), ?: ReleaseType.Album(null),
preArtists = preArtists =
albumPreArtists PreArtistsFrom.Album(albumPreArtists).takeIf { it.preArtists.isNotEmpty() }
.ifEmpty { individualPreArtists } ?: PreArtistsFrom.Individual(
.ifEmpty { listOf(unknownPreArtist()) }) individualPreArtists.ifEmpty { listOf(unknownPreArtist()) }))
} }
private fun makePreArtists( private fun makePreArtists(