diff --git a/app/src/main/kotlin/com/subtracks/data/db/LibraryDao.kt b/app/src/main/kotlin/com/subtracks/data/db/LibraryDao.kt index 658dd693..ccfff12c 100644 --- a/app/src/main/kotlin/com/subtracks/data/db/LibraryDao.kt +++ b/app/src/main/kotlin/com/subtracks/data/db/LibraryDao.kt @@ -29,6 +29,9 @@ internal const val PLAYLIST_SONGS_SQL = PLAYLIST_SONGS_SELECT + PLAYLIST_SONGS_O internal const val ALBUMS_FILTER = "FROM albums WHERE sourceId = :sourceId " + "AND (:starredFilter = 0 OR (:starredFilter = 1 AND starred IS NOT NULL) OR (:starredFilter = 2 AND starred IS NULL)) " + + "AND (:downloadedFilter = 0 OR EXISTS (SELECT 1 FROM songs " + + "JOIN song_downloads sd ON sd.sourceId = songs.sourceId AND sd.songId = songs.id " + + "WHERE songs.sourceId = albums.sourceId AND songs.albumId = albums.id AND sd.status = 'Completed')) " + "AND (:search = '' OR instr(lower(name), lower(:search)) > 0 OR instr(lower(albumArtist), lower(:search)) > 0) " internal const val ALBUM_ORDER_BY_NAME = "name COLLATE NOCASE, id" @@ -48,6 +51,10 @@ internal const val ALBUM_ORDER_BY_STARRED_REVERSED = "starred ASC NULLS LAST, na internal const val ARTISTS_FILTER = "FROM artists WHERE sourceId = :sourceId " + "AND (:starredFilter = 0 OR (:starredFilter = 1 AND starred IS NOT NULL) OR (:starredFilter = 2 AND starred IS NULL)) " + + "AND (:downloadedFilter = 0 OR EXISTS (SELECT 1 FROM songs " + + "JOIN albums dl ON dl.sourceId = songs.sourceId AND dl.id = songs.albumId " + + "JOIN song_downloads sd ON sd.sourceId = songs.sourceId AND sd.songId = songs.id " + + "WHERE songs.sourceId = artists.sourceId AND dl.artistId = artists.id AND sd.status = 'Completed')) " + "AND (:search = '' OR instr(lower(name), lower(:search)) > 0) " internal const val ARTIST_ORDER_BY_NAME = "name COLLATE NOCASE, id" @@ -59,6 +66,9 @@ internal const val ARTIST_ORDER_BY_STARRED_REVERSED = "starred ASC NULLS LAST, n internal const val PLAYLISTS_FILTER = "FROM playlists WHERE sourceId = :sourceId " + + "AND (:downloadedFilter = 0 OR EXISTS (SELECT 1 FROM playlist_songs ps " + + "JOIN song_downloads sd ON sd.sourceId = ps.sourceId AND sd.songId = ps.songId " + + "WHERE ps.sourceId = playlists.sourceId AND ps.playlistId = playlists.id AND sd.status = 'Completed')) " + "AND (:search = '' OR instr(lower(name), lower(:search)) > 0) " internal const val PLAYLIST_ORDER_BY_NAME = "name COLLATE NOCASE, id" @@ -205,6 +215,7 @@ interface LibraryDao { sourceId: Long, starredFilter: Int, search: String, + downloadedFilter: Int = 0, ): PagingSource @Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_NAME_REVERSED") @@ -212,6 +223,7 @@ interface LibraryDao { sourceId: Long, starredFilter: Int, search: String, + downloadedFilter: Int = 0, ): PagingSource @Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_ARTIST") @@ -219,6 +231,7 @@ interface LibraryDao { sourceId: Long, starredFilter: Int, search: String, + downloadedFilter: Int = 0, ): PagingSource @Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_ARTIST_REVERSED") @@ -226,6 +239,7 @@ interface LibraryDao { sourceId: Long, starredFilter: Int, search: String, + downloadedFilter: Int = 0, ): PagingSource @Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_YEAR") @@ -233,6 +247,7 @@ interface LibraryDao { sourceId: Long, starredFilter: Int, search: String, + downloadedFilter: Int = 0, ): PagingSource @Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_YEAR_REVERSED") @@ -240,6 +255,7 @@ interface LibraryDao { sourceId: Long, starredFilter: Int, search: String, + downloadedFilter: Int = 0, ): PagingSource @Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_ADDED") @@ -247,6 +263,7 @@ interface LibraryDao { sourceId: Long, starredFilter: Int, search: String, + downloadedFilter: Int = 0, ): PagingSource @Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_ADDED_REVERSED") @@ -254,6 +271,7 @@ interface LibraryDao { sourceId: Long, starredFilter: Int, search: String, + downloadedFilter: Int = 0, ): PagingSource @Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_STARRED") @@ -261,6 +279,7 @@ interface LibraryDao { sourceId: Long, starredFilter: Int, search: String, + downloadedFilter: Int = 0, ): PagingSource @Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_STARRED_REVERSED") @@ -268,6 +287,7 @@ interface LibraryDao { sourceId: Long, starredFilter: Int, search: String, + downloadedFilter: Int = 0, ): PagingSource @Query("SELECT * $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_NAME") @@ -275,6 +295,7 @@ interface LibraryDao { sourceId: Long, starredFilter: Int, search: String, + downloadedFilter: Int = 0, ): PagingSource @Query("SELECT * $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_NAME_REVERSED") @@ -282,6 +303,7 @@ interface LibraryDao { sourceId: Long, starredFilter: Int, search: String, + downloadedFilter: Int = 0, ): PagingSource @Query("SELECT * $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_ALBUM_COUNT") @@ -289,6 +311,7 @@ interface LibraryDao { sourceId: Long, starredFilter: Int, search: String, + downloadedFilter: Int = 0, ): PagingSource @Query("SELECT * $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_ALBUM_COUNT_REVERSED") @@ -296,6 +319,7 @@ interface LibraryDao { sourceId: Long, starredFilter: Int, search: String, + downloadedFilter: Int = 0, ): PagingSource @Query("SELECT * $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_STARRED") @@ -303,6 +327,7 @@ interface LibraryDao { sourceId: Long, starredFilter: Int, search: String, + downloadedFilter: Int = 0, ): PagingSource @Query("SELECT * $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_STARRED_REVERSED") @@ -310,42 +335,49 @@ interface LibraryDao { sourceId: Long, starredFilter: Int, search: String, + downloadedFilter: Int = 0, ): PagingSource @Query("SELECT * $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_NAME") fun playlistsByName( sourceId: Long, search: String, + downloadedFilter: Int = 0, ): PagingSource @Query("SELECT * $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_NAME_REVERSED") fun playlistsByNameReversed( sourceId: Long, search: String, + downloadedFilter: Int = 0, ): PagingSource @Query("SELECT * $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_ADDED") fun playlistsByAdded( sourceId: Long, search: String, + downloadedFilter: Int = 0, ): PagingSource @Query("SELECT * $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_ADDED_REVERSED") fun playlistsByAddedReversed( sourceId: Long, search: String, + downloadedFilter: Int = 0, ): PagingSource @Query("SELECT * $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_UPDATED") fun playlistsByUpdated( sourceId: Long, search: String, + downloadedFilter: Int = 0, ): PagingSource @Query("SELECT * $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_UPDATED_REVERSED") fun playlistsByUpdatedReversed( sourceId: Long, search: String, + downloadedFilter: Int = 0, ): PagingSource @Query(PLAYLIST_SONGS_SQL) diff --git a/app/src/main/kotlin/com/subtracks/data/prefs/UserPreferences.kt b/app/src/main/kotlin/com/subtracks/data/prefs/UserPreferences.kt index f4366f5e..cf260e19 100644 --- a/app/src/main/kotlin/com/subtracks/data/prefs/UserPreferences.kt +++ b/app/src/main/kotlin/com/subtracks/data/prefs/UserPreferences.kt @@ -35,6 +35,7 @@ data class ListQuery( val sort: String, val descending: Boolean = false, val starred: StarredFilter = StarredFilter.Any, + val downloaded: Boolean = false, ) data class StreamQuality( @@ -107,7 +108,8 @@ class UserPreferences( private fun listQueryKey(tab: LibraryListTab) = stringPreferencesKey("list_query_${tab.key}") - private fun encode(query: ListQuery) = "${query.sort}|${if (query.descending) 1 else 0}|${query.starred.ordinal}" + private fun encode(query: ListQuery) = + "${query.sort}|${if (query.descending) 1 else 0}|${query.starred.ordinal}|${if (query.downloaded) 1 else 0}" private fun encode(quality: StreamQuality) = "${quality.maxBitrate}|${quality.format.orEmpty()}" @@ -130,11 +132,12 @@ class UserPreferences( defaultSort: String, ): ListQuery { val parts = stored?.split("|") - if (parts == null || parts.size != 3) return ListQuery(defaultSort) + if (parts == null || parts.size < 3) return ListQuery(defaultSort) return ListQuery( sort = parts[0].ifEmpty { defaultSort }, descending = parts[1] == "1", starred = StarredFilter.entries.getOrElse(parts[2].toIntOrNull() ?: 0) { StarredFilter.Any }, + downloaded = parts.getOrNull(3) == "1", ) } } diff --git a/app/src/main/kotlin/com/subtracks/data/repo/LibraryRepository.kt b/app/src/main/kotlin/com/subtracks/data/repo/LibraryRepository.kt index 8b85a653..e6623bb4 100644 --- a/app/src/main/kotlin/com/subtracks/data/repo/LibraryRepository.kt +++ b/app/src/main/kotlin/com/subtracks/data/repo/LibraryRepository.kt @@ -41,47 +41,49 @@ class LibraryRepository( descending: Boolean, starred: StarredFilter, search: String, + downloaded: Boolean, ): Flow> = pager(40) { val dao = db.libraryDao() + val downloadedFilter = if (downloaded) 1 else 0 when (sort) { AlbumSort.Name -> { if (descending) { - dao.albumsByNameReversed(sourceId, starred.ordinal, search) + dao.albumsByNameReversed(sourceId, starred.ordinal, search, downloadedFilter) } else { - dao.albumsByName(sourceId, starred.ordinal, search) + dao.albumsByName(sourceId, starred.ordinal, search, downloadedFilter) } } AlbumSort.Artist -> { if (descending) { - dao.albumsByArtistReversed(sourceId, starred.ordinal, search) + dao.albumsByArtistReversed(sourceId, starred.ordinal, search, downloadedFilter) } else { - dao.albumsByArtist(sourceId, starred.ordinal, search) + dao.albumsByArtist(sourceId, starred.ordinal, search, downloadedFilter) } } AlbumSort.Year -> { if (descending) { - dao.albumsByYearReversed(sourceId, starred.ordinal, search) + dao.albumsByYearReversed(sourceId, starred.ordinal, search, downloadedFilter) } else { - dao.albumsByYear(sourceId, starred.ordinal, search) + dao.albumsByYear(sourceId, starred.ordinal, search, downloadedFilter) } } AlbumSort.Added -> { if (descending) { - dao.albumsByRecentlyAddedReversed(sourceId, starred.ordinal, search) + dao.albumsByRecentlyAddedReversed(sourceId, starred.ordinal, search, downloadedFilter) } else { - dao.albumsByRecentlyAdded(sourceId, starred.ordinal, search) + dao.albumsByRecentlyAdded(sourceId, starred.ordinal, search, downloadedFilter) } } AlbumSort.Starred -> { if (descending) { - dao.albumsByStarredReversed(sourceId, starred.ordinal, search) + dao.albumsByStarredReversed(sourceId, starred.ordinal, search, downloadedFilter) } else { - dao.albumsByStarred(sourceId, starred.ordinal, search) + dao.albumsByStarred(sourceId, starred.ordinal, search, downloadedFilter) } } } @@ -93,31 +95,33 @@ class LibraryRepository( descending: Boolean, starred: StarredFilter, search: String, + downloaded: Boolean, ): Flow> = pager(60) { val dao = db.libraryDao() + val downloadedFilter = if (downloaded) 1 else 0 when (sort) { ArtistSort.Name -> { if (descending) { - dao.artistsByNameReversed(sourceId, starred.ordinal, search) + dao.artistsByNameReversed(sourceId, starred.ordinal, search, downloadedFilter) } else { - dao.artistsByName(sourceId, starred.ordinal, search) + dao.artistsByName(sourceId, starred.ordinal, search, downloadedFilter) } } ArtistSort.AlbumCount -> { if (descending) { - dao.artistsByAlbumCountReversed(sourceId, starred.ordinal, search) + dao.artistsByAlbumCountReversed(sourceId, starred.ordinal, search, downloadedFilter) } else { - dao.artistsByAlbumCount(sourceId, starred.ordinal, search) + dao.artistsByAlbumCount(sourceId, starred.ordinal, search, downloadedFilter) } } ArtistSort.Starred -> { if (descending) { - dao.artistsByStarredReversed(sourceId, starred.ordinal, search) + dao.artistsByStarredReversed(sourceId, starred.ordinal, search, downloadedFilter) } else { - dao.artistsByStarred(sourceId, starred.ordinal, search) + dao.artistsByStarred(sourceId, starred.ordinal, search, downloadedFilter) } } } @@ -128,31 +132,33 @@ class LibraryRepository( sort: PlaylistSort, descending: Boolean, search: String, + downloaded: Boolean, ): Flow> = pager(40) { val dao = db.libraryDao() + val downloadedFilter = if (downloaded) 1 else 0 when (sort) { PlaylistSort.Name -> { if (descending) { - dao.playlistsByNameReversed(sourceId, search) + dao.playlistsByNameReversed(sourceId, search, downloadedFilter) } else { - dao.playlistsByName(sourceId, search) + dao.playlistsByName(sourceId, search, downloadedFilter) } } PlaylistSort.Added -> { if (descending) { - dao.playlistsByAddedReversed(sourceId, search) + dao.playlistsByAddedReversed(sourceId, search, downloadedFilter) } else { - dao.playlistsByAdded(sourceId, search) + dao.playlistsByAdded(sourceId, search, downloadedFilter) } } PlaylistSort.Updated -> { if (descending) { - dao.playlistsByUpdatedReversed(sourceId, search) + dao.playlistsByUpdatedReversed(sourceId, search, downloadedFilter) } else { - dao.playlistsByUpdated(sourceId, search) + dao.playlistsByUpdated(sourceId, search, downloadedFilter) } } } diff --git a/app/src/main/kotlin/com/subtracks/ui/library/LibraryScreen.kt b/app/src/main/kotlin/com/subtracks/ui/library/LibraryScreen.kt index 17a9d9ed..df2fa3df 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/LibraryScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/LibraryScreen.kt @@ -32,6 +32,7 @@ import androidx.compose.material.icons.rounded.Album import androidx.compose.material.icons.rounded.ArrowDownward import androidx.compose.material.icons.rounded.ArrowUpward import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.DownloadDone import androidx.compose.material.icons.rounded.FilterAltOff import androidx.compose.material.icons.rounded.Person import androidx.compose.material.icons.rounded.Remove @@ -212,7 +213,7 @@ fun LibraryRoute( LibraryTab.entries.associateWith { tab -> val query by viewModel.listQuery(tab.listTab()).collectAsStateWithLifecycle() val term by viewModel.search(tab.listTab()).collectAsStateWithLifecycle() - "${query.sort}|${query.descending}|${query.starred}|$term" + "${query.sort}|${query.descending}|${query.starred}|${query.downloaded}|$term" } val playback by playbackController.state.collectAsStateWithLifecycle() val artwork = rememberArtworkColors(playbackController.coverArt(playback.item, thumbnail = true)) @@ -271,11 +272,12 @@ fun LibraryRoute( viewModel.setListQuery(listTab, listQuery.copy(starred = next)) }, onClearFilters = { - viewModel.setListQuery(listTab, listQuery.copy(starred = StarredFilter.Any)) + viewModel.setListQuery(listTab, listQuery.copy(starred = StarredFilter.Any, downloaded = false)) viewModel.setSearch(listTab, "") }, search = search, onSearchChange = { viewModel.setSearch(listTab, it) }, + onToggleDownloaded = { viewModel.setListQuery(listTab, listQuery.copy(downloaded = !listQuery.downloaded)) }, albumDownloads = downloads.takeIf { listTab == LibraryListTab.Albums }.orEmpty(), artistDownloads = downloads.takeIf { listTab == LibraryListTab.Artists }.orEmpty(), playlistDownloads = downloads.takeIf { listTab == LibraryListTab.Playlists }.orEmpty(), @@ -315,6 +317,7 @@ fun LibraryScreen( onSortChange: (String) -> Unit = {}, onToggleSortDirection: () -> Unit = {}, onCycleStarred: () -> Unit = {}, + onToggleDownloaded: () -> Unit = {}, onClearFilters: () -> Unit = {}, search: String = "", onSearchChange: (String) -> Unit = {}, @@ -355,7 +358,7 @@ fun LibraryScreen( val density = LocalDensity.current val statusBarTop = with(density) { WindowInsets.statusBars.getTop(this).toDp() } - val filtersActive = listQuery.starred != StarredFilter.Any || search.isNotEmpty() + val filtersActive = listQuery.starred != StarredFilter.Any || listQuery.downloaded || search.isNotEmpty() var tabBarHeightPx by remember { mutableFloatStateOf(with(density) { (TAB_BAR_CONTENT_HEIGHT + bottomInset).toPx() }) } val tabBarHeight = with(density) { tabBarHeightPx.toDp() } val listTopInset = statusBarTop @@ -540,6 +543,7 @@ fun LibraryScreen( onSortChange = onSortChange, onToggleSortDirection = onToggleSortDirection, onCycleStarred = onCycleStarred, + onToggleDownloaded = onToggleDownloaded, onClearFilters = onClearFilters, onSearch = { showOptions = false @@ -560,6 +564,7 @@ private fun ListOptionsSheet( onSortChange: (String) -> Unit, onToggleSortDirection: () -> Unit, onCycleStarred: () -> Unit, + onToggleDownloaded: () -> Unit, onClearFilters: () -> Unit, onSearch: () -> Unit, ) { @@ -615,27 +620,44 @@ private fun ListOptionsSheet( } } - if (starredSupported) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 8.dp, top = 16.dp), + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 8.dp, top = 16.dp), + ) { + Text( + text = "Filters", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.weight(1f), + ) + IconButton( + onClick = onClearFilters, + enabled = canClearFilters, ) { - Text( - text = "Filters", - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.weight(1f), - ) - IconButton( - onClick = onClearFilters, - enabled = canClearFilters, - ) { - Icon(Icons.Rounded.FilterAltOff, contentDescription = "Clear filters") - } + Icon(Icons.Rounded.FilterAltOff, contentDescription = "Clear filters") } - FlowRow( - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), - ) { + } + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + ) { + FilterChip( + selected = listQuery.downloaded, + onClick = onToggleDownloaded, + label = { Text("Downloaded") }, + leadingIcon = + if (listQuery.downloaded) { + { + Icon( + Icons.Rounded.DownloadDone, + contentDescription = null, + modifier = Modifier.size(FilterChipDefaults.IconSize), + ) + } + } else { + null + }, + ) + if (starredSupported) { FilterChip( selected = listQuery.starred != StarredFilter.Any, onClick = onCycleStarred, diff --git a/app/src/main/kotlin/com/subtracks/ui/library/LibraryViewModel.kt b/app/src/main/kotlin/com/subtracks/ui/library/LibraryViewModel.kt index 1c2654cb..71afa02a 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/LibraryViewModel.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/LibraryViewModel.kt @@ -62,7 +62,7 @@ class LibraryViewModel( combine(listQueries.getValue(LibraryListTab.Albums), searches.getValue(LibraryListTab.Albums)) { query, search -> query to search }.flatMapLatest { (query, search) -> - libraryRepository.albums(sourceId, query.albumSort(), query.descending, query.starred, search) + libraryRepository.albums(sourceId, query.albumSort(), query.descending, query.starred, search, query.downloaded) } }.cachedIn(viewModelScope) @@ -73,7 +73,7 @@ class LibraryViewModel( combine(listQueries.getValue(LibraryListTab.Artists), searches.getValue(LibraryListTab.Artists)) { query, search -> query to search }.flatMapLatest { (query, search) -> - libraryRepository.artists(sourceId, query.artistSort(), query.descending, query.starred, search) + libraryRepository.artists(sourceId, query.artistSort(), query.descending, query.starred, search, query.downloaded) } }.cachedIn(viewModelScope) @@ -84,7 +84,7 @@ class LibraryViewModel( combine(listQueries.getValue(LibraryListTab.Playlists), searches.getValue(LibraryListTab.Playlists)) { query, search -> query to search }.flatMapLatest { (query, search) -> - libraryRepository.playlists(sourceId, query.playlistSort(), query.descending, search) + libraryRepository.playlists(sourceId, query.playlistSort(), query.descending, search, query.downloaded) } }.cachedIn(viewModelScope) diff --git a/app/src/test/kotlin/com/subtracks/data/db/LibraryDaoTest.kt b/app/src/test/kotlin/com/subtracks/data/db/LibraryDaoTest.kt index b0f347e2..7782910e 100644 --- a/app/src/test/kotlin/com/subtracks/data/db/LibraryDaoTest.kt +++ b/app/src/test/kotlin/com/subtracks/data/db/LibraryDaoTest.kt @@ -9,8 +9,11 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.subtracks.data.model.Album import com.subtracks.data.model.Artist +import com.subtracks.data.model.DownloadStatus import com.subtracks.data.model.Playlist +import com.subtracks.data.model.PlaylistSong import com.subtracks.data.model.Song +import com.subtracks.data.model.SongDownload import com.subtracks.data.model.Source import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest @@ -290,6 +293,53 @@ class LibraryDaoTest { assertTrue(plan, plan.contains("TEMP B-TREE")) } + @Test + fun theDownloadedFilterKeepsOnlyEntitiesWithACompletedDownload() = + runTest { + val sourceId = source() + val dao = db.libraryDao() + dao.upsertAlbums( + listOf( + album(sourceId, "al-1", "Downloaded", year = 2000, starred = null), + album(sourceId, "al-2", "Empty", year = 2001, starred = null), + ), + ) + dao.upsertArtists(listOf(artist(sourceId, "ar-1", "Artist", albumCount = 2))) + dao.upsertSongs( + listOf( + song(sourceId, "s1", "One", starred = null), + song(sourceId, "s2", "Two", starred = null).copy(albumId = "al-2"), + ), + ) + dao.upsertPlaylists( + listOf( + playlist(sourceId, "pl-1", "Has download", created = 0, changed = 0), + playlist(sourceId, "pl-2", "No download", created = 1, changed = 1), + ), + ) + dao.upsertPlaylistSongs( + listOf( + PlaylistSong(sourceId, "pl-1", "s1", 0), + PlaylistSong(sourceId, "pl-2", "s2", 0), + ), + ) + db.downloadDao().upsert(SongDownload(sourceId, "s1", DownloadStatus.Completed)) + + assertEquals( + listOf("Downloaded"), + dao.albumsByName(sourceId, 0, "", downloadedFilter = 1).page().map { it.name }, + ) + assertEquals( + listOf("ar-1"), + dao.artistsByName(sourceId, 0, "", downloadedFilter = 1).page().map { it.id }, + ) + assertEquals( + listOf("pl-1"), + dao.playlistsByName(sourceId, "", downloadedFilter = 1).page().map { it.id }, + ) + assertEquals(2, dao.albumsByName(sourceId, 0, "").page().size) + } + private suspend fun plan(sql: String): String = db.useReaderConnection { connection -> connection.usePrepared("EXPLAIN QUERY PLAN $sql") { statement ->