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