Twelve: Finish genre backend implementation

Co-authored-by: Luca Stefani <luca.stefani.ge1@gmail.com>
Change-Id: I4a263dfece3e2509a7d14fbe8eaef4d610e89267
diff --git a/app/src/main/java/org/lineageos/twelve/datasources/LocalDataSource.kt b/app/src/main/java/org/lineageos/twelve/datasources/LocalDataSource.kt
index 014db24..a3df640 100644
--- a/app/src/main/java/org/lineageos/twelve/datasources/LocalDataSource.kt
+++ b/app/src/main/java/org/lineageos/twelve/datasources/LocalDataSource.kt
@@ -30,6 +30,7 @@
 import org.lineageos.twelve.models.ArtistWorks
 import org.lineageos.twelve.models.Audio
 import org.lineageos.twelve.models.Genre
+import org.lineageos.twelve.models.GenreContent
 import org.lineageos.twelve.models.MediaType
 import org.lineageos.twelve.models.Playlist
 import org.lineageos.twelve.models.RequestStatus
@@ -38,6 +39,7 @@
 import org.lineageos.twelve.query.and
 import org.lineageos.twelve.query.eq
 import org.lineageos.twelve.query.`in`
+import org.lineageos.twelve.query.`is`
 import org.lineageos.twelve.query.like
 import org.lineageos.twelve.query.neq
 import org.lineageos.twelve.query.query
@@ -402,35 +404,95 @@
         } ?: RequestStatus.Error(MediaError.NOT_FOUND)
     }
 
-    override fun genre(genreUri: Uri) = combine(
-        contentResolver.queryFlow(
-            genresUri,
-            genresProjection,
-            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(genreUri).toString(),
-                ),
-            )
-        ).mapEachRow(genresProjection, mapGenre),
-        contentResolver.queryFlow(
-            audiosUri,
-            audiosProjection,
-            bundleOf(
-                ContentResolver.QUERY_ARG_SQL_SELECTION to query {
-                    MediaStore.Audio.AudioColumns.GENRE_ID eq Query.ARG
-                },
-                ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf(
-                    ContentUris.parseId(genreUri).toString(),
-                ),
-            )
-        ).mapEachRow(audiosProjection, mapAudio)
-    ) { genres, audios ->
-        genres.firstOrNull()?.let {
-            RequestStatus.Success<_, MediaError>(Pair(it, audios))
-        } ?: RequestStatus.Error(MediaError.NOT_FOUND)
+    override fun genre(genreUri: Uri) = ContentUris.parseId(genreUri).let { genreId ->
+        val (genreSelection, genreSelectionArgs) = when (genreId) {
+            0L -> (MediaStore.Audio.AudioColumns.GENRE_ID `is` Query.NULL) to arrayOf()
+
+            else -> (MediaStore.Audio.AudioColumns.GENRE_ID eq Query.ARG) to
+                    arrayOf(genreId.toString())
+        }
+
+        combine(
+            contentResolver.queryFlow(
+                genresUri,
+                genresProjection,
+                bundleOf(
+                    ContentResolver.QUERY_ARG_SQL_SELECTION to query {
+                        when (genreId) {
+                            0L -> MediaStore.Audio.AudioColumns._ID `is` Query.NULL
+                            else -> MediaStore.Audio.AudioColumns._ID eq Query.ARG
+                        }
+                    },
+                    ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf(
+                        *when (genreId) {
+                            0L -> arrayOf()
+                            else -> arrayOf(genreId.toString())
+                        }
+                    ),
+                )
+            ).mapEachRow(genresProjection, mapGenre),
+            contentResolver.queryFlow(
+                audiosUri,
+                audioAlbumIdsProjection,
+                bundleOf(
+                    ContentResolver.QUERY_ARG_SQL_SELECTION to query {
+                        genreSelection
+                    },
+                    ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf(
+                        *genreSelectionArgs,
+                    ),
+                    ContentResolver.QUERY_ARG_SQL_GROUP_BY to
+                            MediaStore.Audio.AudioColumns.ALBUM_ID,
+                )
+            ).mapEachRow(audioAlbumIdsProjection) { it, indexCache ->
+                // albumId
+                it.getLong(indexCache[0])
+            }.flatMapLatest { albumIds ->
+                contentResolver.queryFlow(
+                    albumsUri,
+                    albumsProjection,
+                    bundleOf(
+                        ContentResolver.QUERY_ARG_SQL_SELECTION to query {
+                            MediaStore.Audio.AudioColumns._ID `in` List(albumIds.size) {
+                                Query.ARG
+                            }
+                        },
+                        ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf(
+                            *albumIds
+                                .map { it.toString() }
+                                .toTypedArray(),
+                        ),
+                    )
+                ).mapEachRow(albumsProjection, mapAlbum)
+            },
+            contentResolver.queryFlow(
+                audiosUri,
+                audiosProjection,
+                bundleOf(
+                    ContentResolver.QUERY_ARG_SQL_SELECTION to query {
+                        genreSelection
+                    },
+                    ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf(
+                        *genreSelectionArgs,
+                    ),
+                )
+            ).mapEachRow(audiosProjection, mapAudio)
+        ) { genres, appearsInAlbums, audios ->
+            val genre = genres.firstOrNull() ?: when (genreId) {
+                0L -> Genre(genreUri, null)
+                else -> null
+            }
+
+            genre?.let {
+                val genreContent = GenreContent(
+                    appearsInAlbums,
+                    listOf(),
+                    audios,
+                )
+
+                RequestStatus.Success<_, MediaError>(Pair(it, genreContent))
+            } ?: RequestStatus.Error(MediaError.NOT_FOUND)
+        }
     }
 
     override fun playlist(playlistUri: Uri) = database.getPlaylistDao().getPlaylistWithItems(
diff --git a/app/src/main/java/org/lineageos/twelve/datasources/MediaDataSource.kt b/app/src/main/java/org/lineageos/twelve/datasources/MediaDataSource.kt
index 5fdcb70..1b04633 100644
--- a/app/src/main/java/org/lineageos/twelve/datasources/MediaDataSource.kt
+++ b/app/src/main/java/org/lineageos/twelve/datasources/MediaDataSource.kt
@@ -12,6 +12,7 @@
 import org.lineageos.twelve.models.ArtistWorks
 import org.lineageos.twelve.models.Audio
 import org.lineageos.twelve.models.Genre
+import org.lineageos.twelve.models.GenreContent
 import org.lineageos.twelve.models.MediaItem
 import org.lineageos.twelve.models.MediaType
 import org.lineageos.twelve.models.Playlist
@@ -83,7 +84,7 @@
     /**
      * Get the genre information and all the tracks of the given genre.
      */
-    fun genre(genreUri: Uri): Flow<MediaRequestStatus<Pair<Genre, List<Audio>>>>
+    fun genre(genreUri: Uri): Flow<MediaRequestStatus<Pair<Genre, GenreContent>>>
 
     /**
      * Get the playlist information and all the tracks of the given playlist.
diff --git a/app/src/main/java/org/lineageos/twelve/datasources/SubsonicDataSource.kt b/app/src/main/java/org/lineageos/twelve/datasources/SubsonicDataSource.kt
index 8d9869c..8246b85 100644
--- a/app/src/main/java/org/lineageos/twelve/datasources/SubsonicDataSource.kt
+++ b/app/src/main/java/org/lineageos/twelve/datasources/SubsonicDataSource.kt
@@ -23,6 +23,7 @@
 import org.lineageos.twelve.models.ArtistWorks
 import org.lineageos.twelve.models.Audio
 import org.lineageos.twelve.models.Genre
+import org.lineageos.twelve.models.GenreContent
 import org.lineageos.twelve.models.MediaType
 import org.lineageos.twelve.models.Playlist
 import org.lineageos.twelve.models.ProviderArgument
@@ -142,8 +143,44 @@
 
     override fun genre(genreUri: Uri) = suspend {
         val genreName = genreUri.lastPathSegment!!
-        subsonicClient.getSongsByGenre(genreName).toRequestStatus {
-            Genre(genreUri, genreName) to song.map { it.toMediaItem() }
+
+        val appearsInAlbums = subsonicClient.getAlbumList2(
+            "byGenre",
+            size = 500,
+            genre = genreName
+        ).toRequestStatus {
+            album.map { it.toMediaItem() }
+        }.let {
+            when (it) {
+                is RequestStatus.Success -> it.data
+                else -> null
+            }
+        }
+
+        val audios = subsonicClient.getSongsByGenre(genreName).toRequestStatus {
+            song.map { it.toMediaItem() }
+        }.let {
+            when (it) {
+                is RequestStatus.Success -> it.data
+                else -> null
+            }
+        }
+
+        val exists = listOf(
+            appearsInAlbums,
+            audios,
+        ).any { it != null }
+
+        if (exists) {
+            RequestStatus.Success<_, MediaError>(
+                Genre(genreUri, genreName) to GenreContent(
+                    appearsInAlbums.orEmpty(),
+                    listOf(),
+                    audios.orEmpty(),
+                )
+            )
+        } else {
+            RequestStatus.Error(MediaError.NOT_FOUND)
         }
     }.asFlow()
 
diff --git a/app/src/main/java/org/lineageos/twelve/models/GenreContent.kt b/app/src/main/java/org/lineageos/twelve/models/GenreContent.kt
new file mode 100644
index 0000000..fed4e96
--- /dev/null
+++ b/app/src/main/java/org/lineageos/twelve/models/GenreContent.kt
@@ -0,0 +1,19 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.twelve.models
+
+/**
+ * Content related to a certain genre.
+ *
+ * @param appearsInAlbums Albums with audios related to this genre
+ * @param appearsInPlaylists Playlists with audios related to this genre
+ * @param audios Audios related to this genre
+ */
+data class GenreContent(
+    val appearsInAlbums: List<Album>,
+    val appearsInPlaylists: List<Playlist>,
+    val audios: List<Audio>,
+)
diff --git a/app/src/main/java/org/lineageos/twelve/query/Query.kt b/app/src/main/java/org/lineageos/twelve/query/Query.kt
index 99ad2c7..2c09cc4 100644
--- a/app/src/main/java/org/lineageos/twelve/query/Query.kt
+++ b/app/src/main/java/org/lineageos/twelve/query/Query.kt
@@ -12,11 +12,17 @@
 
     companion object {
         const val ARG = "?"
+        const val NULL = "NULL"
     }
 }
 
 enum class Operator(val symbol: String) {
-    AND("AND"), OR("OR"), EQUALS("="), NOT_EQUALS("!="), LIKE("LIKE"),
+    AND("AND"),
+    OR("OR"),
+    EQUALS("="),
+    NOT_EQUALS("!="),
+    LIKE("LIKE"),
+    IS("IS"),
 }
 
 class LogicalOp(private val lhs: Query, private val op: Operator, private val rhs: Query) : Query {
@@ -37,6 +43,7 @@
 infix fun Column.eq(other: String) = StringOp(this, Operator.EQUALS, other)
 infix fun Column.neq(other: String) = StringOp(this, Operator.NOT_EQUALS, other)
 infix fun Column.like(other: String) = StringOp(this, Operator.LIKE, other)
+infix fun Column.`is`(other: String) = StringOp(this, Operator.IS, other)
 infix fun <T> Column.`in`(values: Collection<T>) = In(this, values)
 
 inline fun query(block: () -> Query) = block().build()
diff --git a/app/src/main/java/org/lineageos/twelve/repositories/MediaRepository.kt b/app/src/main/java/org/lineageos/twelve/repositories/MediaRepository.kt
index 2fa6585..7ed619c 100644
--- a/app/src/main/java/org/lineageos/twelve/repositories/MediaRepository.kt
+++ b/app/src/main/java/org/lineageos/twelve/repositories/MediaRepository.kt
@@ -354,6 +354,13 @@
     }
 
     /**
+     * @see MediaDataSource.genre
+     */
+    fun genre(genreUri: Uri) = withMediaItemsDataSourceFlow(genreUri) {
+        genre(genreUri)
+    }
+
+    /**
      * @see MediaDataSource.playlist
      */
     fun playlist(playlistUri: Uri) = withMediaItemsDataSourceFlow(playlistUri) {
diff --git a/app/src/main/java/org/lineageos/twelve/services/MediaRepositoryTree.kt b/app/src/main/java/org/lineageos/twelve/services/MediaRepositoryTree.kt
index 8ad3265..92bd25f 100644
--- a/app/src/main/java/org/lineageos/twelve/services/MediaRepositoryTree.kt
+++ b/app/src/main/java/org/lineageos/twelve/services/MediaRepositoryTree.kt
@@ -106,7 +106,15 @@
 
             is Audio -> listOf()
 
-            is Genre -> listOf()
+            is Genre -> repository.genre(it.uri).toOneShotResult().second.let { genreContent ->
+                listOf(
+                    genreContent.appearsInAlbums,
+                    genreContent.appearsInPlaylists,
+                    genreContent.audios,
+                ).flatten().map { allRelatedMediaItems ->
+                    allRelatedMediaItems.toMedia3MediaItem()
+                }
+            }
 
             is Playlist -> repository.playlist(
                 it.uri
@@ -152,13 +160,8 @@
         }
 
         mediaId.startsWith(Genre.GENRE_MEDIA_ITEM_ID_PREFIX) -> {
-            // TODO
-            /*
-            repository.genre(Uri.parse(mediaId.removePrefix(GENRE_MEDIA_ITEM_ID_PREFIX)))
+            repository.genre(Uri.parse(mediaId.removePrefix(Genre.GENRE_MEDIA_ITEM_ID_PREFIX)))
                 .toOneShotResult().first
-
-             */
-            null
         }
 
         mediaId.startsWith(Playlist.PLAYLIST_MEDIA_ITEM_ID_PREFIX) -> {