Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Add support for ALBUM_ARTIST tag in Twelve

Here is a working patch :

From 695e31af28614f5480f9e2e2f353424addebd920 Mon Sep 17 00:00:00 2001
From: Sylvain Saboua <sylvain@saboua.me>
Date: Wed, 11 Feb 2026 23:22:41 +0100
Subject: [PATCH] Add ALBUM_ARTIST support for compilation album grouping

- Add ALBUM_ARTIST column to audiosProjection
- Update mapEachRowToAudio() to read and set albumArtist
- Replace artists() method to group by albumArtist instead of querying MediaStore artists table
- Update artist() method to filter by albumArtist with fallback to artistName
- Optimize albums() and genres() methods to query only necessary columns
- Add albumArtist to Audio.areContentsTheSame() comparison

As suggested by Claude
---
 .../datasources/MediaStoreDataSource.kt       | 338 ++++++++++--------
 .../java/org/lineageos/twelve/models/Audio.kt |  12 +
 2 files changed, 206 insertions(+), 144 deletions(-)

diff --git a/app/src/main/java/org/lineageos/twelve/datasources/MediaStoreDataSource.kt b/app/src/main/java/org/lineageos/twelve/datasources/MediaStoreDataSource.kt
index 644f117..3f04ac6 100644
--- a/app/src/main/java/org/lineageos/twelve/datasources/MediaStoreDataSource.kt
+++ b/app/src/main/java/org/lineageos/twelve/datasources/MediaStoreDataSource.kt
@@ -221,61 +221,105 @@ class MediaStoreDataSource(
     }
 
     override fun albums(
-        providerIdentifier: ProviderIdentifier,
-        sortingRule: SortingRule,
+	providerIdentifier: ProviderIdentifier,
+	sortingRule: SortingRule,
     ) = providersManager.flatMapWithInstanceOf(providerIdentifier) {
-        contentResolver.queryFlow(
-            albumsUri,
-            albumsProjection,
-            bundleOf(
-                ContentResolver.QUERY_ARG_SORT_COLUMNS to listOfNotNull(
-                    when (sortingRule.strategy) {
-                        SortingStrategy.ARTIST_NAME -> MediaStore.Audio.AlbumColumns.ARTIST
-                        SortingStrategy.CREATION_DATE -> MediaStore.Audio.AlbumColumns.LAST_YEAR
-                        SortingStrategy.NAME -> MediaStore.Audio.AlbumColumns.ALBUM
-                        else -> null
-                    }?.let { column ->
-                        when (sortingRule.reverse) {
-                            true -> "$column DESC"
-                            false -> column
-                        }
-                    },
-                    MediaStore.Audio.AlbumColumns.ALBUM.takeIf {
-                        sortingRule.strategy != SortingStrategy.NAME
-                    },
-                ).toTypedArray(),
-            )
-        ).mapEachRowToAlbum().mapLatest {
-            Result.Success(it)
-        }
+	contentResolver.queryFlow(
+	    albumsUri,
+	    arrayOf(
+		MediaStore.Audio.AudioColumns._ID,
+		MediaStore.Audio.AlbumColumns.ALBUM,
+		MediaStore.Audio.AlbumColumns.ARTIST_ID,
+		MediaStore.Audio.AlbumColumns.ARTIST,
+		MediaStore.Audio.AlbumColumns.LAST_YEAR,
+	    ),
+	).mapEachRowToAlbumInfo().mapLatest { albumInfos ->
+	    val sortedAlbums = when (sortingRule.strategy) {
+		SortingStrategy.ARTIST_NAME -> {
+		    if (sortingRule.reverse) {
+			albumInfos.sortedByDescending { it.third.second.first }
+		    } else {
+			albumInfos.sortedBy { it.third.second.first }
+		    }
+		}
+		SortingStrategy.CREATION_DATE -> {
+		    if (sortingRule.reverse) {
+			albumInfos.sortedByDescending { it.third.second.second }
+		    } else {
+			albumInfos.sortedBy { it.third.second.second }
+		    }
+		}
+		SortingStrategy.NAME -> {
+		    if (sortingRule.reverse) {
+			albumInfos.sortedByDescending { it.second }
+		    } else {
+			albumInfos.sortedBy { it.second }
+		    }
+		}
+		else -> albumInfos
+	    }
+	    sortedAlbums.mapNotNull { (albumId, album, artistInfo) ->
+		val (artistId, artistPair) = artistInfo
+		val (artist, lastYear) = artistPair
+		val uri = ContentUris.withAppendedId(albumsUri, albumId)
+		val artistUri = ContentUris.withAppendedId(artistsUri, artistId)
+		val albumsArtUri = Uri.Builder()
+		    .scheme(ContentResolver.SCHEME_CONTENT)
+		    .authority(MediaStore.AUTHORITY)
+		    .appendPath("external")
+		    .appendPath("audio")
+		    .appendPath("albumart")
+		    .build()
+		val albumArtUri = ContentUris.withAppendedId(albumsArtUri, albumId)
+		val thumbnail = Thumbnail.Builder()
+		    .setUri(albumArtUri)
+		    .setType(Thumbnail.Type.FRONT_COVER)
+		    .build()
+		Album.Builder(uri)
+		    .setThumbnail(thumbnail)
+		    .setTitle(album?.takeIf { it != MediaStore.UNKNOWN_STRING })
+		    .setArtistUri(artistUri)
+		    .setArtistName(artist?.takeIf { it != MediaStore.UNKNOWN_STRING })
+		    .setYear(lastYear.takeIf { it != 0 })
+		    .build()
+	    }
+	}.mapLatest { albums ->
+	    Result.Success<List<Album>, Error>(albums)
+	}
     }
 
     override fun artists(
-        providerIdentifier: ProviderIdentifier,
-        sortingRule: SortingRule,
+	providerIdentifier: ProviderIdentifier,
+	sortingRule: SortingRule,
     ) = providersManager.flatMapWithInstanceOf(providerIdentifier) {
-        contentResolver.queryFlow(
-            artistsUri,
-            artistsProjection,
-            bundleOf(
-                ContentResolver.QUERY_ARG_SORT_COLUMNS to listOfNotNull(
-                    when (sortingRule.strategy) {
-                        SortingStrategy.NAME -> MediaStore.Audio.ArtistColumns.ARTIST
-                        else -> null
-                    }?.let { column ->
-                        when (sortingRule.reverse) {
-                            true -> "$column DESC"
-                            false -> column
-                        }
-                    },
-                    MediaStore.Audio.ArtistColumns.ARTIST.takeIf {
-                        sortingRule.strategy != SortingStrategy.NAME
-                    },
-                ).toTypedArray(),
-            )
-        ).mapEachRowToArtist().mapLatest {
-            Result.Success(it)
-        }
+	contentResolver.queryFlow(
+	    audiosUri,
+	    arrayOf(
+		MediaStore.Audio.AudioColumns.ARTIST,
+		MediaStore.Audio.AudioColumns.ALBUM_ARTIST,
+	    ),
+	).mapEachRowToArtistNames().mapLatest { artistNames ->
+	    val uniqueArtists = artistNames.distinct()
+	    val sortedArtistNames = when (sortingRule.strategy) {
+		SortingStrategy.NAME -> {
+		    if (sortingRule.reverse) {
+			uniqueArtists.sortedDescending()
+		    } else {
+			uniqueArtists.sorted()
+		    }
+		}
+		else -> uniqueArtists.sorted()
+	    }
+	    sortedArtistNames.mapNotNull { displayName ->
+		val artistId = displayName.hashCode().toLong()
+		val uri = ContentUris.withAppendedId(artistsUri, artistId)
+		Artist.Builder(uri)
+		    .setName(displayName)
+		    .build()
+	    }
+	}.mapLatest { artists ->
+	    Result.Success<List<Artist>, Error>(artists)
+	}
     }
 
     override fun audios(
@@ -309,31 +353,35 @@ class MediaStoreDataSource(
     }
 
     override fun genres(
-        providerIdentifier: ProviderIdentifier,
-        sortingRule: SortingRule,
+	providerIdentifier: ProviderIdentifier,
+	sortingRule: SortingRule,
     ) = providersManager.flatMapWithInstanceOf(providerIdentifier) {
-        contentResolver.queryFlow(
-            genresUri,
-            genresProjection,
-            bundleOf(
-                ContentResolver.QUERY_ARG_SORT_COLUMNS to listOfNotNull(
-                    when (sortingRule.strategy) {
-                        SortingStrategy.NAME -> MediaStore.Audio.GenresColumns.NAME
-                        else -> null
-                    }?.let { column ->
-                        when (sortingRule.reverse) {
-                            true -> "$column DESC"
-                            false -> column
-                        }
-                    },
-                    MediaStore.Audio.GenresColumns.NAME.takeIf {
-                        sortingRule.strategy != SortingStrategy.NAME
-                    },
-                ).toTypedArray(),
-            )
-        ).mapEachRowToGenre().mapLatest {
-            Result.Success(it)
-        }
+	contentResolver.queryFlow(
+	    genresUri,
+	    arrayOf(
+		MediaStore.Audio.AudioColumns._ID,
+		MediaStore.Audio.GenresColumns.NAME,
+	    ),
+	).mapEachRowToGenreInfo().mapLatest { genreInfos ->
+	    val sortedGenres = when (sortingRule.strategy) {
+		SortingStrategy.NAME -> {
+		    if (sortingRule.reverse) {
+			genreInfos.sortedByDescending { it.second }
+		    } else {
+			genreInfos.sortedBy { it.second }
+		    }
+		}
+		else -> genreInfos
+	    }
+	    sortedGenres.mapNotNull { (genreId, name) ->
+		val uri = ContentUris.withAppendedId(genresUri, genreId)
+		Genre.Builder(uri)
+		    .setName(name)
+		    .build()
+	    }
+	}.mapLatest { genres ->
+	    Result.Success<List<Genre>, Error>(genres)
+	}
     }
 
     override fun playlists(
@@ -458,76 +506,54 @@ class MediaStoreDataSource(
     }
 
     override fun artist(artistUri: Uri) = withVolumeName(artistUri) { volumeName ->
-        combine(
-            contentResolver.queryFlow(
-                getArtistsUri(volumeName),
-                artistsProjection,
-                bundleOf(
-                    ContentResolver.QUERY_ARG_SQL_SELECTION to query {
-                        MediaStore.Audio.AudioColumns._ID eq Query.ARG
-                    },
-                    ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf(
-                        ContentUris.parseId(artistUri).toString(),
-                    ),
-                )
-            ).mapEachRowToArtist(volumeName),
-            contentResolver.queryFlow(
-                getAlbumsUri(volumeName),
-                albumsProjection,
-                bundleOf(
-                    ContentResolver.QUERY_ARG_SQL_SELECTION to query {
-                        MediaStore.Audio.AlbumColumns.ARTIST_ID eq Query.ARG
-                    },
-                    ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf(
-                        ContentUris.parseId(artistUri).toString(),
-                    ),
-                )
-            ).mapEachRowToAlbum(volumeName),
-            contentResolver.queryFlow(
-                getAudiosUri(volumeName),
-                audioAlbumIdsProjection,
-                bundleOf(
-                    ContentResolver.QUERY_ARG_SQL_SELECTION to query {
-                        MediaStore.Audio.AudioColumns.ARTIST_ID eq Query.ARG
-                    },
-                    ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf(
-                        ContentUris.parseId(artistUri).toString(),
-                    ),
-                    ContentResolver.QUERY_ARG_SQL_GROUP_BY to MediaStore.Audio.AudioColumns.ALBUM_ID,
-                )
-            ).mapEachRow {
-                it.getLong(MediaStore.Audio.AudioColumns.ALBUM_ID)
-            }.flatMapLatest { albumIds ->
-                contentResolver.queryFlow(
-                    getAlbumsUri(volumeName),
-                    albumsProjection,
-                    bundleOf(
-                        ContentResolver.QUERY_ARG_SQL_SELECTION to query {
-                            (MediaStore.Audio.AudioColumns.ARTIST_ID neq Query.ARG) and
-                                    (MediaStore.Audio.AudioColumns._ID `in` List(albumIds.size) {
-                                        Query.ARG
-                                    })
-                        },
-                        ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf(
-                            ContentUris.parseId(artistUri).toString(),
-                            *albumIds
-                                .map { it.toString() }
-                                .toTypedArray(),
-                        ),
-                    )
-                ).mapEachRowToAlbum(volumeName)
-            }
-        ) { artists, albums, appearsInAlbum ->
-            artists.firstOrNull()?.let { artist ->
-                val artistWorks = ArtistWorks(
-                    albums,
-                    appearsInAlbum,
-                    listOf(),
-                )
-
-                Result.Success(artist to artistWorks)
-            } ?: Result.Error(Error.NOT_FOUND)
-        }
+	val targetArtistId = ContentUris.parseId(artistUri)
+	contentResolver.queryFlow(
+	    getAudiosUri(volumeName),
+	    audiosProjection,
+	).mapEachRowToAudio(volumeName).flatMapLatest { audios ->
+	    val matchingAudios = audios.filter { audio ->
+		val artistName = audio.albumArtist ?: audio.artistName ?: "Unknown Artist"
+		artistName.hashCode().toLong() == targetArtistId
+	    }
+	    if (matchingAudios.isEmpty()) {
+		return@flatMapLatest flowOf(
+		    Result.Error<Pair<Artist, ArtistWorks>, Error>(Error.NOT_FOUND)
+		)
+	    }
+	    val artistName = matchingAudios.first().let { audio ->
+		audio.albumArtist ?: audio.artistName ?: "Unknown Artist"
+	    }
+	    val albumUris = matchingAudios.mapNotNull { it.albumUri }.distinct()
+	    if (albumUris.isEmpty()) {
+		return@flatMapLatest flowOf(
+		    Result.Error<Pair<Artist, ArtistWorks>, Error>(Error.NOT_FOUND)
+		)
+	    }
+	    contentResolver.queryFlow(
+		getAlbumsUri(volumeName),
+		albumsProjection,
+		bundleOf(
+		    ContentResolver.QUERY_ARG_SQL_SELECTION to buildString {
+			append("${MediaStore.Audio.AudioColumns.ALBUM_ID} IN (")
+			append(List(albumUris.size) { "?" }.joinToString(","))
+			append(")")
+		    },
+		    ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to albumUris.map {
+			ContentUris.parseId(it).toString()
+		    }.toTypedArray(),
+		)
+	    ).mapEachRowToAlbum(volumeName).mapLatest { albums ->
+		val artist = Artist.Builder(artistUri)
+		    .setName(artistName)
+		    .build()
+		val artistWorks = ArtistWorks(
+		    albums,
+		    listOf(),  // appearsInAlbum
+		    listOf(),  // genres
+		)
+		Result.Success<Pair<Artist, ArtistWorks>, Error>(artist to artistWorks)
+	    }
+	}
     }
 
     override fun genre(genreUri: Uri) = withVolumeName(genreUri) { volumeName ->
@@ -825,6 +851,15 @@ class MediaStoreDataSource(
         }
     }
 
+    private fun Flow<Cursor?>.mapEachRowToAlbumInfo() = mapEachRow { columnIndexCache ->
+	val albumId = columnIndexCache.getLong(MediaStore.Audio.AudioColumns._ID)
+	val album = columnIndexCache.getStringOrNull(MediaStore.Audio.AlbumColumns.ALBUM)
+	val artistId = columnIndexCache.getLong(MediaStore.Audio.AlbumColumns.ARTIST_ID)
+	val artist = columnIndexCache.getStringOrNull(MediaStore.Audio.AlbumColumns.ARTIST)
+	val lastYear = columnIndexCache.getInt(MediaStore.Audio.AlbumColumns.LAST_YEAR)
+	Triple(albumId, album, Pair(artistId, artist to lastYear))
+    }
+
     private fun Flow<Cursor?>.mapEachRowToArtist(volumeName: String) = run {
         val artistsUri = MediaStore.Audio.Artists.getContentUri(volumeName)
 
@@ -840,6 +875,12 @@ class MediaStoreDataSource(
         }
     }
 
+    private fun Flow<Cursor?>.mapEachRowToArtistNames() = mapEachRow { columnIndexCache ->
+	val artist = columnIndexCache.getStringOrNull(MediaStore.Audio.AudioColumns.ARTIST)
+	val albumArtist = columnIndexCache.getStringOrNull(MediaStore.Audio.AudioColumns.ALBUM_ARTIST)
+	albumArtist ?: artist ?: "Unknown Artist"
+    }
+
     private fun Flow<Cursor?>.mapEachRowToAudio(volumeName: String) = run {
         val audiosUri = MediaStore.Audio.Media.getContentUri(volumeName)
         val artistsUri = MediaStore.Audio.Artists.getContentUri(volumeName)
@@ -859,6 +900,7 @@ class MediaStoreDataSource(
             val artist = columnIndexCache.getStringOrNull(MediaStore.Audio.AudioColumns.ARTIST)
             val albumId = columnIndexCache.getLong(MediaStore.Audio.AudioColumns.ALBUM_ID)
             val album = columnIndexCache.getStringOrNull(MediaStore.Audio.AudioColumns.ALBUM)
+            val albumArtist = columnIndexCache.getStringOrNull(MediaStore.Audio.AudioColumns.ALBUM_ARTIST)
             val track = columnIndexCache.getInt(MediaStore.Audio.AudioColumns.TRACK)
             val genreId = columnIndexCache.getLong(MediaStore.Audio.AudioColumns.GENRE_ID)
             val genre = columnIndexCache.getStringOrNull(MediaStore.Audio.AudioColumns.GENRE)
@@ -910,6 +952,7 @@ class MediaStoreDataSource(
                 .setArtistName(artist?.takeIf { it != MediaStore.UNKNOWN_STRING })
                 .setAlbumUri(albumUri)
                 .setAlbumTitle(album?.takeIf { it != MediaStore.UNKNOWN_STRING })
+                .setAlbumArtist(albumArtist?.takeIf { it != MediaStore.UNKNOWN_STRING })
                 .setDiscNumber(discNumber)
                 .setTrackNumber(discTrack)
                 .setGenreUri(genreUri)
@@ -947,6 +990,12 @@ class MediaStoreDataSource(
         }
     }
 
+    private fun Flow<Cursor?>.mapEachRowToGenreInfo() = mapEachRow { columnIndexCache ->
+	val genreId = columnIndexCache.getLong(MediaStore.Audio.AudioColumns._ID)
+	val name = columnIndexCache.getStringOrNull(MediaStore.Audio.GenresColumns.NAME)
+	genreId to name
+    }
+
     companion object {
         // packages/providers/MediaProvider/src/com/android/providers/media/LocalUriMatcher.java
         private const val AUDIO_ALBUMART = "albumart"
@@ -981,6 +1030,7 @@ class MediaStoreDataSource(
             MediaStore.Audio.AudioColumns.ARTIST,
             MediaStore.Audio.AudioColumns.ALBUM_ID,
             MediaStore.Audio.AudioColumns.ALBUM,
+            MediaStore.Audio.AudioColumns.ALBUM_ARTIST,
             MediaStore.Audio.AudioColumns.TRACK,
             MediaStore.Audio.AudioColumns.GENRE_ID,
             MediaStore.Audio.AudioColumns.GENRE,
diff --git a/app/src/main/java/org/lineageos/twelve/models/Audio.kt b/app/src/main/java/org/lineageos/twelve/models/Audio.kt
index 5429c47..d8ce6b0 100644
--- a/app/src/main/java/org/lineageos/twelve/models/Audio.kt
+++ b/app/src/main/java/org/lineageos/twelve/models/Audio.kt
@@ -27,6 +27,7 @@ import org.lineageos.twelve.ext.toByteArray
  * @param artistName The name of the artist of the audio
  * @param albumUri The URI of the album of the audio
  * @param albumTitle The title of the album of the audio
+ * @param albumArtist The artist for grouping tracks together
  * @param discNumber The number of the disc where the album is present, starts from 1
  * @param trackNumber The track number of the audio within the disc, starts from 1
  * @param genreUri The URI of the genre of the audio
@@ -46,6 +47,7 @@ data class Audio(
     val artistName: String?,
     val albumUri: Uri?,
     val albumTitle: String?,
+    val albumArtist: String?,
     val discNumber: Int?,
     val trackNumber: Int?,
     val genreUri: Uri?,
@@ -89,6 +91,7 @@ data class Audio(
         Audio::durationMs,
         Audio::artistUri,
         Audio::artistName,
+        Audio::albumArtist,
         Audio::albumUri,
         Audio::albumTitle,
         Audio::discNumber,
@@ -129,6 +132,7 @@ data class Audio(
         private var artistName: String? = null
         private var albumUri: Uri? = null
         private var albumTitle: String? = null
+        private var albumArtist: String? = null
         private var discNumber: Int? = null
         private var trackNumber: Int? = null
         private var genreUri: Uri? = null
@@ -199,6 +203,13 @@ data class Audio(
             this.albumTitle = albumTitle
         }
 
+	/**
+         * @see Audio.albumArtist
+         */
+        fun setAlbumArtist(albumArtist: String?) = this.also {
+            this.albumArtist = albumArtist
+        }
+
         /**
          * @see Audio.discNumber
          */
@@ -253,6 +264,7 @@ data class Audio(
             artistName = artistName,
             albumUri = albumUri,
             albumTitle = albumTitle,
+            albumArtist = albumArtist,
             discNumber = discNumber,
             trackNumber = trackNumber,
             genreUri = genreUri,
-- 
2.50.1 (Apple Git-155)

Or if you prefer the simple commit:

diff --git a/app/src/main/java/org/lineageos/twelve/datasources/MediaStoreDataSource.kt b/app/src/main/java/org/lineageos/twelve/datasources/MediaStoreDataSource.kt
index 644f117..3f04ac6 100644
--- a/app/src/main/java/org/lineageos/twelve/datasources/MediaStoreDataSource.kt
+++ b/app/src/main/java/org/lineageos/twelve/datasources/MediaStoreDataSource.kt
@@ -221,61 +221,105 @@ class MediaStoreDataSource(
     }
 
     override fun albums(
-        providerIdentifier: ProviderIdentifier,
-        sortingRule: SortingRule,
+	providerIdentifier: ProviderIdentifier,
+	sortingRule: SortingRule,
     ) = providersManager.flatMapWithInstanceOf(providerIdentifier) {
-        contentResolver.queryFlow(
-            albumsUri,
-            albumsProjection,
-            bundleOf(
-                ContentResolver.QUERY_ARG_SORT_COLUMNS to listOfNotNull(
-                    when (sortingRule.strategy) {
-                        SortingStrategy.ARTIST_NAME -> MediaStore.Audio.AlbumColumns.ARTIST
-                        SortingStrategy.CREATION_DATE -> MediaStore.Audio.AlbumColumns.LAST_YEAR
-                        SortingStrategy.NAME -> MediaStore.Audio.AlbumColumns.ALBUM
-                        else -> null
-                    }?.let { column ->
-                        when (sortingRule.reverse) {
-                            true -> "$column DESC"
-                            false -> column
-                        }
-                    },
-                    MediaStore.Audio.AlbumColumns.ALBUM.takeIf {
-                        sortingRule.strategy != SortingStrategy.NAME
-                    },
-                ).toTypedArray(),
-            )
-        ).mapEachRowToAlbum().mapLatest {
-            Result.Success(it)
-        }
+	contentResolver.queryFlow(
+	    albumsUri,
+	    arrayOf(
+		MediaStore.Audio.AudioColumns._ID,
+		MediaStore.Audio.AlbumColumns.ALBUM,
+		MediaStore.Audio.AlbumColumns.ARTIST_ID,
+		MediaStore.Audio.AlbumColumns.ARTIST,
+		MediaStore.Audio.AlbumColumns.LAST_YEAR,
+	    ),
+	).mapEachRowToAlbumInfo().mapLatest { albumInfos ->
+	    val sortedAlbums = when (sortingRule.strategy) {
+		SortingStrategy.ARTIST_NAME -> {
+		    if (sortingRule.reverse) {
+			albumInfos.sortedByDescending { it.third.second.first }
+		    } else {
+			albumInfos.sortedBy { it.third.second.first }
+		    }
+		}
+		SortingStrategy.CREATION_DATE -> {
+		    if (sortingRule.reverse) {
+			albumInfos.sortedByDescending { it.third.second.second }
+		    } else {
+			albumInfos.sortedBy { it.third.second.second }
+		    }
+		}
+		SortingStrategy.NAME -> {
+		    if (sortingRule.reverse) {
+			albumInfos.sortedByDescending { it.second }
+		    } else {
+			albumInfos.sortedBy { it.second }
+		    }
+		}
+		else -> albumInfos
+	    }
+	    sortedAlbums.mapNotNull { (albumId, album, artistInfo) ->
+		val (artistId, artistPair) = artistInfo
+		val (artist, lastYear) = artistPair
+		val uri = ContentUris.withAppendedId(albumsUri, albumId)
+		val artistUri = ContentUris.withAppendedId(artistsUri, artistId)
+		val albumsArtUri = Uri.Builder()
+		    .scheme(ContentResolver.SCHEME_CONTENT)
+		    .authority(MediaStore.AUTHORITY)
+		    .appendPath("external")
+		    .appendPath("audio")
+		    .appendPath("albumart")
+		    .build()
+		val albumArtUri = ContentUris.withAppendedId(albumsArtUri, albumId)
+		val thumbnail = Thumbnail.Builder()
+		    .setUri(albumArtUri)
+		    .setType(Thumbnail.Type.FRONT_COVER)
+		    .build()
+		Album.Builder(uri)
+		    .setThumbnail(thumbnail)
+		    .setTitle(album?.takeIf { it != MediaStore.UNKNOWN_STRING })
+		    .setArtistUri(artistUri)
+		    .setArtistName(artist?.takeIf { it != MediaStore.UNKNOWN_STRING })
+		    .setYear(lastYear.takeIf { it != 0 })
+		    .build()
+	    }
+	}.mapLatest { albums ->
+	    Result.Success<List<Album>, Error>(albums)
+	}
     }
 
     override fun artists(
-        providerIdentifier: ProviderIdentifier,
-        sortingRule: SortingRule,
+	providerIdentifier: ProviderIdentifier,
+	sortingRule: SortingRule,
     ) = providersManager.flatMapWithInstanceOf(providerIdentifier) {
-        contentResolver.queryFlow(
-            artistsUri,
-            artistsProjection,
-            bundleOf(
-                ContentResolver.QUERY_ARG_SORT_COLUMNS to listOfNotNull(
-                    when (sortingRule.strategy) {
-                        SortingStrategy.NAME -> MediaStore.Audio.ArtistColumns.ARTIST
-                        else -> null
-                    }?.let { column ->
-                        when (sortingRule.reverse) {
-                            true -> "$column DESC"
-                            false -> column
-                        }
-                    },
-                    MediaStore.Audio.ArtistColumns.ARTIST.takeIf {
-                        sortingRule.strategy != SortingStrategy.NAME
-                    },
-                ).toTypedArray(),
-            )
-        ).mapEachRowToArtist().mapLatest {
-            Result.Success(it)
-        }
+	contentResolver.queryFlow(
+	    audiosUri,
+	    arrayOf(
+		MediaStore.Audio.AudioColumns.ARTIST,
+		MediaStore.Audio.AudioColumns.ALBUM_ARTIST,
+	    ),
+	).mapEachRowToArtistNames().mapLatest { artistNames ->
+	    val uniqueArtists = artistNames.distinct()
+	    val sortedArtistNames = when (sortingRule.strategy) {
+		SortingStrategy.NAME -> {
+		    if (sortingRule.reverse) {
+			uniqueArtists.sortedDescending()
+		    } else {
+			uniqueArtists.sorted()
+		    }
+		}
+		else -> uniqueArtists.sorted()
+	    }
+	    sortedArtistNames.mapNotNull { displayName ->
+		val artistId = displayName.hashCode().toLong()
+		val uri = ContentUris.withAppendedId(artistsUri, artistId)
+		Artist.Builder(uri)
+		    .setName(displayName)
+		    .build()
+	    }
+	}.mapLatest { artists ->
+	    Result.Success<List<Artist>, Error>(artists)
+	}
     }
 
     override fun audios(
@@ -309,31 +353,35 @@ class MediaStoreDataSource(
     }
 
     override fun genres(
-        providerIdentifier: ProviderIdentifier,
-        sortingRule: SortingRule,
+	providerIdentifier: ProviderIdentifier,
+	sortingRule: SortingRule,
     ) = providersManager.flatMapWithInstanceOf(providerIdentifier) {
-        contentResolver.queryFlow(
-            genresUri,
-            genresProjection,
-            bundleOf(
-                ContentResolver.QUERY_ARG_SORT_COLUMNS to listOfNotNull(
-                    when (sortingRule.strategy) {
-                        SortingStrategy.NAME -> MediaStore.Audio.GenresColumns.NAME
-                        else -> null
-                    }?.let { column ->
-                        when (sortingRule.reverse) {
-                            true -> "$column DESC"
-                            false -> column
-                        }
-                    },
-                    MediaStore.Audio.GenresColumns.NAME.takeIf {
-                        sortingRule.strategy != SortingStrategy.NAME
-                    },
-                ).toTypedArray(),
-            )
-        ).mapEachRowToGenre().mapLatest {
-            Result.Success(it)
-        }
+	contentResolver.queryFlow(
+	    genresUri,
+	    arrayOf(
+		MediaStore.Audio.AudioColumns._ID,
+		MediaStore.Audio.GenresColumns.NAME,
+	    ),
+	).mapEachRowToGenreInfo().mapLatest { genreInfos ->
+	    val sortedGenres = when (sortingRule.strategy) {
+		SortingStrategy.NAME -> {
+		    if (sortingRule.reverse) {
+			genreInfos.sortedByDescending { it.second }
+		    } else {
+			genreInfos.sortedBy { it.second }
+		    }
+		}
+		else -> genreInfos
+	    }
+	    sortedGenres.mapNotNull { (genreId, name) ->
+		val uri = ContentUris.withAppendedId(genresUri, genreId)
+		Genre.Builder(uri)
+		    .setName(name)
+		    .build()
+	    }
+	}.mapLatest { genres ->
+	    Result.Success<List<Genre>, Error>(genres)
+	}
     }
 
     override fun playlists(
@@ -458,76 +506,54 @@ class MediaStoreDataSource(
     }
 
     override fun artist(artistUri: Uri) = withVolumeName(artistUri) { volumeName ->
-        combine(
-            contentResolver.queryFlow(
-                getArtistsUri(volumeName),
-                artistsProjection,
-                bundleOf(
-                    ContentResolver.QUERY_ARG_SQL_SELECTION to query {
-                        MediaStore.Audio.AudioColumns._ID eq Query.ARG
-                    },
-                    ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf(
-                        ContentUris.parseId(artistUri).toString(),
-                    ),
-                )
-            ).mapEachRowToArtist(volumeName),
-            contentResolver.queryFlow(
-                getAlbumsUri(volumeName),
-                albumsProjection,
-                bundleOf(
-                    ContentResolver.QUERY_ARG_SQL_SELECTION to query {
-                        MediaStore.Audio.AlbumColumns.ARTIST_ID eq Query.ARG
-                    },
-                    ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf(
-                        ContentUris.parseId(artistUri).toString(),
-                    ),
-                )
-            ).mapEachRowToAlbum(volumeName),
-            contentResolver.queryFlow(
-                getAudiosUri(volumeName),
-                audioAlbumIdsProjection,
-                bundleOf(
-                    ContentResolver.QUERY_ARG_SQL_SELECTION to query {
-                        MediaStore.Audio.AudioColumns.ARTIST_ID eq Query.ARG
-                    },
-                    ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf(
-                        ContentUris.parseId(artistUri).toString(),
-                    ),
-                    ContentResolver.QUERY_ARG_SQL_GROUP_BY to MediaStore.Audio.AudioColumns.ALBUM_ID,
-                )
-            ).mapEachRow {
-                it.getLong(MediaStore.Audio.AudioColumns.ALBUM_ID)
-            }.flatMapLatest { albumIds ->
-                contentResolver.queryFlow(
-                    getAlbumsUri(volumeName),
-                    albumsProjection,
-                    bundleOf(
-                        ContentResolver.QUERY_ARG_SQL_SELECTION to query {
-                            (MediaStore.Audio.AudioColumns.ARTIST_ID neq Query.ARG) and
-                                    (MediaStore.Audio.AudioColumns._ID `in` List(albumIds.size) {
-                                        Query.ARG
-                                    })
-                        },
-                        ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf(
-                            ContentUris.parseId(artistUri).toString(),
-                            *albumIds
-                                .map { it.toString() }
-                                .toTypedArray(),
-                        ),
-                    )
-                ).mapEachRowToAlbum(volumeName)
-            }
-        ) { artists, albums, appearsInAlbum ->
-            artists.firstOrNull()?.let { artist ->
-                val artistWorks = ArtistWorks(
-                    albums,
-                    appearsInAlbum,
-                    listOf(),
-                )
-
-                Result.Success(artist to artistWorks)
-            } ?: Result.Error(Error.NOT_FOUND)
-        }
+	val targetArtistId = ContentUris.parseId(artistUri)
+	contentResolver.queryFlow(
+	    getAudiosUri(volumeName),
+	    audiosProjection,
+	).mapEachRowToAudio(volumeName).flatMapLatest { audios ->
+	    val matchingAudios = audios.filter { audio ->
+		val artistName = audio.albumArtist ?: audio.artistName ?: "Unknown Artist"
+		artistName.hashCode().toLong() == targetArtistId
+	    }
+	    if (matchingAudios.isEmpty()) {
+		return@flatMapLatest flowOf(
+		    Result.Error<Pair<Artist, ArtistWorks>, Error>(Error.NOT_FOUND)
+		)
+	    }
+	    val artistName = matchingAudios.first().let { audio ->
+		audio.albumArtist ?: audio.artistName ?: "Unknown Artist"
+	    }
+	    val albumUris = matchingAudios.mapNotNull { it.albumUri }.distinct()
+	    if (albumUris.isEmpty()) {
+		return@flatMapLatest flowOf(
+		    Result.Error<Pair<Artist, ArtistWorks>, Error>(Error.NOT_FOUND)
+		)
+	    }
+	    contentResolver.queryFlow(
+		getAlbumsUri(volumeName),
+		albumsProjection,
+		bundleOf(
+		    ContentResolver.QUERY_ARG_SQL_SELECTION to buildString {
+			append("${MediaStore.Audio.AudioColumns.ALBUM_ID} IN (")
+			append(List(albumUris.size) { "?" }.joinToString(","))
+			append(")")
+		    },
+		    ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to albumUris.map {
+			ContentUris.parseId(it).toString()
+		    }.toTypedArray(),
+		)
+	    ).mapEachRowToAlbum(volumeName).mapLatest { albums ->
+		val artist = Artist.Builder(artistUri)
+		    .setName(artistName)
+		    .build()
+		val artistWorks = ArtistWorks(
+		    albums,
+		    listOf(),  // appearsInAlbum
+		    listOf(),  // genres
+		)
+		Result.Success<Pair<Artist, ArtistWorks>, Error>(artist to artistWorks)
+	    }
+	}
     }
 
     override fun genre(genreUri: Uri) = withVolumeName(genreUri) { volumeName ->
@@ -825,6 +851,15 @@ class MediaStoreDataSource(
         }
     }
 
+    private fun Flow<Cursor?>.mapEachRowToAlbumInfo() = mapEachRow { columnIndexCache ->
+	val albumId = columnIndexCache.getLong(MediaStore.Audio.AudioColumns._ID)
+	val album = columnIndexCache.getStringOrNull(MediaStore.Audio.AlbumColumns.ALBUM)
+	val artistId = columnIndexCache.getLong(MediaStore.Audio.AlbumColumns.ARTIST_ID)
+	val artist = columnIndexCache.getStringOrNull(MediaStore.Audio.AlbumColumns.ARTIST)
+	val lastYear = columnIndexCache.getInt(MediaStore.Audio.AlbumColumns.LAST_YEAR)
+	Triple(albumId, album, Pair(artistId, artist to lastYear))
+    }
+
     private fun Flow<Cursor?>.mapEachRowToArtist(volumeName: String) = run {
         val artistsUri = MediaStore.Audio.Artists.getContentUri(volumeName)
 
@@ -840,6 +875,12 @@ class MediaStoreDataSource(
         }
     }
 
+    private fun Flow<Cursor?>.mapEachRowToArtistNames() = mapEachRow { columnIndexCache ->
+	val artist = columnIndexCache.getStringOrNull(MediaStore.Audio.AudioColumns.ARTIST)
+	val albumArtist = columnIndexCache.getStringOrNull(MediaStore.Audio.AudioColumns.ALBUM_ARTIST)
+	albumArtist ?: artist ?: "Unknown Artist"
+    }
+
     private fun Flow<Cursor?>.mapEachRowToAudio(volumeName: String) = run {
         val audiosUri = MediaStore.Audio.Media.getContentUri(volumeName)
         val artistsUri = MediaStore.Audio.Artists.getContentUri(volumeName)
@@ -859,6 +900,7 @@ class MediaStoreDataSource(
             val artist = columnIndexCache.getStringOrNull(MediaStore.Audio.AudioColumns.ARTIST)
             val albumId = columnIndexCache.getLong(MediaStore.Audio.AudioColumns.ALBUM_ID)
             val album = columnIndexCache.getStringOrNull(MediaStore.Audio.AudioColumns.ALBUM)
+            val albumArtist = columnIndexCache.getStringOrNull(MediaStore.Audio.AudioColumns.ALBUM_ARTIST)
             val track = columnIndexCache.getInt(MediaStore.Audio.AudioColumns.TRACK)
             val genreId = columnIndexCache.getLong(MediaStore.Audio.AudioColumns.GENRE_ID)
             val genre = columnIndexCache.getStringOrNull(MediaStore.Audio.AudioColumns.GENRE)
@@ -910,6 +952,7 @@ class MediaStoreDataSource(
                 .setArtistName(artist?.takeIf { it != MediaStore.UNKNOWN_STRING })
                 .setAlbumUri(albumUri)
                 .setAlbumTitle(album?.takeIf { it != MediaStore.UNKNOWN_STRING })
+                .setAlbumArtist(albumArtist?.takeIf { it != MediaStore.UNKNOWN_STRING })
                 .setDiscNumber(discNumber)
                 .setTrackNumber(discTrack)
                 .setGenreUri(genreUri)
@@ -947,6 +990,12 @@ class MediaStoreDataSource(
         }
     }
 
+    private fun Flow<Cursor?>.mapEachRowToGenreInfo() = mapEachRow { columnIndexCache ->
+	val genreId = columnIndexCache.getLong(MediaStore.Audio.AudioColumns._ID)
+	val name = columnIndexCache.getStringOrNull(MediaStore.Audio.GenresColumns.NAME)
+	genreId to name
+    }
+
     companion object {
         // packages/providers/MediaProvider/src/com/android/providers/media/LocalUriMatcher.java
         private const val AUDIO_ALBUMART = "albumart"
@@ -981,6 +1030,7 @@ class MediaStoreDataSource(
             MediaStore.Audio.AudioColumns.ARTIST,
             MediaStore.Audio.AudioColumns.ALBUM_ID,
             MediaStore.Audio.AudioColumns.ALBUM,
+            MediaStore.Audio.AudioColumns.ALBUM_ARTIST,
             MediaStore.Audio.AudioColumns.TRACK,
             MediaStore.Audio.AudioColumns.GENRE_ID,
             MediaStore.Audio.AudioColumns.GENRE,
diff --git a/app/src/main/java/org/lineageos/twelve/models/Audio.kt b/app/src/main/java/org/lineageos/twelve/models/Audio.kt
index 5429c47..d8ce6b0 100644
--- a/app/src/main/java/org/lineageos/twelve/models/Audio.kt
+++ b/app/src/main/java/org/lineageos/twelve/models/Audio.kt
@@ -27,6 +27,7 @@ import org.lineageos.twelve.ext.toByteArray
  * @param artistName The name of the artist of the audio
  * @param albumUri The URI of the album of the audio
  * @param albumTitle The title of the album of the audio
+ * @param albumArtist The artist for grouping tracks together
  * @param discNumber The number of the disc where the album is present, starts from 1
  * @param trackNumber The track number of the audio within the disc, starts from 1
  * @param genreUri The URI of the genre of the audio
@@ -46,6 +47,7 @@ data class Audio(
     val artistName: String?,
     val albumUri: Uri?,
     val albumTitle: String?,
+    val albumArtist: String?,
     val discNumber: Int?,
     val trackNumber: Int?,
     val genreUri: Uri?,
@@ -89,6 +91,7 @@ data class Audio(
         Audio::durationMs,
         Audio::artistUri,
         Audio::artistName,
+        Audio::albumArtist,
         Audio::albumUri,
         Audio::albumTitle,
         Audio::discNumber,
@@ -129,6 +132,7 @@ data class Audio(
         private var artistName: String? = null
         private var albumUri: Uri? = null
         private var albumTitle: String? = null
+        private var albumArtist: String? = null
         private var discNumber: Int? = null
         private var trackNumber: Int? = null
         private var genreUri: Uri? = null
@@ -199,6 +203,13 @@ data class Audio(
             this.albumTitle = albumTitle
         }
 
+	/**
+         * @see Audio.albumArtist
+         */
+        fun setAlbumArtist(albumArtist: String?) = this.also {
+            this.albumArtist = albumArtist
+        }
+
         /**
          * @see Audio.discNumber
          */
@@ -253,6 +264,7 @@ data class Audio(
             artistName = artistName,
             albumUri = albumUri,
             albumTitle = albumTitle,
+            albumArtist = albumArtist,
             discNumber = discNumber,
             trackNumber = trackNumber,
             genreUri = genreUri,
Edited by Sylvain Saboua