library: stop download writes from invalidating the browse lists

The album/artist/playlist paging SQL named song_downloads, so Room
invalidated the pages on every download progress tick, janking the list
and resetting scroll when a download started. Route the default (filter
off) path through a raw query that only observes its own table; the
downloaded/offline path keeps the download-aware queries.
This commit is contained in:
austinried
2026-10-04 00:30:07 +00:00
parent adc206337b
commit 487296e75e
4 changed files with 374 additions and 90 deletions
@@ -4,6 +4,8 @@ import androidx.paging.PagingSource
import androidx.room3.Dao
import androidx.room3.DaoReturnTypeConverters
import androidx.room3.Query
import androidx.room3.RawQuery
import androidx.room3.RoomRawQuery
import androidx.room3.Transaction
import androidx.room3.Upsert
import androidx.room3.paging.PagingSourceDaoReturnTypeConverter
@@ -432,6 +434,15 @@ interface LibraryDao {
)
suspend fun deleteOrphanPlaylistSongs(sourceId: Long)
@RawQuery(observedEntities = [Album::class])
fun albumsPaging(query: RoomRawQuery): PagingSource<Int, Album>
@RawQuery(observedEntities = [Artist::class])
fun artistsPaging(query: RoomRawQuery): PagingSource<Int, Artist>
@RawQuery(observedEntities = [Playlist::class])
fun playlistsPaging(query: RoomRawQuery): PagingSource<Int, Playlist>
@Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_NAME")
fun albumsByName(
sourceId: Long,
@@ -0,0 +1,142 @@
package com.subtracks.data.db
import androidx.room3.RoomRawQuery
import com.subtracks.data.prefs.AlbumSort
import com.subtracks.data.prefs.ArtistSort
import com.subtracks.data.prefs.PlaylistSort
// These filters mirror the download-aware ones in LibraryDao but never mention song_downloads, so
// the resulting PagingSource is not invalidated by download writes (including progress updates).
internal const val ALBUMS_FILTER_BASE =
"FROM albums WHERE sourceId = ? " +
"AND (? = 0 OR (? = 1 AND starred IS NOT NULL) OR (? = 2 AND starred IS NULL)) " +
"AND (? = '' " +
"OR (length(?) >= 3 AND rowid IN (SELECT rowid FROM album_search " +
"WHERE album_search MATCH '\"' || replace(?, '\"', '\"\"') || '\"')) " +
"OR (length(?) < 3 AND (instr(lower(name), lower(?)) > 0 " +
"OR instr(lower(albumArtist), lower(?)) > 0))) "
internal const val ARTISTS_FILTER_BASE =
"FROM artists WHERE sourceId = ? " +
"AND (? = 0 OR (? = 1 AND starred IS NOT NULL) OR (? = 2 AND starred IS NULL)) " +
"AND (? = '' " +
"OR (length(?) >= 3 AND rowid IN (SELECT rowid FROM artist_search " +
"WHERE artist_search MATCH '\"' || replace(?, '\"', '\"\"') || '\"')) " +
"OR (length(?) < 3 AND instr(lower(name), lower(?)) > 0)) "
internal const val PLAYLISTS_FILTER_BASE =
"FROM playlists WHERE sourceId = ? " +
"AND (? = '' " +
"OR (length(?) >= 3 AND rowid IN (SELECT rowid FROM playlist_search " +
"WHERE playlist_search MATCH '\"' || replace(?, '\"', '\"\"') || '\"')) " +
"OR (length(?) < 3 AND instr(lower(name), lower(?)) > 0)) "
internal fun albumsDefaultQuery(
sourceId: Long,
starredFilter: Int,
search: String,
sort: AlbumSort,
descending: Boolean,
): RoomRawQuery {
val order = albumOrder(sort, descending)
return RoomRawQuery("SELECT * $ALBUMS_FILTER_BASE ORDER BY $order") { stmt ->
stmt.bindLong(1, sourceId)
stmt.bindInt(2, starredFilter)
stmt.bindInt(3, starredFilter)
stmt.bindInt(4, starredFilter)
stmt.bindText(5, search)
stmt.bindText(6, search)
stmt.bindText(7, search)
stmt.bindText(8, search)
stmt.bindText(9, search)
stmt.bindText(10, search)
}
}
internal fun artistsDefaultQuery(
sourceId: Long,
starredFilter: Int,
search: String,
sort: ArtistSort,
descending: Boolean,
): RoomRawQuery {
val order = artistOrder(sort, descending)
return RoomRawQuery("SELECT * $ARTISTS_FILTER_BASE ORDER BY $order") { stmt ->
stmt.bindLong(1, sourceId)
stmt.bindInt(2, starredFilter)
stmt.bindInt(3, starredFilter)
stmt.bindInt(4, starredFilter)
stmt.bindText(5, search)
stmt.bindText(6, search)
stmt.bindText(7, search)
stmt.bindText(8, search)
stmt.bindText(9, search)
}
}
internal fun playlistsDefaultQuery(
sourceId: Long,
search: String,
sort: PlaylistSort,
descending: Boolean,
): RoomRawQuery {
val order = playlistOrder(sort, descending)
return RoomRawQuery("SELECT * $PLAYLISTS_FILTER_BASE ORDER BY $order") { stmt ->
stmt.bindLong(1, sourceId)
stmt.bindText(2, search)
stmt.bindText(3, search)
stmt.bindText(4, search)
stmt.bindText(5, search)
stmt.bindText(6, search)
}
}
private fun albumOrder(
sort: AlbumSort,
descending: Boolean,
): String =
when (sort) {
AlbumSort.Name -> if (descending) ALBUM_ORDER_BY_NAME_REVERSED else ALBUM_ORDER_BY_NAME
AlbumSort.Artist -> if (descending) ALBUM_ORDER_BY_ARTIST_REVERSED else ALBUM_ORDER_BY_ARTIST
AlbumSort.Year -> if (descending) ALBUM_ORDER_BY_YEAR_REVERSED else ALBUM_ORDER_BY_YEAR
AlbumSort.Added -> if (descending) ALBUM_ORDER_BY_ADDED_REVERSED else ALBUM_ORDER_BY_ADDED
AlbumSort.Starred -> if (descending) ALBUM_ORDER_BY_STARRED_REVERSED else ALBUM_ORDER_BY_STARRED
AlbumSort.Frequent -> if (descending) ALBUM_ORDER_BY_FREQUENT_REVERSED else ALBUM_ORDER_BY_FREQUENT
AlbumSort.Recent -> if (descending) ALBUM_ORDER_BY_RECENT_REVERSED else ALBUM_ORDER_BY_RECENT
}
private fun artistOrder(
sort: ArtistSort,
descending: Boolean,
): String =
when (sort) {
ArtistSort.Name -> {
if (descending) ARTIST_ORDER_BY_NAME_REVERSED else ARTIST_ORDER_BY_NAME
}
ArtistSort.AlbumCount -> {
if (descending) ARTIST_ORDER_BY_ALBUM_COUNT_REVERSED else ARTIST_ORDER_BY_ALBUM_COUNT
}
ArtistSort.Starred -> {
if (descending) ARTIST_ORDER_BY_STARRED_REVERSED else ARTIST_ORDER_BY_STARRED
}
ArtistSort.Frequent -> {
if (descending) ARTIST_ORDER_BY_FREQUENT_REVERSED else ARTIST_ORDER_BY_FREQUENT
}
ArtistSort.Recent -> {
if (descending) ARTIST_ORDER_BY_RECENT_REVERSED else ARTIST_ORDER_BY_RECENT
}
}
private fun playlistOrder(
sort: PlaylistSort,
descending: Boolean,
): String =
when (sort) {
PlaylistSort.Name -> if (descending) PLAYLIST_ORDER_BY_NAME_REVERSED else PLAYLIST_ORDER_BY_NAME
PlaylistSort.Added -> if (descending) PLAYLIST_ORDER_BY_ADDED_REVERSED else PLAYLIST_ORDER_BY_ADDED
PlaylistSort.Updated -> if (descending) PLAYLIST_ORDER_BY_UPDATED_REVERSED else PLAYLIST_ORDER_BY_UPDATED
}
@@ -5,6 +5,9 @@ import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.PagingSource
import com.subtracks.data.db.SubtracksDatabase
import com.subtracks.data.db.albumsDefaultQuery
import com.subtracks.data.db.artistsDefaultQuery
import com.subtracks.data.db.playlistsDefaultQuery
import com.subtracks.data.model.Album
import com.subtracks.data.model.AlbumSongItem
import com.subtracks.data.model.Artist
@@ -148,60 +151,64 @@ class LibraryRepository(
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, downloadedFilter)
} else {
dao.albumsByName(sourceId, starred.ordinal, search, downloadedFilter)
if (!downloaded) {
dao.albumsPaging(albumsDefaultQuery(sourceId, starred.ordinal, search, sort, descending))
} else {
when (sort) {
AlbumSort.Name -> {
if (descending) {
dao.albumsByNameReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.albumsByName(sourceId, starred.ordinal, search, downloadedFilter)
}
}
}
AlbumSort.Artist -> {
if (descending) {
dao.albumsByArtistReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.albumsByArtist(sourceId, starred.ordinal, search, downloadedFilter)
AlbumSort.Artist -> {
if (descending) {
dao.albumsByArtistReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.albumsByArtist(sourceId, starred.ordinal, search, downloadedFilter)
}
}
}
AlbumSort.Year -> {
if (descending) {
dao.albumsByYearReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.albumsByYear(sourceId, starred.ordinal, search, downloadedFilter)
AlbumSort.Year -> {
if (descending) {
dao.albumsByYearReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.albumsByYear(sourceId, starred.ordinal, search, downloadedFilter)
}
}
}
AlbumSort.Added -> {
if (descending) {
dao.albumsByRecentlyAddedReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.albumsByRecentlyAdded(sourceId, starred.ordinal, search, downloadedFilter)
AlbumSort.Added -> {
if (descending) {
dao.albumsByRecentlyAddedReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.albumsByRecentlyAdded(sourceId, starred.ordinal, search, downloadedFilter)
}
}
}
AlbumSort.Starred -> {
if (descending) {
dao.albumsByStarredReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.albumsByStarred(sourceId, starred.ordinal, search, downloadedFilter)
AlbumSort.Starred -> {
if (descending) {
dao.albumsByStarredReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.albumsByStarred(sourceId, starred.ordinal, search, downloadedFilter)
}
}
}
AlbumSort.Frequent -> {
if (descending) {
dao.albumsByFrequentReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.albumsByFrequent(sourceId, starred.ordinal, search, downloadedFilter)
AlbumSort.Frequent -> {
if (descending) {
dao.albumsByFrequentReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.albumsByFrequent(sourceId, starred.ordinal, search, downloadedFilter)
}
}
}
AlbumSort.Recent -> {
if (descending) {
dao.albumsByRecentReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.albumsByRecent(sourceId, starred.ordinal, search, downloadedFilter)
AlbumSort.Recent -> {
if (descending) {
dao.albumsByRecentReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.albumsByRecent(sourceId, starred.ordinal, search, downloadedFilter)
}
}
}
}
@@ -218,44 +225,48 @@ class LibraryRepository(
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, downloadedFilter)
} else {
dao.artistsByName(sourceId, starred.ordinal, search, downloadedFilter)
if (!downloaded) {
dao.artistsPaging(artistsDefaultQuery(sourceId, starred.ordinal, search, sort, descending))
} else {
when (sort) {
ArtistSort.Name -> {
if (descending) {
dao.artistsByNameReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.artistsByName(sourceId, starred.ordinal, search, downloadedFilter)
}
}
}
ArtistSort.AlbumCount -> {
if (descending) {
dao.artistsByAlbumCountReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.artistsByAlbumCount(sourceId, starred.ordinal, search, downloadedFilter)
ArtistSort.AlbumCount -> {
if (descending) {
dao.artistsByAlbumCountReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.artistsByAlbumCount(sourceId, starred.ordinal, search, downloadedFilter)
}
}
}
ArtistSort.Starred -> {
if (descending) {
dao.artistsByStarredReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.artistsByStarred(sourceId, starred.ordinal, search, downloadedFilter)
ArtistSort.Starred -> {
if (descending) {
dao.artistsByStarredReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.artistsByStarred(sourceId, starred.ordinal, search, downloadedFilter)
}
}
}
ArtistSort.Frequent -> {
if (descending) {
dao.artistsByFrequentReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.artistsByFrequent(sourceId, starred.ordinal, search, downloadedFilter)
ArtistSort.Frequent -> {
if (descending) {
dao.artistsByFrequentReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.artistsByFrequent(sourceId, starred.ordinal, search, downloadedFilter)
}
}
}
ArtistSort.Recent -> {
if (descending) {
dao.artistsByRecentReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.artistsByRecent(sourceId, starred.ordinal, search, downloadedFilter)
ArtistSort.Recent -> {
if (descending) {
dao.artistsByRecentReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.artistsByRecent(sourceId, starred.ordinal, search, downloadedFilter)
}
}
}
}
@@ -271,28 +282,32 @@ class LibraryRepository(
pager(40) {
val dao = db.libraryDao()
val downloadedFilter = if (downloaded) 1 else 0
when (sort) {
PlaylistSort.Name -> {
if (descending) {
dao.playlistsByNameReversed(sourceId, search, downloadedFilter)
} else {
dao.playlistsByName(sourceId, search, downloadedFilter)
if (!downloaded) {
dao.playlistsPaging(playlistsDefaultQuery(sourceId, search, sort, descending))
} else {
when (sort) {
PlaylistSort.Name -> {
if (descending) {
dao.playlistsByNameReversed(sourceId, search, downloadedFilter)
} else {
dao.playlistsByName(sourceId, search, downloadedFilter)
}
}
}
PlaylistSort.Added -> {
if (descending) {
dao.playlistsByAddedReversed(sourceId, search, downloadedFilter)
} else {
dao.playlistsByAdded(sourceId, search, downloadedFilter)
PlaylistSort.Added -> {
if (descending) {
dao.playlistsByAddedReversed(sourceId, search, downloadedFilter)
} else {
dao.playlistsByAdded(sourceId, search, downloadedFilter)
}
}
}
PlaylistSort.Updated -> {
if (descending) {
dao.playlistsByUpdatedReversed(sourceId, search, downloadedFilter)
} else {
dao.playlistsByUpdated(sourceId, search, downloadedFilter)
PlaylistSort.Updated -> {
if (descending) {
dao.playlistsByUpdatedReversed(sourceId, search, downloadedFilter)
} else {
dao.playlistsByUpdated(sourceId, search, downloadedFilter)
}
}
}
}