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 39f43b16..51868bf6 100644 --- a/app/src/main/kotlin/com/subtracks/data/db/LibraryDao.kt +++ b/app/src/main/kotlin/com/subtracks/data/db/LibraryDao.kt @@ -10,15 +10,21 @@ import androidx.room3.Transaction import androidx.room3.Upsert import androidx.room3.paging.PagingSourceDaoReturnTypeConverter import com.subtracks.data.model.Album +import com.subtracks.data.model.AlbumArtistRef +import com.subtracks.data.model.AlbumSearch import com.subtracks.data.model.AlbumSongItem import com.subtracks.data.model.Artist import com.subtracks.data.model.ArtistBio +import com.subtracks.data.model.ArtistSearch import com.subtracks.data.model.Disc import com.subtracks.data.model.DiscKey import com.subtracks.data.model.Playlist +import com.subtracks.data.model.PlaylistSearch import com.subtracks.data.model.PlaylistSong import com.subtracks.data.model.PlaylistSongItem import com.subtracks.data.model.Song +import com.subtracks.data.model.SongArtistRef +import com.subtracks.data.model.SongDownload import com.subtracks.data.model.Source import kotlinx.coroutines.flow.Flow @@ -36,20 +42,11 @@ internal const val DOWNLOADED_SONG = "EXISTS (SELECT 1 FROM song_downloads sd WHERE sd.sourceId = playlist_songs.sourceId " + "AND sd.songId = playlist_songs.songId AND sd.status = 'Completed')" -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 (length(:search) >= 3 AND rowid IN (SELECT rowid FROM album_search " + - "WHERE album_search MATCH '\"' || replace(:search, '\"', '\"\"') || '\"')) " + - "OR (length(:search) < 3 AND (instr(lower(name), lower(:search)) > 0 " + - "OR instr(lower(albumArtist), lower(:search)) > 0 " + - "OR instr(lower(sortName), lower(:search)) > 0 " + - "OR instr(lower(albumArtistSort), lower(:search)) > 0))) " - +internal const val ALBUM_ORDER_BY_NAME = "sortName COLLATE NOCASE, id" +internal const val ALBUM_ORDER_BY_NAME_REVERSED = "sortName COLLATE NOCASE DESC, id DESC" +internal const val ALBUM_ORDER_BY_ARTIST = "albumArtistSort COLLATE NOCASE, year, sortName COLLATE NOCASE, id" +internal const val ALBUM_ORDER_BY_ARTIST_REVERSED = + "albumArtistSort COLLATE NOCASE DESC, year DESC, sortName COLLATE NOCASE DESC, id DESC" internal const val ALBUM_ORDER_BY_YEAR = "year DESC, sortName COLLATE NOCASE, id" internal const val ALBUM_ORDER_BY_YEAR_REVERSED = "year ASC, sortName COLLATE NOCASE DESC, id DESC" internal const val ALBUM_ORDER_BY_ADDED = "created DESC, sortName COLLATE NOCASE, id" @@ -154,7 +151,7 @@ internal const val DOWNLOADED_ALBUM_ROWS = "JOIN song_downloads sd ON sd.sourceId = songs.sourceId AND sd.songId = songs.id " + "WHERE song_artists.sourceId = :sourceId AND sd.status = 'Completed'" -private const val DOWNLOADED_ARTIST_ALBUMS = +internal const val DOWNLOADED_ARTIST_ALBUMS = "SELECT albumId AS id FROM ($DOWNLOADED_ALBUM_ROWS) WHERE artistId = artists.id" private const val DOWNLOADED_COVER_ALBUM_ROWS = @@ -300,33 +297,13 @@ internal const val DOWNLOADED_ARTIST_ART_UPDATE_SQL = "FROM desired WHERE artists.rowid = desired.rowid AND " + "artists.downloadedCoverArts != desired.downloadedCoverArts" -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 $ARTIST_CREDITED " + - "AND (:downloadedFilter = 0 OR EXISTS (" + - "SELECT 1 FROM ($ARTIST_DOWNLOAD_ROWS) dl " + - "WHERE dl.id = artists.id AND dl.status = 'Completed')) " + - "AND (:search = '' " + - "OR (length(:search) >= 3 AND rowid IN (SELECT rowid FROM artist_search " + - "WHERE artist_search MATCH '\"' || replace(:search, '\"', '\"\"') || '\"')) " + - "OR (length(:search) < 3 AND (instr(lower(name), lower(:search)) > 0 " + - "OR instr(lower(sortName), lower(:search)) > 0))) " +internal const val ARTIST_COLUMNS_TAIL = + "artists.starred, artists.coverArt, artists.artistImageUrl, artists.coverArts, " + + "artists.downloadedCoverArts, artists.playCount, artists.played, artists.songCount, " + + "artists.sortName, artists.credited" internal const val ARTIST_COLUMNS = - "artists.sourceId, artists.id, artists.name, artists.albumCount, artists.starred, " + - "artists.coverArt, artists.artistImageUrl, artists.coverArts, artists.downloadedCoverArts, " + - "artists.playCount, artists.played, artists.songCount, artists.sortName, artists.credited" - -// With the Downloaded filter on, count the albums that actually have a download so the number under -// an artist matches the albums its detail screen lists. -internal const val ARTISTS_SELECT = - "SELECT artists.sourceId, artists.id, artists.name, " + - "CASE WHEN :downloadedFilter = 1 THEN (" + - "SELECT COUNT(*) FROM ($DOWNLOADED_ARTIST_ALBUMS)" + - ") ELSE artists.albumCount END AS albumCount, " + - "artists.starred, artists.coverArt, artists.artistImageUrl, artists.coverArts, artists.downloadedCoverArts, " + - "artists.playCount, artists.played, artists.songCount, artists.sortName, artists.credited " + "artists.sourceId, artists.id, artists.name, artists.albumCount, $ARTIST_COLUMNS_TAIL" internal const val ARTIST_ORDER_BY_ALBUM_COUNT = "albumCount DESC, sortName COLLATE NOCASE, id" internal const val ARTIST_ORDER_BY_ALBUM_COUNT_REVERSED = "albumCount ASC, sortName COLLATE NOCASE DESC, id DESC" @@ -346,27 +323,6 @@ private const val SONG_NAME_SORT_KEY_MODE = "(CASE :sortMode WHEN 'Name' THEN songs.title WHEN 'NonLatin' THEN (CASE WHEN songs.title GLOB '*[A-Za-z]*' THEN songs.title ELSE songs.sortName END) ELSE songs.sortName END) COLLATE NOCASE" internal const val SONG_SEARCH_ORDER_SORT_MODE = "$SONG_NAME_SORT_KEY_MODE, songs.id" -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 (length(:search) >= 3 AND rowid IN (SELECT rowid FROM playlist_search " + - "WHERE playlist_search MATCH '\"' || replace(:search, '\"', '\"\"') || '\"')) " + - "OR (length(:search) < 3 AND instr(lower(name), lower(:search)) > 0)) " - -// With the Downloaded filter on, count the songs that actually have a download so the number under -// a playlist matches the songs its detail screen lists. -internal const val PLAYLISTS_SELECT = - "SELECT playlists.sourceId, playlists.id, playlists.name, playlists.comment, playlists.coverArt, " + - "CASE WHEN :downloadedFilter = 1 THEN (" + - "SELECT COUNT(DISTINCT ps.songId) FROM playlist_songs ps " + - "JOIN song_downloads sd ON sd.sourceId = ps.sourceId AND sd.songId = ps.songId AND sd.status = 'Completed' " + - "WHERE ps.sourceId = playlists.sourceId AND ps.playlistId = playlists.id " + - ") ELSE playlists.songCount END AS songCount, " + - "playlists.created, playlists.changed, playlists.duration, playlists.owner, playlists.shared " - internal const val PLAYLIST_ORDER_BY_NAME = "name COLLATE NOCASE, id" internal const val PLAYLIST_ORDER_BY_NAME_REVERSED = "name COLLATE NOCASE DESC, id DESC" internal const val PLAYLIST_ORDER_BY_ADDED = "created DESC, name COLLATE NOCASE, id" @@ -804,245 +760,27 @@ interface LibraryDao { @RawQuery(observedEntities = [Playlist::class]) fun playlistsPaging(query: RoomRawQuery): PagingSource - @Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_NAME_SORT_MODE") - fun albumsByName( - sourceId: Long, - starredFilter: Int, - search: String, - downloadedFilter: Int = 0, - sortMode: String = "", - ): PagingSource + // These observe every table the statement reads, matching what Room inferred for the + // hand-written downloaded queries, so download and library writes invalidate as before. + @RawQuery(observedEntities = [Album::class, Song::class, SongDownload::class, AlbumSearch::class]) + fun downloadedAlbumsPaging(query: RoomRawQuery): PagingSource - @Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_NAME_SORT_MODE_REVERSED") - fun albumsByNameReversed( - sourceId: Long, - starredFilter: Int, - search: String, - downloadedFilter: Int = 0, - sortMode: String = "", - ): PagingSource + @RawQuery( + observedEntities = [ + Album::class, + Song::class, + SongDownload::class, + AlbumArtistRef::class, + SongArtistRef::class, + Artist::class, + Source::class, + ArtistSearch::class, + ], + ) + fun downloadedArtistsPaging(query: RoomRawQuery): PagingSource - @Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_ARTIST_SORT_MODE") - fun albumsByArtist( - sourceId: Long, - starredFilter: Int, - search: String, - downloadedFilter: Int = 0, - sortMode: String = "", - ): PagingSource - - @Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_ARTIST_SORT_MODE_REVERSED") - fun albumsByArtistReversed( - sourceId: Long, - starredFilter: Int, - search: String, - downloadedFilter: Int = 0, - sortMode: String = "", - ): PagingSource - - @Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_YEAR") - fun albumsByYear( - sourceId: Long, - starredFilter: Int, - search: String, - downloadedFilter: Int = 0, - ): PagingSource - - @Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_YEAR_REVERSED") - fun albumsByYearReversed( - sourceId: Long, - starredFilter: Int, - search: String, - downloadedFilter: Int = 0, - ): PagingSource - - @Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_ADDED") - fun albumsByRecentlyAdded( - sourceId: Long, - starredFilter: Int, - search: String, - downloadedFilter: Int = 0, - ): PagingSource - - @Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_ADDED_REVERSED") - fun albumsByRecentlyAddedReversed( - sourceId: Long, - starredFilter: Int, - search: String, - downloadedFilter: Int = 0, - ): PagingSource - - @Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_STARRED") - fun albumsByStarred( - sourceId: Long, - starredFilter: Int, - search: String, - downloadedFilter: Int = 0, - ): PagingSource - - @Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_STARRED_REVERSED") - fun albumsByStarredReversed( - sourceId: Long, - starredFilter: Int, - search: String, - downloadedFilter: Int = 0, - ): PagingSource - - @Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_FREQUENT") - fun albumsByFrequent( - sourceId: Long, - starredFilter: Int, - search: String, - downloadedFilter: Int = 0, - ): PagingSource - - @Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_FREQUENT_REVERSED") - fun albumsByFrequentReversed( - sourceId: Long, - starredFilter: Int, - search: String, - downloadedFilter: Int = 0, - ): PagingSource - - @Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_RECENT") - fun albumsByRecent( - sourceId: Long, - starredFilter: Int, - search: String, - downloadedFilter: Int = 0, - ): PagingSource - - @Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_RECENT_REVERSED") - fun albumsByRecentReversed( - sourceId: Long, - starredFilter: Int, - search: String, - downloadedFilter: Int = 0, - ): PagingSource - - @Query("$ARTISTS_SELECT $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_NAME_SORT_MODE") - fun artistsByName( - sourceId: Long, - starredFilter: Int, - search: String, - downloadedFilter: Int = 0, - sortMode: String = "", - ): PagingSource - - @Query("$ARTISTS_SELECT $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_NAME_SORT_MODE_REVERSED") - fun artistsByNameReversed( - sourceId: Long, - starredFilter: Int, - search: String, - downloadedFilter: Int = 0, - sortMode: String = "", - ): PagingSource - - @Query("$ARTISTS_SELECT $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_ALBUM_COUNT") - fun artistsByAlbumCount( - sourceId: Long, - starredFilter: Int, - search: String, - downloadedFilter: Int = 0, - ): PagingSource - - @Query("$ARTISTS_SELECT $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_ALBUM_COUNT_REVERSED") - fun artistsByAlbumCountReversed( - sourceId: Long, - starredFilter: Int, - search: String, - downloadedFilter: Int = 0, - ): PagingSource - - @Query("$ARTISTS_SELECT $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_STARRED") - fun artistsByStarred( - sourceId: Long, - starredFilter: Int, - search: String, - downloadedFilter: Int = 0, - ): PagingSource - - @Query("$ARTISTS_SELECT $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_STARRED_REVERSED") - fun artistsByStarredReversed( - sourceId: Long, - starredFilter: Int, - search: String, - downloadedFilter: Int = 0, - ): PagingSource - - @Query("$ARTISTS_SELECT $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_FREQUENT") - fun artistsByFrequent( - sourceId: Long, - starredFilter: Int, - search: String, - downloadedFilter: Int = 0, - ): PagingSource - - @Query("$ARTISTS_SELECT $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_FREQUENT_REVERSED") - fun artistsByFrequentReversed( - sourceId: Long, - starredFilter: Int, - search: String, - downloadedFilter: Int = 0, - ): PagingSource - - @Query("$ARTISTS_SELECT $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_RECENT") - fun artistsByRecent( - sourceId: Long, - starredFilter: Int, - search: String, - downloadedFilter: Int = 0, - ): PagingSource - - @Query("$ARTISTS_SELECT $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_RECENT_REVERSED") - fun artistsByRecentReversed( - sourceId: Long, - starredFilter: Int, - search: String, - downloadedFilter: Int = 0, - ): PagingSource - - @Query("$PLAYLISTS_SELECT $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_NAME") - fun playlistsByName( - sourceId: Long, - search: String, - downloadedFilter: Int = 0, - ): PagingSource - - @Query("$PLAYLISTS_SELECT $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_NAME_REVERSED") - fun playlistsByNameReversed( - sourceId: Long, - search: String, - downloadedFilter: Int = 0, - ): PagingSource - - @Query("$PLAYLISTS_SELECT $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_ADDED") - fun playlistsByAdded( - sourceId: Long, - search: String, - downloadedFilter: Int = 0, - ): PagingSource - - @Query("$PLAYLISTS_SELECT $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_ADDED_REVERSED") - fun playlistsByAddedReversed( - sourceId: Long, - search: String, - downloadedFilter: Int = 0, - ): PagingSource - - @Query("$PLAYLISTS_SELECT $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_UPDATED") - fun playlistsByUpdated( - sourceId: Long, - search: String, - downloadedFilter: Int = 0, - ): PagingSource - - @Query("$PLAYLISTS_SELECT $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_UPDATED_REVERSED") - fun playlistsByUpdatedReversed( - sourceId: Long, - search: String, - downloadedFilter: Int = 0, - ): PagingSource + @RawQuery(observedEntities = [PlaylistSong::class, SongDownload::class, Playlist::class, PlaylistSearch::class]) + fun downloadedPlaylistsPaging(query: RoomRawQuery): PagingSource @Query("SELECT * FROM albums WHERE sourceId = :sourceId AND played IS NOT NULL ORDER BY played DESC, sortName COLLATE NOCASE, id") fun homeRecentlyPlayedAlbums(sourceId: Long): PagingSource diff --git a/app/src/main/kotlin/com/subtracks/data/db/LibraryPaging.kt b/app/src/main/kotlin/com/subtracks/data/db/LibraryPaging.kt index 984b710c..9a9ac3f5 100644 --- a/app/src/main/kotlin/com/subtracks/data/db/LibraryPaging.kt +++ b/app/src/main/kotlin/com/subtracks/data/db/LibraryPaging.kt @@ -7,49 +7,161 @@ import com.subtracks.data.prefs.ArtistSort import com.subtracks.data.prefs.PlaylistSort import com.subtracks.data.prefs.SortNameMode -// 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 " + - "OR instr(lower(sortName), lower(?)) > 0 " + - "OR instr(lower(albumArtistSort), lower(?)) > 0))) " +// The default filters never mention song_downloads, so the paging sources used without the +// Downloaded filter are not invalidated by download writes (including progress updates). The +// downloaded variants are the same statements the hand-written queries used. +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 (:search = '' + OR (length(:search) >= $SEARCH_MIN_LENGTH AND rowid IN (SELECT rowid FROM album_search + WHERE album_search MATCH '"' || replace(:search, '"', '""') || '"')) + OR (length(:search) < $SEARCH_MIN_LENGTH AND (instr(lower(name), lower(:search)) > 0 + OR instr(lower(albumArtist), lower(:search)) > 0 + OR instr(lower(sortName), lower(:search)) > 0 + OR instr(lower(albumArtistSort), lower(:search)) > 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 $ARTIST_CREDITED " + - "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 " + - "OR instr(lower(sortName), lower(?)) > 0))) " +internal const val ALBUMS_FILTER_DOWNLOADED = """ +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 (length(:search) >= $SEARCH_MIN_LENGTH AND rowid IN (SELECT rowid FROM album_search + WHERE album_search MATCH '"' || replace(:search, '"', '""') || '"')) + OR (length(:search) < $SEARCH_MIN_LENGTH AND (instr(lower(name), lower(:search)) > 0 + OR instr(lower(albumArtist), lower(:search)) > 0 + OR instr(lower(sortName), lower(:search)) > 0 + OR instr(lower(albumArtistSort), lower(:search)) > 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 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 $ARTIST_CREDITED +AND (:search = '' + OR (length(:search) >= $SEARCH_MIN_LENGTH AND rowid IN (SELECT rowid FROM artist_search + WHERE artist_search MATCH '"' || replace(:search, '"', '""') || '"')) + OR (length(:search) < $SEARCH_MIN_LENGTH AND (instr(lower(name), lower(:search)) > 0 + OR instr(lower(sortName), lower(:search)) > 0))) +""" -internal fun albumsDefaultQuery( +internal const val ARTISTS_FILTER_DOWNLOADED = """ +FROM artists WHERE sourceId = :sourceId +AND (:starredFilter = 0 OR (:starredFilter = 1 AND starred IS NOT NULL) OR (:starredFilter = 2 AND starred IS NULL)) +AND $ARTIST_CREDITED +AND (:downloadedFilter = 0 OR EXISTS (SELECT 1 FROM ($ARTIST_DOWNLOAD_ROWS) dl + WHERE dl.id = artists.id AND dl.status = 'Completed')) +AND (:search = '' + OR (length(:search) >= $SEARCH_MIN_LENGTH AND rowid IN (SELECT rowid FROM artist_search + WHERE artist_search MATCH '"' || replace(:search, '"', '""') || '"')) + OR (length(:search) < $SEARCH_MIN_LENGTH AND (instr(lower(name), lower(:search)) > 0 + OR instr(lower(sortName), lower(:search)) > 0))) +""" + +// With the Downloaded filter on, count the albums that actually have a download so the number under +// an artist matches the albums its detail screen lists. +internal const val ARTIST_COLUMNS_DOWNLOADED = """ +artists.sourceId, artists.id, artists.name, +CASE WHEN :downloadedFilter = 1 THEN (SELECT COUNT(*) FROM ($DOWNLOADED_ARTIST_ALBUMS)) ELSE artists.albumCount END AS albumCount, +$ARTIST_COLUMNS_TAIL +""" + +internal const val PLAYLISTS_FILTER = """ +FROM playlists WHERE sourceId = :sourceId +AND (:search = '' + OR (length(:search) >= $SEARCH_MIN_LENGTH AND rowid IN (SELECT rowid FROM playlist_search + WHERE playlist_search MATCH '"' || replace(:search, '"', '""') || '"')) + OR (length(:search) < $SEARCH_MIN_LENGTH AND instr(lower(name), lower(:search)) > 0)) +""" + +internal const val PLAYLISTS_FILTER_DOWNLOADED = """ +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 (length(:search) >= $SEARCH_MIN_LENGTH AND rowid IN (SELECT rowid FROM playlist_search + WHERE playlist_search MATCH '"' || replace(:search, '"', '""') || '"')) + OR (length(:search) < $SEARCH_MIN_LENGTH AND instr(lower(name), lower(:search)) > 0)) +""" + +// With the Downloaded filter on, count the songs that actually have a download so the number under +// a playlist matches the songs its detail screen lists. +internal const val PLAYLISTS_COLUMNS_DOWNLOADED = """ +playlists.sourceId, playlists.id, playlists.name, playlists.comment, playlists.coverArt, +CASE WHEN :downloadedFilter = 1 THEN (SELECT COUNT(DISTINCT ps.songId) + FROM playlist_songs ps + JOIN song_downloads sd ON sd.sourceId = ps.sourceId AND sd.songId = ps.songId AND sd.status = 'Completed' + WHERE ps.sourceId = playlists.sourceId AND ps.playlistId = playlists.id +) ELSE playlists.songCount END AS songCount, +playlists.created, playlists.changed, playlists.duration, playlists.owner, playlists.shared +""" + +internal fun albumsPageQuery( sourceId: Long, starredFilter: Int, search: String, sort: AlbumSort, descending: Boolean, + downloaded: Boolean, sortNameMode: SortNameMode = SortNameMode.SortName, -): RoomRawQuery { - val order = albumOrder(sort, descending, sortNameMode) - return RoomRawQuery("SELECT * $ALBUMS_FILTER_BASE ORDER BY $order") { stmt -> - stmt.bindAlbumFilters(sourceId, starredFilter, search) - } -} +): RoomRawQuery = + rawQuery( + sql = "SELECT * ${if (downloaded) ALBUMS_FILTER_DOWNLOADED else ALBUMS_FILTER} ORDER BY ${albumOrder(sort, descending, sortNameMode)}", + bindings = + mapOf( + "sourceId" to sourceId, + "starredFilter" to starredFilter, + "downloadedFilter" to if (downloaded) 1 else 0, + "search" to search, + ), + ) + +internal fun artistsPageQuery( + sourceId: Long, + starredFilter: Int, + search: String, + sort: ArtistSort, + descending: Boolean, + downloaded: Boolean, + sortNameMode: SortNameMode = SortNameMode.SortName, +): RoomRawQuery = + rawQuery( + sql = + "SELECT ${if (downloaded) ARTIST_COLUMNS_DOWNLOADED else ARTIST_COLUMNS} " + + "${if (downloaded) ARTISTS_FILTER_DOWNLOADED else ARTISTS_FILTER} " + + "ORDER BY ${artistOrder(sort, descending, sortNameMode)}", + bindings = + mapOf( + "sourceId" to sourceId, + "starredFilter" to starredFilter, + "downloadedFilter" to if (downloaded) 1 else 0, + "search" to search, + ), + ) + +internal fun playlistsPageQuery( + sourceId: Long, + search: String, + sort: PlaylistSort, + descending: Boolean, + downloaded: Boolean, +): RoomRawQuery = + rawQuery( + sql = + "SELECT ${if (downloaded) PLAYLISTS_COLUMNS_DOWNLOADED else "*"} " + + "${if (downloaded) PLAYLISTS_FILTER_DOWNLOADED else PLAYLISTS_FILTER} " + + "ORDER BY ${playlistOrder(sort, descending)}", + bindings = + mapOf( + "sourceId" to sourceId, + "downloadedFilter" to if (downloaded) 1 else 0, + "search" to search, + ), + ) internal fun rediscoverAlbumsQuery( sourceId: Long, @@ -57,70 +169,71 @@ internal fun rediscoverAlbumsQuery( search: String, cutoff: Long, ): RoomRawQuery = - RoomRawQuery( - "SELECT * $ALBUMS_FILTER_BASE AND played IS NOT NULL AND played < ? " + - "ORDER BY played ASC, sortName COLLATE NOCASE, id", - ) { stmt -> - stmt.bindAlbumFilters(sourceId, starredFilter, search) - stmt.bindLong(13, cutoff) - } + rawQuery( + sql = + "SELECT * $ALBUMS_FILTER AND played IS NOT NULL AND played < :cutoff " + + "ORDER BY played ASC, sortName COLLATE NOCASE, id", + bindings = + mapOf( + "sourceId" to sourceId, + "starredFilter" to starredFilter, + "search" to search, + "cutoff" to cutoff, + ), + ) -private fun SQLiteStatement.bindAlbumFilters( - sourceId: Long, - starredFilter: Int, - search: String, +// RoomRawQuery binds by position, so the named fragments are rewritten once here instead of +// every caller carrying an index for each repeated :sourceId. +internal fun rawQuery( + sql: String, + bindings: Map, +): RoomRawQuery { + val names = mutableListOf() + val positional = + buildString(sql.length) { + var index = 0 + var quote: Char? = null + while (index < sql.length) { + val char = sql[index] + if (quote != null) { + append(char) + if (char == quote) quote = null + index++ + continue + } + if (char == '\'' || char == '"') { + quote = char + append(char) + index++ + continue + } + if (char == ':' && sql.getOrNull(index + 1).let { it != null && (it.isLetter() || it == '_') }) { + var end = index + 1 + while (end < sql.length && (sql[end].isLetterOrDigit() || sql[end] == '_')) end++ + names += sql.substring(index + 1, end) + append('?') + index = end + continue + } + append(char) + index++ + } + } + return RoomRawQuery(positional) { statement -> + names.forEachIndexed { index, name -> statement.bind(index + 1, bindings.getValue(name)) } + } +} + +private fun SQLiteStatement.bind( + index: Int, + value: Any?, ) { - bindLong(1, sourceId) - bindInt(2, starredFilter) - bindInt(3, starredFilter) - bindInt(4, starredFilter) - bindText(5, search) - bindText(6, search) - bindText(7, search) - bindText(8, search) - bindText(9, search) - bindText(10, search) - bindText(11, search) - bindText(12, search) -} - -internal fun artistsDefaultQuery( - sourceId: Long, - starredFilter: Int, - search: String, - sort: ArtistSort, - descending: Boolean, - sortNameMode: SortNameMode = SortNameMode.SortName, -): RoomRawQuery { - val order = artistOrder(sort, descending, sortNameMode) - return RoomRawQuery("SELECT $ARTIST_COLUMNS $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) - stmt.bindText(10, 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) + when (value) { + null -> bindNull(index) + is Int -> bindLong(index, value.toLong()) + is Long -> bindLong(index, value) + is String -> bindText(index, value) + else -> error("Unsupported SQL parameter: ${value::class.qualifiedName}") } } 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 635d6fcd..d5cd52a8 100644 --- a/app/src/main/kotlin/com/subtracks/data/repo/LibraryRepository.kt +++ b/app/src/main/kotlin/com/subtracks/data/repo/LibraryRepository.kt @@ -7,9 +7,9 @@ import androidx.paging.PagingSource import com.subtracks.R import com.subtracks.UiMessage 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.db.albumsPageQuery +import com.subtracks.data.db.artistsPageQuery +import com.subtracks.data.db.playlistsPageQuery import com.subtracks.data.db.rediscoverAlbumsQuery import com.subtracks.data.model.Album import com.subtracks.data.model.AlbumSongItem @@ -195,68 +195,8 @@ class LibraryRepository( userPreferences.sortNameMode().flatMapLatest { mode -> pager(40, enablePlaceholders = true) { val dao = db.libraryDao() - if (!downloaded) { - dao.albumsPaging(albumsDefaultQuery(sourceId, starred.ordinal, search, sort, descending, mode)) - } else { - val downloadedFilter = 1 - when (sort) { - AlbumSort.Name -> { - if (descending) { - dao.albumsByNameReversed(sourceId, starred.ordinal, search, downloadedFilter, mode.name) - } else { - dao.albumsByName(sourceId, starred.ordinal, search, downloadedFilter, mode.name) - } - } - - AlbumSort.Artist -> { - if (descending) { - dao.albumsByArtistReversed(sourceId, starred.ordinal, search, downloadedFilter, mode.name) - } else { - dao.albumsByArtist(sourceId, starred.ordinal, search, downloadedFilter, mode.name) - } - } - - 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.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.Recent -> { - if (descending) { - dao.albumsByRecentReversed(sourceId, starred.ordinal, search, downloadedFilter) - } else { - dao.albumsByRecent(sourceId, starred.ordinal, search, downloadedFilter) - } - } - } - } + val query = albumsPageQuery(sourceId, starred.ordinal, search, sort, descending, downloaded, mode) + if (downloaded) dao.downloadedAlbumsPaging(query) else dao.albumsPaging(query) } } @@ -271,52 +211,8 @@ class LibraryRepository( userPreferences.sortNameMode().flatMapLatest { mode -> pager(60) { val dao = db.libraryDao() - if (!downloaded) { - dao.artistsPaging(artistsDefaultQuery(sourceId, starred.ordinal, search, sort, descending, mode)) - } else { - val downloadedFilter = 1 - when (sort) { - ArtistSort.Name -> { - if (descending) { - dao.artistsByNameReversed(sourceId, starred.ordinal, search, downloadedFilter, mode.name) - } else { - dao.artistsByName(sourceId, starred.ordinal, search, downloadedFilter, mode.name) - } - } - - 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.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) - } - } - } - } + val query = artistsPageQuery(sourceId, starred.ordinal, search, sort, descending, downloaded, mode) + if (downloaded) dao.downloadedArtistsPaging(query) else dao.artistsPaging(query) } } @@ -329,36 +225,8 @@ class LibraryRepository( ): Flow> = pager(40) { val dao = db.libraryDao() - if (!downloaded) { - dao.playlistsPaging(playlistsDefaultQuery(sourceId, search, sort, descending)) - } else { - val downloadedFilter = 1 - 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.Updated -> { - if (descending) { - dao.playlistsByUpdatedReversed(sourceId, search, downloadedFilter) - } else { - dao.playlistsByUpdated(sourceId, search, downloadedFilter) - } - } - } - } + val query = playlistsPageQuery(sourceId, search, sort, descending, downloaded) + if (downloaded) dao.downloadedPlaylistsPaging(query) else dao.playlistsPaging(query) } fun album( 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 75c0de2a..ffbe95e2 100644 --- a/app/src/test/kotlin/com/subtracks/data/db/LibraryDaoTest.kt +++ b/app/src/test/kotlin/com/subtracks/data/db/LibraryDaoTest.kt @@ -67,17 +67,17 @@ class LibraryDaoTest { assertEquals( listOf("Alpha", "Beta", "Gamma"), - dao.albumsByName(sourceId, starredFilter = 0, search = "").page().map { it.name }, + dao.albumPages(sourceId, starredFilter = 0, search = "").page().map { it.name }, ) assertEquals( listOf("Gamma", "Beta", "Alpha"), - dao.albumsByNameReversed(sourceId, starredFilter = 0, search = "").page().map { it.name }, + dao.albumPages(sourceId, starredFilter = 0, search = "", descending = true).page().map { it.name }, ) assertEquals( listOf("Alpha", "Beta", "Gamma"), - dao.albumsByYear(sourceId, starredFilter = 0, search = "").page().map { it.name }, + dao.albumPages(sourceId, starredFilter = 0, search = "", sort = AlbumSort.Year).page().map { it.name }, ) - assertEquals(listOf("Alpha"), dao.albumsByName(sourceId, starredFilter = 1, search = "").page().map { it.name }) + assertEquals(listOf("Alpha"), dao.albumPages(sourceId, starredFilter = 1, search = "").page().map { it.name }) } @Test @@ -95,14 +95,14 @@ class LibraryDaoTest { assertEquals( listOf("Beta", "Gamma", "Alpha"), - dao.albumsByRecentlyAdded(sourceId, starredFilter = 0, search = "").page().map { it.name }, + dao.albumPages(sourceId, starredFilter = 0, search = "", sort = AlbumSort.Added).page().map { it.name }, ) assertEquals( listOf("Alpha", "Gamma", "Beta"), - dao.albumsByStarred(sourceId, starredFilter = 0, search = "").page().map { it.name }, + dao.albumPages(sourceId, starredFilter = 0, search = "", sort = AlbumSort.Starred).page().map { it.name }, ) - assertEquals(listOf("Gamma"), dao.albumsByName(sourceId, starredFilter = 0, search = "gam").page().map { it.name }) - assertEquals(listOf("Beta"), dao.albumsByName(sourceId, starredFilter = 2, search = "").page().map { it.name }) + assertEquals(listOf("Gamma"), dao.albumPages(sourceId, starredFilter = 0, search = "gam").page().map { it.name }) + assertEquals(listOf("Beta"), dao.albumPages(sourceId, starredFilter = 2, search = "").page().map { it.name }) } @Test @@ -148,19 +148,19 @@ class LibraryDaoTest { assertEquals( listOf("Beta", "Alpha", "Delta", "Gamma"), - dao.albumsByStarred(sourceId, 0, "").page().map { it.name }, + dao.albumPages(sourceId, 0, "", sort = AlbumSort.Starred).page().map { it.name }, ) assertEquals( listOf("Alpha", "Beta", "Gamma", "Delta"), - dao.albumsByStarredReversed(sourceId, 0, "").page().map { it.name }, + dao.albumPages(sourceId, 0, "", sort = AlbumSort.Starred, descending = true).page().map { it.name }, ) assertEquals( listOf("Beta", "Alpha", "Delta", "Gamma"), - dao.artistsByStarred(sourceId, 0, "").page().map { it.name }, + dao.artistPages(sourceId, 0, "", sort = ArtistSort.Starred).page().map { it.name }, ) assertEquals( listOf("Alpha", "Beta", "Gamma", "Delta"), - dao.artistsByStarredReversed(sourceId, 0, "").page().map { it.name }, + dao.artistPages(sourceId, 0, "", sort = ArtistSort.Starred, descending = true).page().map { it.name }, ) } @@ -206,19 +206,19 @@ class LibraryDaoTest { assertEquals( listOf("Two", "One", "Three"), - dao.albumsByFrequent(sourceId, 0, "").page().map { it.name }, + dao.albumPages(sourceId, 0, "", sort = AlbumSort.Frequent).page().map { it.name }, ) assertEquals( listOf("Two", "One", "Three"), - dao.albumsByRecent(sourceId, 0, "").page().map { it.name }, + dao.albumPages(sourceId, 0, "", sort = AlbumSort.Recent).page().map { it.name }, ) assertEquals( listOf("Many", "Few"), - dao.artistsByFrequent(sourceId, 0, "").page().map { it.name }, + dao.artistPages(sourceId, 0, "", sort = ArtistSort.Frequent).page().map { it.name }, ) assertEquals( listOf("Many", "Few"), - dao.artistsByRecent(sourceId, 0, "").page().map { it.name }, + dao.artistPages(sourceId, 0, "", sort = ArtistSort.Recent).page().map { it.name }, ) } @@ -268,12 +268,17 @@ class LibraryDaoTest { val pairs = listOf( - dao.albumsByName(sourceId, 0, "") to dao.albumsByNameReversed(sourceId, 0, ""), - dao.albumsByArtist(sourceId, 0, "") to dao.albumsByArtistReversed(sourceId, 0, ""), - dao.albumsByYear(sourceId, 0, "") to dao.albumsByYearReversed(sourceId, 0, ""), - dao.albumsByRecentlyAdded(sourceId, 0, "") to dao.albumsByRecentlyAddedReversed(sourceId, 0, ""), - dao.albumsByFrequent(sourceId, 0, "") to dao.albumsByFrequentReversed(sourceId, 0, ""), - dao.albumsByRecent(sourceId, 0, "") to dao.albumsByRecentReversed(sourceId, 0, ""), + dao.albumPages(sourceId, 0, "") to dao.albumPages(sourceId, 0, "", descending = true), + dao.albumPages(sourceId, 0, "", sort = AlbumSort.Artist) to + dao.albumPages(sourceId, 0, "", sort = AlbumSort.Artist, descending = true), + dao.albumPages(sourceId, 0, "", sort = AlbumSort.Year) to + dao.albumPages(sourceId, 0, "", sort = AlbumSort.Year, descending = true), + dao.albumPages(sourceId, 0, "", sort = AlbumSort.Added) to + dao.albumPages(sourceId, 0, "", sort = AlbumSort.Added, descending = true), + dao.albumPages(sourceId, 0, "", sort = AlbumSort.Frequent) to + dao.albumPages(sourceId, 0, "", sort = AlbumSort.Frequent, descending = true), + dao.albumPages(sourceId, 0, "", sort = AlbumSort.Recent) to + dao.albumPages(sourceId, 0, "", sort = AlbumSort.Recent, descending = true), ) for ((base, reversed) in pairs) { val ids = base.page().map { it.id } @@ -303,23 +308,23 @@ class LibraryDaoTest { val artistPairs = listOf( - dao.artistsByName(sourceId, 0, "").page().map { it.id } to - dao.artistsByNameReversed(sourceId, 0, "").page().map { it.id }, - dao.artistsByAlbumCount(sourceId, 0, "").page().map { it.id } to - dao.artistsByAlbumCountReversed(sourceId, 0, "").page().map { it.id }, - dao.artistsByFrequent(sourceId, 0, "").page().map { it.id } to - dao.artistsByFrequentReversed(sourceId, 0, "").page().map { it.id }, - dao.artistsByRecent(sourceId, 0, "").page().map { it.id } to - dao.artistsByRecentReversed(sourceId, 0, "").page().map { it.id }, + dao.artistPages(sourceId, 0, "").page().map { it.id } to + dao.artistPages(sourceId, 0, "", descending = true).page().map { it.id }, + dao.artistPages(sourceId, 0, "", sort = ArtistSort.AlbumCount).page().map { it.id } to + dao.artistPages(sourceId, 0, "", sort = ArtistSort.AlbumCount, descending = true).page().map { it.id }, + dao.artistPages(sourceId, 0, "", sort = ArtistSort.Frequent).page().map { it.id } to + dao.artistPages(sourceId, 0, "", sort = ArtistSort.Frequent, descending = true).page().map { it.id }, + dao.artistPages(sourceId, 0, "", sort = ArtistSort.Recent).page().map { it.id } to + dao.artistPages(sourceId, 0, "", sort = ArtistSort.Recent, descending = true).page().map { it.id }, ) val playlistPairs = listOf( - dao.playlistsByName(sourceId, "").page().map { it.id } to - dao.playlistsByNameReversed(sourceId, "").page().map { it.id }, - dao.playlistsByAdded(sourceId, "").page().map { it.id } to - dao.playlistsByAddedReversed(sourceId, "").page().map { it.id }, - dao.playlistsByUpdated(sourceId, "").page().map { it.id } to - dao.playlistsByUpdatedReversed(sourceId, "").page().map { it.id }, + dao.playlistPages(sourceId, "").page().map { it.id } to + dao.playlistPages(sourceId, "", descending = true).page().map { it.id }, + dao.playlistPages(sourceId, "", sort = PlaylistSort.Added).page().map { it.id } to + dao.playlistPages(sourceId, "", sort = PlaylistSort.Added, descending = true).page().map { it.id }, + dao.playlistPages(sourceId, "", sort = PlaylistSort.Updated).page().map { it.id } to + dao.playlistPages(sourceId, "", sort = PlaylistSort.Updated, descending = true).page().map { it.id }, ) for ((base, reversed) in artistPairs + playlistPairs) { assertEquals(base.reversed(), reversed) @@ -357,9 +362,11 @@ class LibraryDaoTest { ALBUM_ORDER_BY_RECENT_REVERSED to "index_albums_recent", ) for ((order, index) in orders) { - val plan = plan("SELECT * $ALBUMS_FILTER ORDER BY $order LIMIT 20 OFFSET 0") - assertTrue(plan, plan.contains("USING INDEX $index")) - assertFalse(plan, plan.contains("TEMP B-TREE")) + for (filter in listOf(ALBUMS_FILTER, ALBUMS_FILTER_DOWNLOADED)) { + val plan = plan("SELECT * $filter ORDER BY $order LIMIT 20 OFFSET 0") + assertTrue(plan, plan.contains("USING INDEX $index")) + assertFalse(plan, plan.contains("TEMP B-TREE")) + } } } @@ -382,9 +389,11 @@ class LibraryDaoTest { ARTIST_ORDER_BY_RECENT_REVERSED to "index_artists_recent", ) for ((order, index) in orders) { - val plan = plan("SELECT * $ARTISTS_FILTER ORDER BY $order LIMIT 20 OFFSET 0") - assertTrue(plan, plan.contains("USING INDEX $index")) - assertFalse(plan, plan.contains("TEMP B-TREE")) + for (filter in listOf(ARTISTS_FILTER, ARTISTS_FILTER_DOWNLOADED)) { + val plan = plan("SELECT * $filter ORDER BY $order LIMIT 20 OFFSET 0") + assertTrue(plan, plan.contains("USING INDEX $index")) + assertFalse(plan, plan.contains("TEMP B-TREE")) + } } } @@ -401,9 +410,11 @@ class LibraryDaoTest { PLAYLIST_ORDER_BY_UPDATED_REVERSED to "index_playlists_updated", ) for ((order, index) in orders) { - val plan = plan("SELECT * $PLAYLISTS_FILTER ORDER BY $order LIMIT 20 OFFSET 0") - assertTrue(plan, plan.contains("USING INDEX $index")) - assertFalse(plan, plan.contains("TEMP B-TREE")) + for (filter in listOf(PLAYLISTS_FILTER, PLAYLISTS_FILTER_DOWNLOADED)) { + val plan = plan("SELECT * $filter ORDER BY $order LIMIT 20 OFFSET 0") + assertTrue(plan, plan.contains("USING INDEX $index")) + assertFalse(plan, plan.contains("TEMP B-TREE")) + } } } @@ -456,17 +467,17 @@ class LibraryDaoTest { assertEquals( listOf("Downloaded"), - dao.albumsByName(sourceId, 0, "", downloadedFilter = 1).page().map { it.name }, + dao.albumPages(sourceId, 0, "", downloaded = true).page().map { it.name }, ) assertEquals( listOf("ar-1"), - dao.artistsByName(sourceId, 0, "", downloadedFilter = 1).page().map { it.id }, + dao.artistPages(sourceId, 0, "", downloaded = true).page().map { it.id }, ) assertEquals( listOf("pl-1"), - dao.playlistsByName(sourceId, "", downloadedFilter = 1).page().map { it.id }, + dao.playlistPages(sourceId, "", downloaded = true).page().map { it.id }, ) - assertEquals(3, dao.albumsByName(sourceId, 0, "").page().size) + assertEquals(3, dao.albumPages(sourceId, 0, "").page().size) } @Test @@ -628,7 +639,7 @@ class LibraryDaoTest { dao.recomputeArtistArt(sourceId) - val artists = dao.artistsByName(sourceId, 0, "").page().associateBy { it.id } + val artists = dao.artistPages(sourceId, 0, "").page().associateBy { it.id } assertEquals("own-art", artists.getValue("ar-own").coverArt) assertNull(artists.getValue("ar-album").coverArt) @@ -648,7 +659,7 @@ class LibraryDaoTest { assertEquals( listOf("album-ref-art"), dao - .artistsPaging(artistsDefaultQuery(sourceId, 0, "", ArtistSort.Name, false)) + .artistPages(sourceId, 0, "", ArtistSort.Name, false) .page() .first { it.id == "ar-album-ref" } .coverArts, @@ -904,7 +915,7 @@ class LibraryDaoTest { db.downloadDao().upsert(SongDownload(sourceId, "s1", DownloadStatus.Completed)) assertEquals( listOf("Album Artist"), - dao.artistsByName(sourceId, 0, "", downloadedFilter = 1).page().map { it.name }, + dao.artistPages(sourceId, 0, "", downloaded = true).page().map { it.name }, ) assertEquals( listOf("Album Artist"), @@ -913,7 +924,7 @@ class LibraryDaoTest { db.downloadDao().upsert(SongDownload(sourceId, "s2", DownloadStatus.Completed)) - val filtered = dao.artistsByName(sourceId, 0, "", downloadedFilter = 1).page() + val filtered = dao.artistPages(sourceId, 0, "", downloaded = true).page() assertEquals(listOf("Album Artist", "Guest"), filtered.map { it.name }) assertEquals(listOf(1L, 1L), filtered.map { it.albumCount }) @@ -961,14 +972,14 @@ class LibraryDaoTest { ) db.downloadDao().upsert(SongDownload(sourceId, "s1", DownloadStatus.Completed)) - val filtered = dao.artistsByName(sourceId, 0, "", downloadedFilter = 1).page() + val filtered = dao.artistPages(sourceId, 0, "", downloaded = true).page() assertEquals(1, filtered.size) assertEquals(1L, filtered.first().albumCount) assertEquals( 5L, dao - .artistsByName(sourceId, 0, "") + .artistPages(sourceId, 0, "") .page() .first { it.id == "ar-1" } .albumCount, @@ -1006,7 +1017,7 @@ class LibraryDaoTest { // ar-1 has two downloaded albums, ar-2 one, so the downloaded order is ar-1 first even // though the synced counts would put ar-2 first. - val filtered = dao.artistsByAlbumCount(sourceId, 0, "", downloadedFilter = 1).page() + val filtered = dao.artistPages(sourceId, 0, "", downloaded = true, sort = ArtistSort.AlbumCount).page() assertEquals(listOf("ar-1", "ar-2"), filtered.map { it.id }) assertEquals(listOf(2L, 1L), filtered.map { it.albumCount }) } @@ -1036,7 +1047,7 @@ class LibraryDaoTest { ) db.downloadDao().upsert(SongDownload(sourceId, "s1", DownloadStatus.Completed)) - val filtered = dao.playlistsByName(sourceId, "", downloadedFilter = 1).page() + val filtered = dao.playlistPages(sourceId, "", downloaded = true).page() assertEquals(1, filtered.size) assertEquals(1L, filtered.first().songCount) assertEquals("austin", filtered.first().owner) @@ -1045,7 +1056,7 @@ class LibraryDaoTest { assertEquals( 5L, dao - .playlistsByName(sourceId, "") + .playlistPages(sourceId, "") .page() .first { it.id == "pl-1" } .songCount, @@ -1064,11 +1075,11 @@ class LibraryDaoTest { ), ) - assertEquals(listOf("Gamma"), dao.albumsByName(sourceId, 0, "amm").page().map { it.name }) - assertEquals(listOf("Gamma"), dao.albumsByName(sourceId, 0, "GAM").page().map { it.name }) - assertEquals(listOf("Gamma"), dao.albumsByName(sourceId, 0, "ga").page().map { it.name }) - assertEquals(listOf("Alpha"), dao.albumsByName(sourceId, 0, "Else").page().map { it.name }) - assertTrue(dao.albumsByName(sourceId, 0, "zzz").page().isEmpty()) + assertEquals(listOf("Gamma"), dao.albumPages(sourceId, 0, "amm").page().map { it.name }) + assertEquals(listOf("Gamma"), dao.albumPages(sourceId, 0, "GAM").page().map { it.name }) + assertEquals(listOf("Gamma"), dao.albumPages(sourceId, 0, "ga").page().map { it.name }) + assertEquals(listOf("Alpha"), dao.albumPages(sourceId, 0, "Else").page().map { it.name }) + assertTrue(dao.albumPages(sourceId, 0, "zzz").page().isEmpty()) } @Test @@ -1077,17 +1088,17 @@ class LibraryDaoTest { val sourceId = source() val dao = db.libraryDao() dao.upsertAlbums(listOf(album(sourceId, "a", "Gamma", year = null, starred = null))) - assertEquals(listOf("Gamma"), dao.albumsByName(sourceId, 0, "gam").page().map { it.name }) + assertEquals(listOf("Gamma"), dao.albumPages(sourceId, 0, "gam").page().map { it.name }) dao.setAlbumStar(sourceId, "a", 7) - assertEquals(listOf("Gamma"), dao.albumsByName(sourceId, 0, "gam").page().map { it.name }) + assertEquals(listOf("Gamma"), dao.albumPages(sourceId, 0, "gam").page().map { it.name }) dao.upsertAlbums(listOf(album(sourceId, "a", "Delta", year = null, starred = null))) - assertTrue(dao.albumsByName(sourceId, 0, "gam").page().isEmpty()) - assertEquals(listOf("Delta"), dao.albumsByName(sourceId, 0, "elt").page().map { it.name }) + assertTrue(dao.albumPages(sourceId, 0, "gam").page().isEmpty()) + assertEquals(listOf("Delta"), dao.albumPages(sourceId, 0, "elt").page().map { it.name }) dao.deleteAlbums(sourceId, listOf("a")) - assertTrue(dao.albumsByName(sourceId, 0, "elt").page().isEmpty()) + assertTrue(dao.albumPages(sourceId, 0, "elt").page().isEmpty()) } @Test @@ -1101,8 +1112,8 @@ class LibraryDaoTest { val second = source() dao.upsertAlbums(listOf(album(second, "a", "Beta", year = null, starred = null))) - assertEquals(listOf("Beta"), dao.albumsByName(second, 0, "eta").page().map { it.name }) - assertTrue(dao.albumsByName(second, 0, "gam").page().isEmpty()) + assertEquals(listOf("Beta"), dao.albumPages(second, 0, "eta").page().map { it.name }) + assertTrue(dao.albumPages(second, 0, "gam").page().isEmpty()) } @Test @@ -1113,10 +1124,10 @@ class LibraryDaoTest { dao.upsertArtists(listOf(artist(sourceId, "r1", "The Beatles", albumCount = 1))) dao.upsertPlaylists(listOf(playlist(sourceId, "l1", "Road Trip", created = 0, changed = 0))) - assertEquals(listOf("r1"), dao.artistsByName(sourceId, 0, "eat").page().map { it.id }) - assertEquals(listOf("l1"), dao.playlistsByName(sourceId, "rip").page().map { it.id }) - assertTrue(dao.artistsByName(sourceId, 0, "rip").page().isEmpty()) - assertTrue(dao.playlistsByName(sourceId, "eat").page().isEmpty()) + assertEquals(listOf("r1"), dao.artistPages(sourceId, 0, "eat").page().map { it.id }) + assertEquals(listOf("l1"), dao.playlistPages(sourceId, "rip").page().map { it.id }) + assertTrue(dao.artistPages(sourceId, 0, "rip").page().isEmpty()) + assertTrue(dao.playlistPages(sourceId, "eat").page().isEmpty()) } @Test @@ -1131,10 +1142,10 @@ class LibraryDaoTest { ), ) - assertEquals(listOf("Say \"Hello\""), dao.albumsByName(sourceId, 0, "\"Hel").page().map { it.name }) + assertEquals(listOf("Say \"Hello\""), dao.albumPages(sourceId, 0, "\"Hel").page().map { it.name }) assertEquals( listOf("Say \"Hello\""), - dao.albumsPaging(albumsDefaultQuery(sourceId, 0, "\"Hel", AlbumSort.Name, false)).page().map { it.name }, + dao.albumPages(sourceId, 0, "\"Hel", AlbumSort.Name, false).page().map { it.name }, ) } @@ -1145,7 +1156,7 @@ class LibraryDaoTest { val dao = db.libraryDao() dao.upsertAlbums(listOf(album(sourceId, "a", "Gamma", year = null, starred = null).copy(albumArtist = null))) - assertEquals(listOf("Gamma"), dao.albumsByName(sourceId, 0, "amm").page().map { it.name }) + assertEquals(listOf("Gamma"), dao.albumPages(sourceId, 0, "amm").page().map { it.name }) } @Test @@ -1274,35 +1285,35 @@ class LibraryDaoTest { assertEquals( listOf("Alpha", "Beta", "Gamma"), - dao.albumsPaging(albumsDefaultQuery(sourceId, 0, "", AlbumSort.Name, false)).page().map { it.name }, + dao.albumPages(sourceId, 0, "", AlbumSort.Name, false).page().map { it.name }, ) assertEquals( listOf("Gamma", "Beta", "Alpha"), - dao.albumsPaging(albumsDefaultQuery(sourceId, 0, "", AlbumSort.Name, true)).page().map { it.name }, + dao.albumPages(sourceId, 0, "", AlbumSort.Name, true).page().map { it.name }, ) assertEquals( listOf("Alpha", "Beta", "Gamma"), - dao.albumsPaging(albumsDefaultQuery(sourceId, 0, "", AlbumSort.Year, false)).page().map { it.name }, + dao.albumPages(sourceId, 0, "", AlbumSort.Year, false).page().map { it.name }, ) assertEquals( listOf("Gamma", "Beta", "Alpha"), - dao.albumsPaging(albumsDefaultQuery(sourceId, 0, "", AlbumSort.Artist, false)).page().map { it.name }, + dao.albumPages(sourceId, 0, "", AlbumSort.Artist, false).page().map { it.name }, ) assertEquals( listOf("Alpha"), - dao.albumsPaging(albumsDefaultQuery(sourceId, 0, "alp", AlbumSort.Name, false)).page().map { it.name }, + dao.albumPages(sourceId, 0, "alp", AlbumSort.Name, false).page().map { it.name }, ) assertEquals( listOf("Gamma"), - dao.albumsPaging(albumsDefaultQuery(sourceId, 0, "am", AlbumSort.Name, false)).page().map { it.name }, + dao.albumPages(sourceId, 0, "am", AlbumSort.Name, false).page().map { it.name }, ) assertEquals( listOf("Alpha"), - dao.albumsPaging(albumsDefaultQuery(sourceId, 1, "", AlbumSort.Name, false)).page().map { it.name }, + dao.albumPages(sourceId, 1, "", AlbumSort.Name, false).page().map { it.name }, ) assertEquals( listOf("Beta", "Gamma"), - dao.albumsPaging(albumsDefaultQuery(sourceId, 2, "", AlbumSort.Name, false)).page().map { it.name }, + dao.albumPages(sourceId, 2, "", AlbumSort.Name, false).page().map { it.name }, ) } @@ -1326,23 +1337,23 @@ class LibraryDaoTest { assertEquals( listOf("Alpha", "Beta"), - dao.artistsPaging(artistsDefaultQuery(sourceId, 0, "a", ArtistSort.Name, false)).page().map { it.name }, + dao.artistPages(sourceId, 0, "a", ArtistSort.Name, false).page().map { it.name }, ) assertEquals( listOf("Beta", "Alpha"), - dao.artistsPaging(artistsDefaultQuery(sourceId, 0, "", ArtistSort.AlbumCount, false)).page().map { it.name }, + dao.artistPages(sourceId, 0, "", ArtistSort.AlbumCount, false).page().map { it.name }, ) assertEquals( listOf("Alpha", "Beta"), - dao.playlistsPaging(playlistsDefaultQuery(sourceId, "a", PlaylistSort.Name, false)).page().map { it.name }, + dao.playlistPages(sourceId, "a", PlaylistSort.Name, false).page().map { it.name }, ) assertEquals( listOf("Alpha", "Beta"), - dao.playlistsPaging(playlistsDefaultQuery(sourceId, "", PlaylistSort.Added, false)).page().map { it.name }, + dao.playlistPages(sourceId, "", PlaylistSort.Added, false).page().map { it.name }, ) assertEquals( listOf("Beta", "Alpha"), - dao.playlistsPaging(playlistsDefaultQuery(sourceId, "", PlaylistSort.Updated, false)).page().map { it.name }, + dao.playlistPages(sourceId, "", PlaylistSort.Updated, false).page().map { it.name }, ) } @@ -1355,12 +1366,12 @@ class LibraryDaoTest { dao.upsertSongs(listOf(song(sourceId, "s1", "One", starred = null).copy(albumId = "a"))) val defaultInvalidated = AtomicBoolean(false) - val default = dao.albumsPaging(albumsDefaultQuery(sourceId, 0, "", AlbumSort.Name, false)) + val default = dao.albumPages(sourceId, 0, "", AlbumSort.Name, false) default.registerInvalidatedCallback { defaultInvalidated.set(true) } default.page() val downloadedInvalidated = AtomicBoolean(false) - val downloaded = dao.albumsByName(sourceId, 0, "", downloadedFilter = 1) + val downloaded = dao.albumPages(sourceId, 0, "", downloaded = true) downloaded.registerInvalidatedCallback { downloadedInvalidated.set(true) } downloaded.page() @@ -1395,27 +1406,27 @@ class LibraryDaoTest { assertEquals( listOf("宇多田ヒカル", "あいみょん"), - dao.artistsByName(sourceId, 0, "").page().map { it.name }, + dao.artistPages(sourceId, 0, "").page().map { it.name }, ) assertEquals( listOf("あいみょん", "宇多田ヒカル"), - dao.artistsByNameReversed(sourceId, 0, "").page().map { it.name }, + dao.artistPages(sourceId, 0, "", descending = true).page().map { it.name }, ) assertEquals( listOf("Zzz", "Aaa"), - dao.albumsByName(sourceId, 0, "").page().map { it.name }, + dao.albumPages(sourceId, 0, "").page().map { it.name }, ) assertEquals( listOf("Aaa", "Zzz"), - dao.albumsByNameReversed(sourceId, 0, "").page().map { it.name }, + dao.albumPages(sourceId, 0, "", descending = true).page().map { it.name }, ) assertEquals( listOf("Aaa", "Zzz"), - dao.albumsByArtist(sourceId, 0, "").page().map { it.name }, + dao.albumPages(sourceId, 0, "", sort = AlbumSort.Artist).page().map { it.name }, ) assertEquals( listOf("Zzz", "Aaa"), - dao.albumsByArtistReversed(sourceId, 0, "").page().map { it.name }, + dao.albumPages(sourceId, 0, "", sort = AlbumSort.Artist, descending = true).page().map { it.name }, ) } @@ -1445,40 +1456,48 @@ class LibraryDaoTest { assertEquals( listOf("宇多田ヒカル", "Zebra", "Beatles", "あいみょん"), - dao.artistsByName(sourceId, 0, "", sortMode = SortNameMode.SortName.name).page().map { it.name }, + dao.artistPages(sourceId, 0, "", sortNameMode = SortNameMode.SortName).page().map { it.name }, ) assertEquals( listOf("Beatles", "Zebra", "あいみょん", "宇多田ヒカル"), - dao.artistsByName(sourceId, 0, "", sortMode = SortNameMode.Name.name).page().map { it.name }, + dao.artistPages(sourceId, 0, "", sortNameMode = SortNameMode.Name).page().map { it.name }, ) assertEquals( listOf("宇多田ヒカル", "あいみょん", "Zebra", "Beatles"), - dao.artistsByNameReversed(sourceId, 0, "", sortMode = SortNameMode.Name.name).page().map { it.name }, + dao + .artistPages(sourceId, 0, "", descending = true, sortNameMode = SortNameMode.Name) + .page() + .map { it.name }, ) assertEquals( listOf("宇多田ヒカル", "Beatles", "Zebra", "あいみょん"), - dao.artistsByName(sourceId, 0, "", sortMode = SortNameMode.NonLatin.name).page().map { it.name }, + dao.artistPages(sourceId, 0, "", sortNameMode = SortNameMode.NonLatin).page().map { it.name }, ) assertEquals( listOf("Aaa", "Beatles", "Zzz"), - dao.albumsByName(sourceId, 0, "", sortMode = SortNameMode.Name.name).page().map { it.name }, + dao.albumPages(sourceId, 0, "", sortNameMode = SortNameMode.Name).page().map { it.name }, ) assertEquals( listOf("Aaa", "Zzz", "Beatles"), - dao.albumsByArtist(sourceId, 0, "", sortMode = SortNameMode.Name.name).page().map { it.name }, + dao + .albumPages(sourceId, 0, "", sort = AlbumSort.Artist, sortNameMode = SortNameMode.Name) + .page() + .map { it.name }, ) assertEquals( listOf("Beatles", "Zebra", "あいみょん", "宇多田ヒカル"), dao - .artistsPaging(artistsDefaultQuery(sourceId, 0, "", ArtistSort.Name, false, SortNameMode.Name)) - .page() + .artistsPaging( + artistsPageQuery(sourceId, 0, "", ArtistSort.Name, false, false, SortNameMode.Name), + ).page() .map { it.name }, ) assertEquals( listOf("Aaa", "Beatles", "Zzz"), dao - .albumsPaging(albumsDefaultQuery(sourceId, 0, "", AlbumSort.Name, false, SortNameMode.Name)) - .page() + .albumsPaging( + albumsPageQuery(sourceId, 0, "", AlbumSort.Name, false, false, SortNameMode.Name), + ).page() .map { it.name }, ) } @@ -1507,11 +1526,11 @@ class LibraryDaoTest { ), ) - assertEquals(listOf("宇多田ヒカル"), dao.artistsByName(sourceId, 0, "utada").page().map { it.name }) - assertEquals(listOf("宇多田ヒカル"), dao.artistsByName(sourceId, 0, "ut").page().map { it.name }) + assertEquals(listOf("宇多田ヒカル"), dao.artistPages(sourceId, 0, "utada").page().map { it.name }) + assertEquals(listOf("宇多田ヒカル"), dao.artistPages(sourceId, 0, "ut").page().map { it.name }) assertEquals(listOf("宇多田ヒカル"), dao.searchArtists(sourceId, "utada", 10).first().map { it.name }) - assertEquals(listOf("First Love"), dao.albumsByName(sourceId, 0, "first").page().map { it.name }) - assertEquals(listOf("First Love"), dao.albumsByName(sourceId, 0, "fi").page().map { it.name }) + assertEquals(listOf("First Love"), dao.albumPages(sourceId, 0, "first").page().map { it.name }) + assertEquals(listOf("First Love"), dao.albumPages(sourceId, 0, "fi").page().map { it.name }) assertEquals(listOf("First Love"), dao.searchAlbums(sourceId, "first", 10).first().map { it.name }) assertEquals(listOf("Automatic"), dao.searchSongs(sourceId, "utada", 10).first().map { it.song.title }) } diff --git a/app/src/test/kotlin/com/subtracks/data/db/LibraryPagingTestSupport.kt b/app/src/test/kotlin/com/subtracks/data/db/LibraryPagingTestSupport.kt new file mode 100644 index 00000000..e8be9c56 --- /dev/null +++ b/app/src/test/kotlin/com/subtracks/data/db/LibraryPagingTestSupport.kt @@ -0,0 +1,53 @@ +package com.subtracks.data.db + +import androidx.paging.PagingSource +import com.subtracks.data.model.Album +import com.subtracks.data.model.Artist +import com.subtracks.data.model.Playlist +import com.subtracks.data.prefs.AlbumSort +import com.subtracks.data.prefs.ArtistSort +import com.subtracks.data.prefs.PlaylistSort +import com.subtracks.data.prefs.SortNameMode + +internal fun LibraryDao.albumPages( + sourceId: Long, + starredFilter: Int = 0, + search: String = "", + sort: AlbumSort = AlbumSort.Name, + descending: Boolean = false, + downloaded: Boolean = false, + sortNameMode: SortNameMode = SortNameMode.SortName, +): PagingSource = + if (downloaded) { + downloadedAlbumsPaging(albumsPageQuery(sourceId, starredFilter, search, sort, descending, true, sortNameMode)) + } else { + albumsPaging(albumsPageQuery(sourceId, starredFilter, search, sort, descending, false, sortNameMode)) + } + +internal fun LibraryDao.artistPages( + sourceId: Long, + starredFilter: Int = 0, + search: String = "", + sort: ArtistSort = ArtistSort.Name, + descending: Boolean = false, + downloaded: Boolean = false, + sortNameMode: SortNameMode = SortNameMode.SortName, +): PagingSource = + if (downloaded) { + downloadedArtistsPaging(artistsPageQuery(sourceId, starredFilter, search, sort, descending, true, sortNameMode)) + } else { + artistsPaging(artistsPageQuery(sourceId, starredFilter, search, sort, descending, false, sortNameMode)) + } + +internal fun LibraryDao.playlistPages( + sourceId: Long, + search: String = "", + sort: PlaylistSort = PlaylistSort.Name, + descending: Boolean = false, + downloaded: Boolean = false, +): PagingSource = + if (downloaded) { + downloadedPlaylistsPaging(playlistsPageQuery(sourceId, search, sort, descending, true)) + } else { + playlistsPaging(playlistsPageQuery(sourceId, search, sort, descending, false)) + } diff --git a/app/src/test/kotlin/com/subtracks/data/db/QueryTimingMeasurementTest.kt b/app/src/test/kotlin/com/subtracks/data/db/QueryTimingMeasurementTest.kt index c8b6a612..ac5b93eb 100644 --- a/app/src/test/kotlin/com/subtracks/data/db/QueryTimingMeasurementTest.kt +++ b/app/src/test/kotlin/com/subtracks/data/db/QueryTimingMeasurementTest.kt @@ -14,6 +14,9 @@ import com.subtracks.data.model.Playlist import com.subtracks.data.model.PlaylistSong import com.subtracks.data.model.Song import com.subtracks.data.model.Source +import com.subtracks.data.prefs.AlbumSort +import com.subtracks.data.prefs.ArtistSort +import com.subtracks.data.prefs.PlaylistSort import com.subtracks.data.source.MusicSource import com.subtracks.data.source.StarType import com.subtracks.data.sync.SyncService @@ -110,28 +113,31 @@ class QueryTimingMeasurementTest { private suspend fun measureReads() { val dao = db.libraryDao() - timed("albums first page (name)") { dao.albumsByName(1, 0, "").firstPage() } - timed("albums first page (artist)") { dao.albumsByArtist(1, 0, "").firstPage() } - timed("albums first page (year)") { dao.albumsByYear(1, 0, "").firstPage() } - timed("albums first page (added)") { dao.albumsByRecentlyAdded(1, 0, "").firstPage() } - timed("albums first page (starred)") { dao.albumsByStarred(1, 0, "").firstPage() } - timed("albums first page (frequent)") { dao.albumsByFrequent(1, 0, "").firstPage() } - timed("albums first page (recent)") { dao.albumsByRecent(1, 0, "").firstPage() } - timed("artists first page (name)") { dao.artistsByName(1, 0, "").firstPage() } - timed("artists first page (frequent)") { dao.artistsByFrequent(1, 0, "").firstPage() } + timed("albums first page (name)") { dao.albumPages(1, 0, "", sort = AlbumSort.Name, downloaded = true).firstPage() } + timed("albums first page (name, default)") { dao.albumPages(1, 0, "", sort = AlbumSort.Name).firstPage() } + timed("albums first page (artist)") { dao.albumPages(1, 0, "", sort = AlbumSort.Artist, downloaded = true).firstPage() } + timed("albums first page (year)") { dao.albumPages(1, 0, "", sort = AlbumSort.Year, downloaded = true).firstPage() } + timed("albums first page (added)") { dao.albumPages(1, 0, "", sort = AlbumSort.Added, downloaded = true).firstPage() } + timed("albums first page (starred)") { dao.albumPages(1, 0, "", sort = AlbumSort.Starred, downloaded = true).firstPage() } + timed("albums first page (frequent)") { dao.albumPages(1, 0, "", sort = AlbumSort.Frequent, downloaded = true).firstPage() } + timed("albums first page (recent)") { dao.albumPages(1, 0, "", sort = AlbumSort.Recent, downloaded = true).firstPage() } + timed("artists first page (name)") { dao.artistPages(1, 0, "", sort = ArtistSort.Name, downloaded = true).firstPage() } + timed("artists first page (name, default)") { dao.artistPages(1, 0, "", sort = ArtistSort.Name).firstPage() } + timed("artists first page (frequent)") { dao.artistPages(1, 0, "", sort = ArtistSort.Frequent, downloaded = true).firstPage() } timed("artist albums") { dao.albumsForArtist(1, "ar0").first() } timed("artist name") { dao.artistName(1, "ar0").first() } timed("artist songs") { db.queueDao().artistSongs(1, "ar0", 0, 100) } timed("artist length") { db.queueDao().artistLength(1, "ar0") } timed("artist song ids") { db.queueDao().artistSongIds(1, "ar0") } timed("downloaded artists") { dao.downloadedArtists(1).firstPage() } - timed("artists downloaded filter") { dao.artistsByName(1, 0, "", downloadedFilter = 1).firstPage() } + timed("artists downloaded filter") { dao.artistPages(1, 0, "", downloaded = true).firstPage() } timed("artist statuses") { db.downloadDao().artistStatuses(1).first() } timed("artist status") { db.downloadDao().artistStatus(1, "ar0").first() } - timed("playlists first page (name)") { dao.playlistsByName(1, "").firstPage() } - timed("albums deep page") { dao.albumsByName(1, 0, "").deepPage(1000) } - timed("albums search FTS") { dao.albumsByName(1, 0, "Album 1").firstPage() } - timed("albums search short") { dao.albumsByName(1, 0, "al").firstPage() } + timed("playlists first page (name)") { dao.playlistPages(1, "", sort = PlaylistSort.Name, downloaded = true).firstPage() } + timed("playlists first page (name, default)") { dao.playlistPages(1, "", sort = PlaylistSort.Name).firstPage() } + timed("albums deep page") { dao.albumPages(1, 0, "", sort = AlbumSort.Name, downloaded = true).deepPage(1000) } + timed("albums search FTS") { dao.albumPages(1, 0, "Album 1", sort = AlbumSort.Name, downloaded = true).firstPage() } + timed("albums search short") { dao.albumPages(1, 0, "al", sort = AlbumSort.Name, downloaded = true).firstPage() } timed("songs search FTS") { dao.searchSongs(1, "Song", 50).first() } timed("home recently played") { dao.recentlyPlayedAlbums(1, 20).first() } timed("home most played") { dao.mostPlayedAlbums(1, 20).first() } diff --git a/app/src/test/kotlin/com/subtracks/data/sync/SyncServiceTest.kt b/app/src/test/kotlin/com/subtracks/data/sync/SyncServiceTest.kt index 8444eecf..4d78a764 100644 --- a/app/src/test/kotlin/com/subtracks/data/sync/SyncServiceTest.kt +++ b/app/src/test/kotlin/com/subtracks/data/sync/SyncServiceTest.kt @@ -10,7 +10,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.subtracks.data.db.LoggingSQLiteDriver import com.subtracks.data.db.SqlExecution import com.subtracks.data.db.SubtracksDatabase -import com.subtracks.data.db.artistsDefaultQuery +import com.subtracks.data.db.artistPages +import com.subtracks.data.db.artistsPageQuery import com.subtracks.data.model.Album import com.subtracks.data.model.Artist import com.subtracks.data.model.ArtistRef @@ -75,7 +76,7 @@ class SyncServiceTest { 2, db .libraryDao() - .artistsByName(1, 0, "") + .artistPages(1, 0, "") .allRows() .size, ) @@ -98,7 +99,7 @@ class SyncServiceTest { listOf("a1"), db .libraryDao() - .artistsByName(1, 0, "") + .artistPages(1, 0, "") .allRows() .map { it.id }, ) @@ -172,7 +173,7 @@ class SyncServiceTest { "Old", db .libraryDao() - .artistsByName(1, 0, "") + .artistPages(1, 0, "") .allRows() .single() .name, @@ -185,7 +186,7 @@ class SyncServiceTest { "New", db .libraryDao() - .artistsByName(1, 0, "") + .artistPages(1, 0, "") .allRows() .single() .name, @@ -1107,16 +1108,16 @@ class SyncServiceTest { listOf("a1"), db .libraryDao() - .artistsByName(1, 0, "") + .artistPages(1, 0, "") .allRows() .map { it.id }, ) - val query = artistsDefaultQuery(1, 0, "alpha", ArtistSort.Name, false) + val query = artistsPageQuery(1, 0, "alpha", ArtistSort.Name, false, false) assertEquals( listOf("a1"), db .libraryDao() - .artistsByName(1, 0, "alpha") + .artistPages(1, 0, "alpha") .allRows() .map { it.id }, ) @@ -1203,7 +1204,7 @@ class SyncServiceTest { listOf("a1", "combo"), db .libraryDao() - .artistsByName(1, 0, "") + .artistPages(1, 0, "") .allRows() .map { it.id }, ) @@ -1216,7 +1217,7 @@ class SyncServiceTest { listOf("a1"), db .libraryDao() - .artistsByName(1, 0, "") + .artistPages(1, 0, "") .allRows() .map { it.id }, ) @@ -1434,7 +1435,7 @@ class SyncServiceTest { SyncService(db, source).sync() db.downloadDao().upsert(SongDownload(1, "s1", DownloadStatus.Completed)) - val filtered = db.libraryDao().artistsByName(1, 0, "", downloadedFilter = 1).allRows() + val filtered = db.libraryDao().artistPages(1, 0, "", downloaded = true).allRows() assertEquals(listOf("A", "B"), filtered.map { it.name }) assertEquals(listOf(1L, 1L), filtered.map { it.albumCount }) assertEquals(ListDownloadStatus(total = 1, downloaded = 1), db.downloadDao().artistStatus(1, "a2").first()) @@ -1538,7 +1539,7 @@ class SyncServiceTest { albums.size.toLong(), db .libraryDao() - .artistsByName(1, 0, "") + .artistPages(1, 0, "") .allRows() .first { it.id == "a2" } .albumCount, @@ -1780,7 +1781,7 @@ class SyncServiceTest { assertEquals("$artistId downloaded", downloaded.toLong(), status.downloaded) } - val downloadedArtists = db.libraryDao().artistsByName(1, 0, "", downloadedFilter = 1).allRows() + val downloadedArtists = db.libraryDao().artistPages(1, 0, "", downloaded = true).allRows() for (artist in downloadedArtists) { assertEquals( "${artist.id} downloaded albums",