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) -> {