From edbcbeb3803618cfaf1942138f71a4482a8fe01a Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Mon, 28 Sep 2026 05:15:55 +0000 Subject: [PATCH] group download status per entity for the library tabs The detail heroes ask for one entity's status at a time. The list surfaces need every status on screen at once, so add one grouped flow per tab that returns a lookup of entity id to counts, which the tiles and rows read the way song rows read the per-song map. --- .../com/subtracks/data/db/DownloadDao.kt | 39 +++++++++++++ .../com/subtracks/data/model/Download.kt | 9 +++ .../subtracks/data/repo/DownloadRepository.kt | 10 ++++ .../data/repo/DownloadRepositoryTest.kt | 58 +++++++++++++++++++ 4 files changed, 116 insertions(+) diff --git a/app/src/main/kotlin/com/subtracks/data/db/DownloadDao.kt b/app/src/main/kotlin/com/subtracks/data/db/DownloadDao.kt index 81eac212..5305866b 100644 --- a/app/src/main/kotlin/com/subtracks/data/db/DownloadDao.kt +++ b/app/src/main/kotlin/com/subtracks/data/db/DownloadDao.kt @@ -4,6 +4,7 @@ import androidx.room3.Dao import androidx.room3.Query import androidx.room3.Upsert import com.subtracks.data.model.DownloadArtwork +import com.subtracks.data.model.EntityDownloadStatus import com.subtracks.data.model.ListDownloadStatus import com.subtracks.data.model.SongDownload import kotlinx.coroutines.flow.Flow @@ -57,6 +58,44 @@ interface DownloadDao { albumId: String, ): Flow + @Query( + "SELECT songs.albumId AS id, " + + "COUNT(songs.id) AS total, " + + "COALESCE(SUM(CASE WHEN song_downloads.status = 'Completed' THEN 1 ELSE 0 END), 0) AS downloaded, " + + "COALESCE(SUM(CASE WHEN song_downloads.status IN ('Queued', 'Running') THEN 1 ELSE 0 END), 0) AS downloading " + + "FROM songs " + + "LEFT JOIN song_downloads ON song_downloads.sourceId = songs.sourceId AND song_downloads.songId = songs.id " + + "WHERE songs.sourceId = :sourceId AND songs.albumId IS NOT NULL " + + "GROUP BY songs.albumId", + ) + fun albumStatuses(sourceId: Long): Flow> + + @Query( + "SELECT albums.artistId AS id, " + + "COUNT(songs.id) AS total, " + + "COALESCE(SUM(CASE WHEN song_downloads.status = 'Completed' THEN 1 ELSE 0 END), 0) AS downloaded, " + + "COALESCE(SUM(CASE WHEN song_downloads.status IN ('Queued', 'Running') THEN 1 ELSE 0 END), 0) AS downloading " + + "FROM songs " + + "JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " + + "LEFT JOIN song_downloads ON song_downloads.sourceId = songs.sourceId AND song_downloads.songId = songs.id " + + "WHERE songs.sourceId = :sourceId AND albums.artistId IS NOT NULL " + + "GROUP BY albums.artistId", + ) + fun artistStatuses(sourceId: Long): Flow> + + @Query( + "SELECT playlist_songs.playlistId AS id, " + + "COUNT(DISTINCT songs.id) AS total, " + + "COUNT(DISTINCT CASE WHEN song_downloads.status = 'Completed' THEN songs.id END) AS downloaded, " + + "COUNT(DISTINCT CASE WHEN song_downloads.status IN ('Queued', 'Running') THEN songs.id END) AS downloading " + + "FROM playlist_songs " + + "JOIN songs ON songs.sourceId = playlist_songs.sourceId AND songs.id = playlist_songs.songId " + + "LEFT JOIN song_downloads ON song_downloads.sourceId = songs.sourceId AND song_downloads.songId = songs.id " + + "WHERE playlist_songs.sourceId = :sourceId " + + "GROUP BY playlist_songs.playlistId", + ) + fun playlistStatuses(sourceId: Long): Flow> + @Query( "SELECT d.sourceId AS sourceId, s.albumId AS albumId, s.artistId AS artistId, " + "al.id AS albumRow, al.coverArt AS albumCoverArt, " + diff --git a/app/src/main/kotlin/com/subtracks/data/model/Download.kt b/app/src/main/kotlin/com/subtracks/data/model/Download.kt index e8ee4f44..83a5c8cb 100644 --- a/app/src/main/kotlin/com/subtracks/data/model/Download.kt +++ b/app/src/main/kotlin/com/subtracks/data/model/Download.kt @@ -52,6 +52,15 @@ data class ListDownloadStatus( val complete: Boolean get() = total > 0 && downloaded == total } +data class EntityDownloadStatus( + val id: String, + val total: Long = 0, + val downloaded: Long = 0, + val downloading: Long = 0, +) { + fun toListStatus(): ListDownloadStatus = ListDownloadStatus(total, downloaded, downloading) +} + data class DownloadArtwork( val sourceId: Long, val albumId: String?, diff --git a/app/src/main/kotlin/com/subtracks/data/repo/DownloadRepository.kt b/app/src/main/kotlin/com/subtracks/data/repo/DownloadRepository.kt index 9b743a9f..e957b0f6 100644 --- a/app/src/main/kotlin/com/subtracks/data/repo/DownloadRepository.kt +++ b/app/src/main/kotlin/com/subtracks/data/repo/DownloadRepository.kt @@ -185,6 +185,16 @@ class DownloadRepository( DownloadList.Artist -> db.downloadDao().artistStatus(sourceId, refId) } + fun statuses( + sourceId: Long, + list: DownloadList, + ): Flow> = + when (list) { + DownloadList.Album -> db.downloadDao().albumStatuses(sourceId) + DownloadList.Playlist -> db.downloadDao().playlistStatuses(sourceId) + DownloadList.Artist -> db.downloadDao().artistStatuses(sourceId) + }.map { rows -> rows.associate { it.id to it.toListStatus() } } + suspend fun downloadAll( sourceId: Long, list: DownloadList, diff --git a/app/src/test/kotlin/com/subtracks/data/repo/DownloadRepositoryTest.kt b/app/src/test/kotlin/com/subtracks/data/repo/DownloadRepositoryTest.kt index 57909ce0..33b08eb9 100644 --- a/app/src/test/kotlin/com/subtracks/data/repo/DownloadRepositoryTest.kt +++ b/app/src/test/kotlin/com/subtracks/data/repo/DownloadRepositoryTest.kt @@ -731,6 +731,64 @@ class DownloadRepositoryTest { assertEquals(listOf("s1", "s2"), requestedSongIds()) } + @Test + fun groupedStatusesCountEachAlbumArtistAndPlaylistSeparately() = + runTest { + seedLibrary() + seedPlaylist(listOf("s1", "s2")) + runBlocking { + db.libraryDao().upsertSongs( + listOf( + Song( + sourceId = 1, + id = "s3", + albumId = "al2", + artistId = "ar2", + title = "Song 3", + album = "Other", + artist = "Other", + duration = 100, + track = 1, + disc = 1, + starred = null, + genre = null, + ), + ), + ) + db.libraryDao().upsertAlbums( + listOf( + Album( + sourceId = 1, + id = "al2", + artistId = "ar2", + name = "Other", + albumArtist = "Other", + created = 0, + coverArt = null, + genre = null, + year = 2000, + starred = null, + songCount = 1, + ), + ), + ) + db.libraryDao().upsertArtists(listOf(Artist(sourceId = 1, id = "ar2", name = "Other", albumCount = 1, starred = null, coverArt = null))) + db.downloadDao().upsert(SongDownload(1, "s1", DownloadStatus.Completed)) + db.downloadDao().upsert(SongDownload(1, "s3", DownloadStatus.Queued)) + } + + val albums = runBlocking { repository.statuses(1, DownloadList.Album).first() } + assertEquals(ListDownloadStatus(total = 2, downloaded = 1), albums["al1"]) + assertEquals(ListDownloadStatus(total = 1, downloading = 1), albums["al2"]) + + val artists = runBlocking { repository.statuses(1, DownloadList.Artist).first() } + assertEquals(ListDownloadStatus(total = 2, downloaded = 1), artists["ar1"]) + assertEquals(ListDownloadStatus(total = 1, downloading = 1), artists["ar2"]) + + val playlists = runBlocking { repository.statuses(1, DownloadList.Playlist).first() } + assertEquals(ListDownloadStatus(total = 2, downloaded = 1), playlists["pl1"]) + } + private fun requestedSongIds(): List = engine.requests.map { it.second.path.substringAfterLast('/') } private fun seedSongs(tracks: IntRange) {