mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
Compare commits
10
Commits
29935b7bee
...
446a35d26e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
446a35d26e | ||
|
|
b18e0891f6 | ||
|
|
c53f1aef06 | ||
|
|
87e0609c05 | ||
|
|
c3a5382c27 | ||
|
|
e8cbb93714 | ||
|
|
6f1e05d15a | ||
|
|
c165a6cc88 | ||
|
|
18c24ca566 | ||
|
|
8d6f7e2341 |
@@ -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,6 @@ 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_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"
|
||||
@@ -67,13 +59,7 @@ internal const val ALBUM_ORDER_BY_RECENT_REVERSED = "played ASC NULLS LAST, sort
|
||||
|
||||
private const val ALBUM_NAME_SORT_KEY_MODE =
|
||||
"(CASE :sortMode WHEN 'Name' THEN name WHEN 'NonLatin' THEN (CASE WHEN name GLOB '*[A-Za-z]*' THEN name ELSE sortName END) ELSE sortName END) COLLATE NOCASE"
|
||||
private const val ALBUM_ARTIST_SORT_KEY_MODE =
|
||||
"(CASE :sortMode WHEN 'Name' THEN albumArtist WHEN 'NonLatin' THEN (CASE WHEN albumArtist GLOB '*[A-Za-z]*' THEN albumArtist ELSE albumArtistSort END) ELSE albumArtistSort END) COLLATE NOCASE"
|
||||
internal const val ALBUM_ORDER_BY_NAME_SORT_MODE = "$ALBUM_NAME_SORT_KEY_MODE, id"
|
||||
internal const val ALBUM_ORDER_BY_NAME_SORT_MODE_REVERSED = "$ALBUM_NAME_SORT_KEY_MODE DESC, id DESC"
|
||||
internal const val ALBUM_ORDER_BY_ARTIST_SORT_MODE = "$ALBUM_ARTIST_SORT_KEY_MODE, year, $ALBUM_NAME_SORT_KEY_MODE, id"
|
||||
internal const val ALBUM_ORDER_BY_ARTIST_SORT_MODE_REVERSED =
|
||||
"$ALBUM_ARTIST_SORT_KEY_MODE DESC, year DESC, $ALBUM_NAME_SORT_KEY_MODE DESC, id DESC"
|
||||
|
||||
internal const val ARTIST_CREDITED =
|
||||
"(artists.credited = 1 OR COALESCE((SELECT creditsStale FROM sources WHERE id = artists.sourceId), 1) = 1)"
|
||||
@@ -154,7 +140,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 +286,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"
|
||||
@@ -340,33 +306,11 @@ internal const val ARTIST_ORDER_BY_RECENT_REVERSED = "artists.played ASC NULLS L
|
||||
private const val ARTIST_NAME_SORT_KEY_MODE =
|
||||
"(CASE :sortMode WHEN 'Name' THEN name WHEN 'NonLatin' THEN (CASE WHEN name GLOB '*[A-Za-z]*' THEN name ELSE sortName END) ELSE sortName END) COLLATE NOCASE"
|
||||
internal const val ARTIST_ORDER_BY_NAME_SORT_MODE = "$ARTIST_NAME_SORT_KEY_MODE, id"
|
||||
internal const val ARTIST_ORDER_BY_NAME_SORT_MODE_REVERSED = "$ARTIST_NAME_SORT_KEY_MODE DESC, id DESC"
|
||||
|
||||
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 +748,27 @@ interface LibraryDao {
|
||||
@RawQuery(observedEntities = [Playlist::class])
|
||||
fun playlistsPaging(query: RoomRawQuery): PagingSource<Int, Playlist>
|
||||
|
||||
@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<Int, Album>
|
||||
// These observe every table the statements read; the sets match what Room inferred for the
|
||||
// old downloaded @Query methods, so download and library writes invalidate as before.
|
||||
@RawQuery(observedEntities = [Album::class, Song::class, SongDownload::class, AlbumSearch::class])
|
||||
fun downloadedAlbumsPaging(query: RoomRawQuery): PagingSource<Int, Album>
|
||||
|
||||
@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<Int, Album>
|
||||
@RawQuery(
|
||||
observedEntities = [
|
||||
Album::class,
|
||||
Song::class,
|
||||
SongDownload::class,
|
||||
AlbumArtistRef::class,
|
||||
SongArtistRef::class,
|
||||
Artist::class,
|
||||
Source::class,
|
||||
ArtistSearch::class,
|
||||
],
|
||||
)
|
||||
fun downloadedArtistsPaging(query: RoomRawQuery): PagingSource<Int, Artist>
|
||||
|
||||
@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<Int, Album>
|
||||
|
||||
@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<Int, Album>
|
||||
|
||||
@Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_YEAR")
|
||||
fun albumsByYear(
|
||||
sourceId: Long,
|
||||
starredFilter: Int,
|
||||
search: String,
|
||||
downloadedFilter: Int = 0,
|
||||
): PagingSource<Int, Album>
|
||||
|
||||
@Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_YEAR_REVERSED")
|
||||
fun albumsByYearReversed(
|
||||
sourceId: Long,
|
||||
starredFilter: Int,
|
||||
search: String,
|
||||
downloadedFilter: Int = 0,
|
||||
): PagingSource<Int, Album>
|
||||
|
||||
@Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_ADDED")
|
||||
fun albumsByRecentlyAdded(
|
||||
sourceId: Long,
|
||||
starredFilter: Int,
|
||||
search: String,
|
||||
downloadedFilter: Int = 0,
|
||||
): PagingSource<Int, Album>
|
||||
|
||||
@Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_ADDED_REVERSED")
|
||||
fun albumsByRecentlyAddedReversed(
|
||||
sourceId: Long,
|
||||
starredFilter: Int,
|
||||
search: String,
|
||||
downloadedFilter: Int = 0,
|
||||
): PagingSource<Int, Album>
|
||||
|
||||
@Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_STARRED")
|
||||
fun albumsByStarred(
|
||||
sourceId: Long,
|
||||
starredFilter: Int,
|
||||
search: String,
|
||||
downloadedFilter: Int = 0,
|
||||
): PagingSource<Int, Album>
|
||||
|
||||
@Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_STARRED_REVERSED")
|
||||
fun albumsByStarredReversed(
|
||||
sourceId: Long,
|
||||
starredFilter: Int,
|
||||
search: String,
|
||||
downloadedFilter: Int = 0,
|
||||
): PagingSource<Int, Album>
|
||||
|
||||
@Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_FREQUENT")
|
||||
fun albumsByFrequent(
|
||||
sourceId: Long,
|
||||
starredFilter: Int,
|
||||
search: String,
|
||||
downloadedFilter: Int = 0,
|
||||
): PagingSource<Int, Album>
|
||||
|
||||
@Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_FREQUENT_REVERSED")
|
||||
fun albumsByFrequentReversed(
|
||||
sourceId: Long,
|
||||
starredFilter: Int,
|
||||
search: String,
|
||||
downloadedFilter: Int = 0,
|
||||
): PagingSource<Int, Album>
|
||||
|
||||
@Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_RECENT")
|
||||
fun albumsByRecent(
|
||||
sourceId: Long,
|
||||
starredFilter: Int,
|
||||
search: String,
|
||||
downloadedFilter: Int = 0,
|
||||
): PagingSource<Int, Album>
|
||||
|
||||
@Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_RECENT_REVERSED")
|
||||
fun albumsByRecentReversed(
|
||||
sourceId: Long,
|
||||
starredFilter: Int,
|
||||
search: String,
|
||||
downloadedFilter: Int = 0,
|
||||
): PagingSource<Int, Album>
|
||||
|
||||
@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<Int, Artist>
|
||||
|
||||
@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<Int, Artist>
|
||||
|
||||
@Query("$ARTISTS_SELECT $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_ALBUM_COUNT")
|
||||
fun artistsByAlbumCount(
|
||||
sourceId: Long,
|
||||
starredFilter: Int,
|
||||
search: String,
|
||||
downloadedFilter: Int = 0,
|
||||
): PagingSource<Int, Artist>
|
||||
|
||||
@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<Int, Artist>
|
||||
|
||||
@Query("$ARTISTS_SELECT $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_STARRED")
|
||||
fun artistsByStarred(
|
||||
sourceId: Long,
|
||||
starredFilter: Int,
|
||||
search: String,
|
||||
downloadedFilter: Int = 0,
|
||||
): PagingSource<Int, Artist>
|
||||
|
||||
@Query("$ARTISTS_SELECT $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_STARRED_REVERSED")
|
||||
fun artistsByStarredReversed(
|
||||
sourceId: Long,
|
||||
starredFilter: Int,
|
||||
search: String,
|
||||
downloadedFilter: Int = 0,
|
||||
): PagingSource<Int, Artist>
|
||||
|
||||
@Query("$ARTISTS_SELECT $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_FREQUENT")
|
||||
fun artistsByFrequent(
|
||||
sourceId: Long,
|
||||
starredFilter: Int,
|
||||
search: String,
|
||||
downloadedFilter: Int = 0,
|
||||
): PagingSource<Int, Artist>
|
||||
|
||||
@Query("$ARTISTS_SELECT $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_FREQUENT_REVERSED")
|
||||
fun artistsByFrequentReversed(
|
||||
sourceId: Long,
|
||||
starredFilter: Int,
|
||||
search: String,
|
||||
downloadedFilter: Int = 0,
|
||||
): PagingSource<Int, Artist>
|
||||
|
||||
@Query("$ARTISTS_SELECT $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_RECENT")
|
||||
fun artistsByRecent(
|
||||
sourceId: Long,
|
||||
starredFilter: Int,
|
||||
search: String,
|
||||
downloadedFilter: Int = 0,
|
||||
): PagingSource<Int, Artist>
|
||||
|
||||
@Query("$ARTISTS_SELECT $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_RECENT_REVERSED")
|
||||
fun artistsByRecentReversed(
|
||||
sourceId: Long,
|
||||
starredFilter: Int,
|
||||
search: String,
|
||||
downloadedFilter: Int = 0,
|
||||
): PagingSource<Int, Artist>
|
||||
|
||||
@Query("$PLAYLISTS_SELECT $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_NAME")
|
||||
fun playlistsByName(
|
||||
sourceId: Long,
|
||||
search: String,
|
||||
downloadedFilter: Int = 0,
|
||||
): PagingSource<Int, Playlist>
|
||||
|
||||
@Query("$PLAYLISTS_SELECT $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_NAME_REVERSED")
|
||||
fun playlistsByNameReversed(
|
||||
sourceId: Long,
|
||||
search: String,
|
||||
downloadedFilter: Int = 0,
|
||||
): PagingSource<Int, Playlist>
|
||||
|
||||
@Query("$PLAYLISTS_SELECT $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_ADDED")
|
||||
fun playlistsByAdded(
|
||||
sourceId: Long,
|
||||
search: String,
|
||||
downloadedFilter: Int = 0,
|
||||
): PagingSource<Int, Playlist>
|
||||
|
||||
@Query("$PLAYLISTS_SELECT $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_ADDED_REVERSED")
|
||||
fun playlistsByAddedReversed(
|
||||
sourceId: Long,
|
||||
search: String,
|
||||
downloadedFilter: Int = 0,
|
||||
): PagingSource<Int, Playlist>
|
||||
|
||||
@Query("$PLAYLISTS_SELECT $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_UPDATED")
|
||||
fun playlistsByUpdated(
|
||||
sourceId: Long,
|
||||
search: String,
|
||||
downloadedFilter: Int = 0,
|
||||
): PagingSource<Int, Playlist>
|
||||
|
||||
@Query("$PLAYLISTS_SELECT $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_UPDATED_REVERSED")
|
||||
fun playlistsByUpdatedReversed(
|
||||
sourceId: Long,
|
||||
search: String,
|
||||
downloadedFilter: Int = 0,
|
||||
): PagingSource<Int, Playlist>
|
||||
@RawQuery(observedEntities = [PlaylistSong::class, SongDownload::class, Playlist::class, PlaylistSearch::class])
|
||||
fun downloadedPlaylistsPaging(query: RoomRawQuery): PagingSource<Int, Playlist>
|
||||
|
||||
@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<Int, Album>
|
||||
|
||||
@@ -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 intentionally omit song_downloads: the unfiltered lists do not need the
|
||||
// join, and their paging sources do not observe download writes (including progress updates).
|
||||
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 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 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)))
|
||||
"""
|
||||
|
||||
// The downloaded list counts 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,
|
||||
(SELECT COUNT(*) FROM ($DOWNLOADED_ARTIST_ALBUMS)) 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 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))
|
||||
"""
|
||||
|
||||
// The downloaded list counts 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,
|
||||
(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
|
||||
) 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,
|
||||
"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,
|
||||
"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,
|
||||
"search" to search,
|
||||
),
|
||||
)
|
||||
|
||||
internal fun rediscoverAlbumsQuery(
|
||||
sourceId: Long,
|
||||
@@ -57,70 +169,47 @@ 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 takes a positional bind function, but SQLite gives a repeated named parameter one
|
||||
// index, so the SQL can keep its names and only the first-appearance order needs binding.
|
||||
internal fun rawQuery(
|
||||
sql: String,
|
||||
bindings: Map<String, Any?>,
|
||||
): RoomRawQuery {
|
||||
val values =
|
||||
Regex(":(\\w+)")
|
||||
.findAll(sql)
|
||||
.map { it.groupValues[1] }
|
||||
.distinct()
|
||||
.map(bindings::getValue)
|
||||
.toList()
|
||||
return RoomRawQuery(sql) { statement ->
|
||||
values.forEachIndexed { index, value -> statement.bind(index + 1, value) }
|
||||
}
|
||||
}
|
||||
|
||||
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}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,17 @@ 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 = sourceId,
|
||||
starredFilter = starred.ordinal,
|
||||
search = search,
|
||||
sort = sort,
|
||||
descending = descending,
|
||||
downloaded = downloaded,
|
||||
sortNameMode = mode,
|
||||
)
|
||||
if (downloaded) dao.downloadedAlbumsPaging(query) else dao.albumsPaging(query)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,52 +220,17 @@ 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 = sourceId,
|
||||
starredFilter = starred.ordinal,
|
||||
search = search,
|
||||
sort = sort,
|
||||
descending = descending,
|
||||
downloaded = downloaded,
|
||||
sortNameMode = mode,
|
||||
)
|
||||
if (downloaded) dao.downloadedArtistsPaging(query) else dao.artistsPaging(query)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,36 +243,15 @@ class LibraryRepository(
|
||||
): Flow<PagingData<Playlist>> =
|
||||
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 = sourceId,
|
||||
search = search,
|
||||
sort = sort,
|
||||
descending = descending,
|
||||
downloaded = downloaded,
|
||||
)
|
||||
if (downloaded) dao.downloadedPlaylistsPaging(query) else dao.playlistsPaging(query)
|
||||
}
|
||||
|
||||
fun album(
|
||||
|
||||
@@ -61,7 +61,7 @@ class SyncService(
|
||||
source.artists().collect { batch ->
|
||||
if (batch.isEmpty()) return@collect
|
||||
changed =
|
||||
write("artists", artistColumns, primaryKey, batch, preserved = setOf("albumCount"), markStale = true) { it.values() } ||
|
||||
write("artists", primaryKey, batch, preserved = setOf("albumCount"), markStale = true) { it.columns() } ||
|
||||
changed
|
||||
batch.forEach { seen.add(idHash(it.id)) }
|
||||
artistsSynced += batch.size
|
||||
@@ -137,7 +137,7 @@ class SyncService(
|
||||
connection.immediateTransaction {
|
||||
val before = connection.totalChanges()
|
||||
val resolved = connection.resolveSongSortNames(batch)
|
||||
connection.upsertChanged("songs", songColumns, primaryKey, resolved) { it.values() }
|
||||
connection.upsertChanged("songs", primaryKey, resolved) { it.columns() }
|
||||
connection.usePrepared("DELETE FROM song_genres WHERE sourceId = ? AND songId = ? AND position >= ?") { statement ->
|
||||
batch.forEach { song ->
|
||||
statement.bind(1, song.sourceId)
|
||||
@@ -148,7 +148,7 @@ class SyncService(
|
||||
statement.clearBindings()
|
||||
}
|
||||
}
|
||||
connection.upsertChanged("song_genres", songGenreColumns, songGenreKey, genres) { it.values() }
|
||||
connection.upsertChanged("song_genres", songGenreKey, genres) { it.columns() }
|
||||
connection.usePrepared("DELETE FROM song_artists WHERE sourceId = ? AND songId = ? AND position >= ?") { statement ->
|
||||
batch.forEach { song ->
|
||||
statement.bind(1, song.sourceId)
|
||||
@@ -159,7 +159,7 @@ class SyncService(
|
||||
statement.clearBindings()
|
||||
}
|
||||
}
|
||||
connection.upsertChanged("song_artists", songArtistColumns, songArtistKey, artists) { it.values() }
|
||||
connection.upsertChanged("song_artists", songArtistKey, artists) { it.columns() }
|
||||
(connection.totalChanges() != before).also { changed ->
|
||||
if (changed) connection.markCreditsStale(batch.first().sourceId)
|
||||
}
|
||||
@@ -256,7 +256,7 @@ class SyncService(
|
||||
val seen = HashedIds()
|
||||
source.playlists().collect { batch ->
|
||||
if (batch.isEmpty()) return@collect
|
||||
write("playlists", playlistColumns, primaryKey, batch) { it.values() }
|
||||
write("playlists", primaryKey, batch) { it.columns() }
|
||||
batch.forEach { seen.add(idHash(it.id)) }
|
||||
playlistsSynced += batch.size
|
||||
}
|
||||
@@ -269,7 +269,7 @@ class SyncService(
|
||||
val counts = HashMap<String, Long>()
|
||||
source.playlistSongs(playlistIds).collect { batch ->
|
||||
if (batch.isEmpty()) return@collect
|
||||
write("playlist_songs", playlistSongColumns, playlistSongKey, batch) { it.values() }
|
||||
write("playlist_songs", playlistSongKey, batch) { it.columns() }
|
||||
batch.forEach { counts[it.playlistId] = (counts[it.playlistId] ?: 0L) + 1L }
|
||||
playlistSongsSynced += batch.size
|
||||
}
|
||||
@@ -280,17 +280,16 @@ class SyncService(
|
||||
// One commit per batch, so Room notifies paged observers once per batch rather than per row.
|
||||
private suspend fun <T> write(
|
||||
table: String,
|
||||
columns: List<String>,
|
||||
key: List<String>,
|
||||
rows: List<T>,
|
||||
preserved: Set<String> = emptySet(),
|
||||
markStale: Boolean = false,
|
||||
values: (T) -> List<Any?>,
|
||||
columns: (T) -> List<Pair<String, Any?>>,
|
||||
): Boolean =
|
||||
db.useWriterConnection { connection ->
|
||||
connection.immediateTransaction {
|
||||
val before = connection.totalChanges()
|
||||
connection.upsertChanged(table, columns, key, rows, preserved, values)
|
||||
connection.upsertChanged(table, key, rows, preserved, columns)
|
||||
(connection.totalChanges() != before).also { changed ->
|
||||
if (changed && markStale) connection.markCreditsStale(source.id)
|
||||
}
|
||||
@@ -309,8 +308,8 @@ class SyncService(
|
||||
connection.immediateTransaction {
|
||||
val before = connection.totalChanges()
|
||||
val resolved = connection.resolveAlbumSortNames(albums)
|
||||
connection.upsertChanged("albums", albumColumns, primaryKey, resolved) { it.values() }
|
||||
connection.upsertChanged("discs", discColumns, discKey, discs) { it.values() }
|
||||
connection.upsertChanged("albums", primaryKey, resolved) { it.columns() }
|
||||
connection.upsertChanged("discs", discKey, discs) { it.columns() }
|
||||
connection.usePrepared("DELETE FROM album_artists WHERE sourceId = ? AND albumId = ? AND position >= ?") { statement ->
|
||||
albums.forEach { album ->
|
||||
statement.bind(1, album.sourceId)
|
||||
@@ -321,7 +320,7 @@ class SyncService(
|
||||
statement.clearBindings()
|
||||
}
|
||||
}
|
||||
connection.upsertChanged("album_artists", albumArtistColumns, albumArtistKey, artists) { it.values() }
|
||||
connection.upsertChanged("album_artists", albumArtistKey, artists) { it.columns() }
|
||||
(connection.totalChanges() != before).also { changed ->
|
||||
if (changed) connection.markCreditsStale(albums.first().sourceId)
|
||||
}
|
||||
@@ -438,58 +437,8 @@ private fun discHash(
|
||||
|
||||
private val primaryKey = listOf("sourceId", "id")
|
||||
|
||||
private val artistColumns = listOf("sourceId", "id", "name", "albumCount", "starred", "coverArt", "artistImageUrl", "sortName")
|
||||
|
||||
private val albumColumns =
|
||||
listOf(
|
||||
"sourceId",
|
||||
"id",
|
||||
"artistId",
|
||||
"name",
|
||||
"albumArtist",
|
||||
"created",
|
||||
"coverArt",
|
||||
"genre",
|
||||
"year",
|
||||
"starred",
|
||||
"songCount",
|
||||
"artists",
|
||||
"sortName",
|
||||
"albumArtistSort",
|
||||
)
|
||||
|
||||
private val discKey = listOf("sourceId", "albumId", "disc")
|
||||
|
||||
private val discColumns = listOf("sourceId", "albumId", "disc", "title")
|
||||
|
||||
private val songColumns =
|
||||
listOf(
|
||||
"sourceId",
|
||||
"id",
|
||||
"albumId",
|
||||
"artistId",
|
||||
"title",
|
||||
"album",
|
||||
"artist",
|
||||
"duration",
|
||||
"track",
|
||||
"disc",
|
||||
"starred",
|
||||
"created",
|
||||
"playCount",
|
||||
"played",
|
||||
"trackGain",
|
||||
"albumGain",
|
||||
"trackPeak",
|
||||
"albumPeak",
|
||||
"artists",
|
||||
"sortName",
|
||||
"artistSort",
|
||||
)
|
||||
|
||||
private val playlistColumns =
|
||||
listOf("sourceId", "id", "name", "comment", "coverArt", "songCount", "created", "changed", "duration", "owner", "shared")
|
||||
|
||||
private fun Song.genreNames(): List<String> =
|
||||
genres
|
||||
.flatMap { it.genreParts() }
|
||||
@@ -500,71 +449,93 @@ private fun Song.genreNames(): List<String> =
|
||||
// Navidrome's default genre tag split (resources/mappings.yaml): semicolon, slash, comma.
|
||||
private fun String.genreParts(): List<String> = split(';', '/', ',').map { it.trim() }.filter { it.isNotEmpty() }
|
||||
|
||||
private val songGenreColumns = listOf("sourceId", "songId", "position", "genre")
|
||||
|
||||
private val songGenreKey = listOf("sourceId", "songId", "position")
|
||||
|
||||
private val albumArtistColumns = listOf("sourceId", "albumId", "position", "artistId", "name")
|
||||
|
||||
private val albumArtistKey = listOf("sourceId", "albumId", "position")
|
||||
|
||||
private val songArtistColumns = listOf("sourceId", "songId", "position", "artistId", "name")
|
||||
|
||||
private val songArtistKey = listOf("sourceId", "songId", "position")
|
||||
|
||||
private val playlistSongColumns = listOf("sourceId", "playlistId", "songId", "position")
|
||||
|
||||
private val playlistSongKey = listOf("sourceId", "playlistId", "position")
|
||||
|
||||
private fun Artist.values() = listOf<Any?>(sourceId, id, name, albumCount, starred, coverArt, artistImageUrl, sortName)
|
||||
|
||||
private fun Album.values() =
|
||||
listOf<Any?>(
|
||||
sourceId,
|
||||
id,
|
||||
artistId,
|
||||
name,
|
||||
albumArtist,
|
||||
created,
|
||||
coverArt,
|
||||
genre,
|
||||
year,
|
||||
starred,
|
||||
songCount,
|
||||
ArtistRefsCodec.encode(artistRefs),
|
||||
sortName,
|
||||
albumArtistSort,
|
||||
private fun Artist.columns(): List<Pair<String, Any?>> =
|
||||
listOf(
|
||||
"sourceId" to sourceId,
|
||||
"id" to id,
|
||||
"name" to name,
|
||||
"albumCount" to albumCount,
|
||||
"starred" to starred,
|
||||
"coverArt" to coverArt,
|
||||
"artistImageUrl" to artistImageUrl,
|
||||
"sortName" to sortName,
|
||||
)
|
||||
|
||||
private fun AlbumArtistRef.values() = listOf<Any?>(sourceId, albumId, position, artistId, name)
|
||||
private fun Album.columns(): List<Pair<String, Any?>> =
|
||||
listOf(
|
||||
"sourceId" to sourceId,
|
||||
"id" to id,
|
||||
"artistId" to artistId,
|
||||
"name" to name,
|
||||
"albumArtist" to albumArtist,
|
||||
"created" to created,
|
||||
"coverArt" to coverArt,
|
||||
"genre" to genre,
|
||||
"year" to year,
|
||||
"starred" to starred,
|
||||
"songCount" to songCount,
|
||||
"artists" to ArtistRefsCodec.encode(artistRefs),
|
||||
"sortName" to sortName,
|
||||
"albumArtistSort" to albumArtistSort,
|
||||
)
|
||||
|
||||
private fun SongArtistRef.values() = listOf<Any?>(sourceId, songId, position, artistId, name)
|
||||
private fun AlbumArtistRef.columns(): List<Pair<String, Any?>> =
|
||||
listOf(
|
||||
"sourceId" to sourceId,
|
||||
"albumId" to albumId,
|
||||
"position" to position,
|
||||
"artistId" to artistId,
|
||||
"name" to name,
|
||||
)
|
||||
|
||||
private fun Disc.values() = listOf<Any?>(sourceId, albumId, disc, title)
|
||||
private fun SongArtistRef.columns(): List<Pair<String, Any?>> =
|
||||
listOf(
|
||||
"sourceId" to sourceId,
|
||||
"songId" to songId,
|
||||
"position" to position,
|
||||
"artistId" to artistId,
|
||||
"name" to name,
|
||||
)
|
||||
|
||||
private fun Song.values() =
|
||||
listOf<Any?>(
|
||||
sourceId,
|
||||
id,
|
||||
albumId,
|
||||
artistId,
|
||||
title,
|
||||
album,
|
||||
artist,
|
||||
duration,
|
||||
track,
|
||||
disc,
|
||||
starred,
|
||||
created,
|
||||
playCount,
|
||||
played,
|
||||
trackGain,
|
||||
albumGain,
|
||||
trackPeak,
|
||||
albumPeak,
|
||||
ArtistRefsCodec.encode(artistRefs),
|
||||
sortName,
|
||||
artistSort,
|
||||
private fun Disc.columns(): List<Pair<String, Any?>> =
|
||||
listOf(
|
||||
"sourceId" to sourceId,
|
||||
"albumId" to albumId,
|
||||
"disc" to disc,
|
||||
"title" to title,
|
||||
)
|
||||
|
||||
private fun Song.columns(): List<Pair<String, Any?>> =
|
||||
listOf(
|
||||
"sourceId" to sourceId,
|
||||
"id" to id,
|
||||
"albumId" to albumId,
|
||||
"artistId" to artistId,
|
||||
"title" to title,
|
||||
"album" to album,
|
||||
"artist" to artist,
|
||||
"duration" to duration,
|
||||
"track" to track,
|
||||
"disc" to disc,
|
||||
"starred" to starred,
|
||||
"created" to created,
|
||||
"playCount" to playCount,
|
||||
"played" to played,
|
||||
"trackGain" to trackGain,
|
||||
"albumGain" to albumGain,
|
||||
"trackPeak" to trackPeak,
|
||||
"albumPeak" to albumPeak,
|
||||
"artists" to ArtistRefsCodec.encode(artistRefs),
|
||||
"sortName" to sortName,
|
||||
"artistSort" to artistSort,
|
||||
)
|
||||
|
||||
private fun List<ArtistRef>.positions(): List<Pair<Long, ArtistRef>> =
|
||||
@@ -576,32 +547,56 @@ private fun Album.artistPositions(): List<Pair<Long, ArtistRef>> = artistRefs.po
|
||||
|
||||
private fun Song.artistPositions(): List<Pair<Long, ArtistRef>> = artistRefs.positions()
|
||||
|
||||
private fun SongGenre.values() = listOf<Any?>(sourceId, songId, position, genre)
|
||||
private fun SongGenre.columns(): List<Pair<String, Any?>> =
|
||||
listOf(
|
||||
"sourceId" to sourceId,
|
||||
"songId" to songId,
|
||||
"position" to position,
|
||||
"genre" to genre,
|
||||
)
|
||||
|
||||
private fun Playlist.values() = listOf<Any?>(sourceId, id, name, comment, coverArt, songCount, created, changed, duration, owner, shared)
|
||||
private fun Playlist.columns(): List<Pair<String, Any?>> =
|
||||
listOf(
|
||||
"sourceId" to sourceId,
|
||||
"id" to id,
|
||||
"name" to name,
|
||||
"comment" to comment,
|
||||
"coverArt" to coverArt,
|
||||
"songCount" to songCount,
|
||||
"created" to created,
|
||||
"changed" to changed,
|
||||
"duration" to duration,
|
||||
"owner" to owner,
|
||||
"shared" to shared,
|
||||
)
|
||||
|
||||
private fun PlaylistSong.values() = listOf<Any?>(sourceId, playlistId, songId, position)
|
||||
private fun PlaylistSong.columns(): List<Pair<String, Any?>> =
|
||||
listOf(
|
||||
"sourceId" to sourceId,
|
||||
"playlistId" to playlistId,
|
||||
"songId" to songId,
|
||||
"position" to position,
|
||||
)
|
||||
|
||||
private suspend fun <T> PooledConnection.upsertChanged(
|
||||
table: String,
|
||||
columns: List<String>,
|
||||
key: List<String>,
|
||||
rows: List<T>,
|
||||
preserved: Set<String> = emptySet(),
|
||||
values: (T) -> List<Any?>,
|
||||
columns: (T) -> List<Pair<String, Any?>>,
|
||||
) {
|
||||
if (rows.isEmpty()) return
|
||||
check(values(rows.first()).size == columns.size) { "column/value mismatch for $table" }
|
||||
val updates = columns.filterNot { it in key || it in preserved }
|
||||
val columnNames = columns(rows.first()).map { it.first }
|
||||
val updates = columnNames.filterNot { it in key || it in preserved }
|
||||
val sql =
|
||||
"INSERT INTO $table (${columns.joinToString()}) VALUES (${columns.joinToString { "?" }}) " +
|
||||
"INSERT INTO $table (${columnNames.joinToString()}) VALUES (${columnNames.joinToString { "?" }}) " +
|
||||
"ON CONFLICT(${key.joinToString()}) DO UPDATE SET " +
|
||||
updates.joinToString { "$it = excluded.$it" } +
|
||||
" WHERE " +
|
||||
updates.joinToString(" OR ") { "$table.$it COLLATE BINARY IS NOT excluded.$it" }
|
||||
usePrepared(sql) { statement ->
|
||||
rows.forEach { row ->
|
||||
values(row).forEachIndexed { index, value -> statement.bind(index + 1, value) }
|
||||
columns(row).forEachIndexed { index, (_, value) -> statement.bind(index + 1, value) }
|
||||
statement.step()
|
||||
statement.reset()
|
||||
statement.clearBindings()
|
||||
|
||||
@@ -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,12 +389,49 @@ 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")
|
||||
val plan = plan("SELECT $ARTIST_COLUMNS $ARTISTS_FILTER ORDER BY $order LIMIT 20 OFFSET 0")
|
||||
assertTrue(plan, plan.contains("USING INDEX $index"))
|
||||
assertFalse(plan, plan.contains("TEMP B-TREE"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun downloadedArtistOrdersUseTheIndexesTheirSortColumnsHave() =
|
||||
runTest {
|
||||
val orders =
|
||||
listOf(
|
||||
"${artistNameSortKey(SortNameMode.SortName)}, id" to "index_artists_name",
|
||||
"${artistNameSortKey(SortNameMode.SortName)} DESC, id DESC" to "index_artists_name",
|
||||
"${artistNameSortKey(SortNameMode.Name)}, id" to "index_artists_name_plain",
|
||||
"${artistNameSortKey(SortNameMode.Name)} DESC, id DESC" to "index_artists_name_plain",
|
||||
ARTIST_ORDER_BY_ALBUM_COUNT to "index_artists_albumCount",
|
||||
ARTIST_ORDER_BY_ALBUM_COUNT_REVERSED to "index_artists_albumCount",
|
||||
ARTIST_ORDER_BY_STARRED to "index_artists_starred",
|
||||
ARTIST_ORDER_BY_STARRED_REVERSED to "index_artists_starred",
|
||||
ARTIST_ORDER_BY_FREQUENT to "index_artists_frequent",
|
||||
ARTIST_ORDER_BY_FREQUENT_REVERSED to "index_artists_frequent",
|
||||
ARTIST_ORDER_BY_RECENT to "index_artists_recent",
|
||||
ARTIST_ORDER_BY_RECENT_REVERSED to "index_artists_recent",
|
||||
)
|
||||
for ((order, index) in orders) {
|
||||
val plan =
|
||||
plan(
|
||||
"SELECT $ARTIST_COLUMNS_DOWNLOADED $ARTISTS_FILTER_DOWNLOADED " +
|
||||
"ORDER BY $order LIMIT 20 OFFSET 0",
|
||||
topLevelOnly = true,
|
||||
)
|
||||
if (order == ARTIST_ORDER_BY_ALBUM_COUNT || order == ARTIST_ORDER_BY_ALBUM_COUNT_REVERSED) {
|
||||
// The displayed count comes from song_downloads, so this order cannot use
|
||||
// index_artists_albumCount; the hand-written statement sorted in memory too.
|
||||
assertTrue("$order\n$plan", plan.contains("USING INDEX index_artists_sourceId"))
|
||||
assertTrue("$order\n$plan", plan.contains("USE TEMP B-TREE FOR ORDER BY"))
|
||||
} else {
|
||||
assertTrue("$order\n$plan", plan.contains("USING INDEX $index"))
|
||||
assertFalse("$order\n$plan", plan.contains("USE TEMP B-TREE FOR ORDER BY"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun playlistOrdersAreIndexBacked() =
|
||||
runTest {
|
||||
@@ -401,9 +445,18 @@ 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"))
|
||||
val defaultPlan = plan("SELECT * $PLAYLISTS_FILTER ORDER BY $order LIMIT 20 OFFSET 0")
|
||||
assertTrue(defaultPlan, defaultPlan.contains("USING INDEX $index"))
|
||||
assertFalse(defaultPlan, defaultPlan.contains("TEMP B-TREE"))
|
||||
|
||||
val downloadedPlan =
|
||||
plan(
|
||||
"SELECT $PLAYLISTS_COLUMNS_DOWNLOADED $PLAYLISTS_FILTER_DOWNLOADED " +
|
||||
"ORDER BY $order LIMIT 20 OFFSET 0",
|
||||
topLevelOnly = true,
|
||||
)
|
||||
assertTrue(downloadedPlan, downloadedPlan.contains("USING INDEX $index"))
|
||||
assertFalse(downloadedPlan, downloadedPlan.contains("USE TEMP B-TREE FOR ORDER BY"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,17 +509,85 @@ 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
|
||||
fun downloadedFiltersHonourSearchAndStarred() =
|
||||
runTest {
|
||||
val sourceId = source()
|
||||
val dao = db.libraryDao()
|
||||
dao.upsertArtists(
|
||||
listOf(
|
||||
artist(sourceId, "ar-1", "Artist", albumCount = 2).copy(starred = 5),
|
||||
artist(sourceId, "ar-2", "Other", albumCount = 1),
|
||||
),
|
||||
)
|
||||
dao.upsertAlbums(
|
||||
listOf(
|
||||
album(sourceId, "al-1", "Downloaded One", year = 2000, starred = 5),
|
||||
album(sourceId, "al-2", "Downloaded Two", year = 2001, starred = null),
|
||||
album(sourceId, "al-3", "Missing", year = 2002, starred = 5),
|
||||
),
|
||||
)
|
||||
dao.upsertSongs(
|
||||
listOf(
|
||||
song(sourceId, "s1", "One", starred = null),
|
||||
song(sourceId, "s2", "Two", starred = null).copy(albumId = "al-2"),
|
||||
song(sourceId, "s3", "Three", starred = null).copy(albumId = "al-3", artistId = "ar-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 One"),
|
||||
dao.albumPages(sourceId, 0, "one", downloaded = true).page().map { it.name },
|
||||
)
|
||||
assertTrue(dao.albumPages(sourceId, 0, "missing", downloaded = true).page().isEmpty())
|
||||
assertEquals(
|
||||
listOf("Downloaded One"),
|
||||
dao.albumPages(sourceId, 1, "", downloaded = true).page().map { it.name },
|
||||
)
|
||||
assertTrue(dao.albumPages(sourceId, 2, "", downloaded = true).page().isEmpty())
|
||||
|
||||
assertEquals(
|
||||
listOf("ar-1"),
|
||||
dao.artistPages(sourceId, 0, "art", downloaded = true).page().map { it.id },
|
||||
)
|
||||
assertTrue(dao.artistPages(sourceId, 0, "other", downloaded = true).page().isEmpty())
|
||||
assertEquals(
|
||||
listOf("ar-1"),
|
||||
dao.artistPages(sourceId, 1, "", downloaded = true).page().map { it.id },
|
||||
)
|
||||
assertTrue(dao.artistPages(sourceId, 2, "", downloaded = true).page().isEmpty())
|
||||
|
||||
assertEquals(
|
||||
listOf("pl-1"),
|
||||
dao.playlistPages(sourceId, "has", downloaded = true).page().map { it.id },
|
||||
)
|
||||
assertTrue(dao.playlistPages(sourceId, "no", downloaded = true).page().isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -628,7 +749,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 +769,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 +1025,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 +1034,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 +1082,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 +1127,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 +1157,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 +1166,7 @@ class LibraryDaoTest {
|
||||
assertEquals(
|
||||
5L,
|
||||
dao
|
||||
.playlistsByName(sourceId, "")
|
||||
.playlistPages(sourceId, "")
|
||||
.page()
|
||||
.first { it.id == "pl-1" }
|
||||
.songCount,
|
||||
@@ -1064,11 +1185,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 +1198,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 +1222,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 +1234,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 +1252,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 +1266,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
|
||||
@@ -1250,11 +1371,16 @@ class LibraryDaoTest {
|
||||
assertFalse(plan, plan.contains("SCAN songs"))
|
||||
}
|
||||
|
||||
private suspend fun plan(sql: String): String =
|
||||
private suspend fun plan(
|
||||
sql: String,
|
||||
topLevelOnly: Boolean = false,
|
||||
): String =
|
||||
db.useReaderConnection { connection ->
|
||||
connection.usePrepared("EXPLAIN QUERY PLAN $sql") { statement ->
|
||||
val details = mutableListOf<String>()
|
||||
while (statement.step()) details += statement.getText(3)
|
||||
while (statement.step()) {
|
||||
if (!topLevelOnly || statement.getLong(1) == 0L) details += statement.getText(3)
|
||||
}
|
||||
details.joinToString("\n")
|
||||
}
|
||||
}
|
||||
@@ -1274,35 +1400,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 +1452,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 +1481,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()
|
||||
|
||||
@@ -1373,6 +1499,52 @@ class LibraryDaoTest {
|
||||
awaitUntil("the default list observes album writes") { defaultInvalidated.get() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun downloadWritesInvalidateTheDownloadedArtistAndPlaylistPagingSources() =
|
||||
runTest {
|
||||
val sourceId = source()
|
||||
val dao = db.libraryDao()
|
||||
dao.upsertArtists(listOf(artist(sourceId, "ar-1", "Artist", albumCount = 1)))
|
||||
dao.upsertAlbums(listOf(album(sourceId, "al-1", "Album", year = null, starred = null)))
|
||||
dao.upsertSongs(listOf(song(sourceId, "s1", "One", starred = null)))
|
||||
dao.upsertPlaylists(listOf(playlist(sourceId, "pl-1", "Playlist", created = 0, changed = 0)))
|
||||
dao.upsertPlaylistSongs(listOf(PlaylistSong(sourceId, "pl-1", "s1", 0)))
|
||||
|
||||
val artistInvalidated = AtomicBoolean(false)
|
||||
val artists = dao.artistPages(sourceId, downloaded = true)
|
||||
artists.registerInvalidatedCallback { artistInvalidated.set(true) }
|
||||
artists.page()
|
||||
|
||||
val playlistInvalidated = AtomicBoolean(false)
|
||||
val playlists = dao.playlistPages(sourceId, downloaded = true)
|
||||
playlists.registerInvalidatedCallback { playlistInvalidated.set(true) }
|
||||
playlists.page()
|
||||
|
||||
db.downloadDao().upsert(SongDownload(sourceId, "s1", DownloadStatus.Completed))
|
||||
|
||||
awaitUntil("the downloaded artists observe download writes") { artistInvalidated.get() }
|
||||
awaitUntil("the downloaded playlists observe download writes") { playlistInvalidated.get() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun songWritesInvalidateTheDownloadedArtistPagingSource() =
|
||||
runTest {
|
||||
val sourceId = source()
|
||||
val dao = db.libraryDao()
|
||||
dao.upsertArtists(listOf(artist(sourceId, "ar-1", "Artist", albumCount = 1)))
|
||||
dao.upsertAlbums(listOf(album(sourceId, "al-1", "Album", year = null, starred = null)))
|
||||
dao.upsertSongs(listOf(song(sourceId, "s1", "One", starred = null)))
|
||||
|
||||
val invalidated = AtomicBoolean(false)
|
||||
val artists = dao.artistPages(sourceId, downloaded = true)
|
||||
artists.registerInvalidatedCallback { invalidated.set(true) }
|
||||
artists.page()
|
||||
|
||||
dao.upsertSongs(listOf(song(sourceId, "s2", "Two", starred = null)))
|
||||
|
||||
awaitUntil("the downloaded artists observe song writes") { invalidated.get() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sortNamesDriveArtistAndAlbumOrder() =
|
||||
runTest {
|
||||
@@ -1395,27 +1567,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,39 +1617,111 @@ 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))
|
||||
.albumsPaging(
|
||||
albumsPageQuery(sourceId, 0, "", AlbumSort.Name, false, false, SortNameMode.Name),
|
||||
).page()
|
||||
.map { it.name },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun downloadedSortsHonourTheSortNamePreference() =
|
||||
runTest {
|
||||
val sourceId = source()
|
||||
val dao = db.libraryDao()
|
||||
dao.upsertArtists(
|
||||
listOf(
|
||||
artist(sourceId, "ar-1", "Beatles", albumCount = 1).copy(sortName = "Beatles, The"),
|
||||
artist(sourceId, "ar-2", "Zebra", albumCount = 1).copy(sortName = "Aaa"),
|
||||
),
|
||||
)
|
||||
dao.upsertAlbums(
|
||||
listOf(
|
||||
album(sourceId, "al-1", "Beatles", year = null, starred = null)
|
||||
.copy(sortName = "Beatles, The", albumArtistSort = "Beatles, The", albumArtist = "Beatles"),
|
||||
album(sourceId, "al-2", "Zebra", year = null, starred = null)
|
||||
.copy(
|
||||
sortName = "Aaa",
|
||||
albumArtistSort = "Aaa",
|
||||
albumArtist = "Zebra",
|
||||
artistId = "ar-2",
|
||||
),
|
||||
),
|
||||
)
|
||||
dao.upsertSongs(
|
||||
listOf(
|
||||
song(sourceId, "s1", "One", starred = null).copy(albumId = "al-1"),
|
||||
song(sourceId, "s2", "Two", starred = null).copy(albumId = "al-2", artistId = "ar-2"),
|
||||
),
|
||||
)
|
||||
listOf("s1", "s2").forEach {
|
||||
db.downloadDao().upsert(SongDownload(sourceId, it, DownloadStatus.Completed))
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
listOf("Zebra", "Beatles"),
|
||||
dao
|
||||
.artistPages(sourceId, 0, "", downloaded = true, sortNameMode = SortNameMode.SortName)
|
||||
.page()
|
||||
.map { it.name },
|
||||
)
|
||||
assertEquals(
|
||||
listOf("Beatles", "Zebra"),
|
||||
dao
|
||||
.artistPages(sourceId, 0, "", downloaded = true, sortNameMode = SortNameMode.Name)
|
||||
.page()
|
||||
.map { it.name },
|
||||
)
|
||||
assertEquals(
|
||||
listOf("Zebra", "Beatles"),
|
||||
dao
|
||||
.albumPages(sourceId, 0, "", downloaded = true, sortNameMode = SortNameMode.SortName)
|
||||
.page()
|
||||
.map { it.name },
|
||||
)
|
||||
assertEquals(
|
||||
listOf("Beatles", "Zebra"),
|
||||
dao
|
||||
.albumPages(sourceId, 0, "", downloaded = true, sortNameMode = SortNameMode.Name)
|
||||
.page()
|
||||
.map { it.name },
|
||||
)
|
||||
@@ -1507,11 +1751,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 })
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
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<Int, Album> =
|
||||
if (downloaded) {
|
||||
downloadedAlbumsPaging(
|
||||
albumsPageQuery(
|
||||
sourceId = sourceId,
|
||||
starredFilter = starredFilter,
|
||||
search = search,
|
||||
sort = sort,
|
||||
descending = descending,
|
||||
downloaded = true,
|
||||
sortNameMode = sortNameMode,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
albumsPaging(
|
||||
albumsPageQuery(
|
||||
sourceId = sourceId,
|
||||
starredFilter = starredFilter,
|
||||
search = search,
|
||||
sort = sort,
|
||||
descending = descending,
|
||||
downloaded = false,
|
||||
sortNameMode = 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<Int, Artist> =
|
||||
if (downloaded) {
|
||||
downloadedArtistsPaging(
|
||||
artistsPageQuery(
|
||||
sourceId = sourceId,
|
||||
starredFilter = starredFilter,
|
||||
search = search,
|
||||
sort = sort,
|
||||
descending = descending,
|
||||
downloaded = true,
|
||||
sortNameMode = sortNameMode,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
artistsPaging(
|
||||
artistsPageQuery(
|
||||
sourceId = sourceId,
|
||||
starredFilter = starredFilter,
|
||||
search = search,
|
||||
sort = sort,
|
||||
descending = descending,
|
||||
downloaded = false,
|
||||
sortNameMode = sortNameMode,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun LibraryDao.playlistPages(
|
||||
sourceId: Long,
|
||||
search: String = "",
|
||||
sort: PlaylistSort = PlaylistSort.Name,
|
||||
descending: Boolean = false,
|
||||
downloaded: Boolean = false,
|
||||
): PagingSource<Int, Playlist> =
|
||||
if (downloaded) {
|
||||
downloadedPlaylistsPaging(
|
||||
playlistsPageQuery(
|
||||
sourceId = sourceId,
|
||||
search = search,
|
||||
sort = sort,
|
||||
descending = descending,
|
||||
downloaded = true,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
playlistsPaging(
|
||||
playlistsPageQuery(
|
||||
sourceId = sourceId,
|
||||
search = search,
|
||||
sort = sort,
|
||||
descending = descending,
|
||||
downloaded = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -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,37 @@ 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).firstPage() }
|
||||
timed("albums first page (name, downloaded)") {
|
||||
dao.albumPages(1, 0, "", sort = AlbumSort.Name, downloaded = true).firstPage()
|
||||
}
|
||||
timed("albums first page (artist)") { dao.albumPages(1, 0, "", sort = AlbumSort.Artist).firstPage() }
|
||||
timed("albums first page (year)") { dao.albumPages(1, 0, "", sort = AlbumSort.Year).firstPage() }
|
||||
timed("albums first page (added)") { dao.albumPages(1, 0, "", sort = AlbumSort.Added).firstPage() }
|
||||
timed("albums first page (starred)") { dao.albumPages(1, 0, "", sort = AlbumSort.Starred).firstPage() }
|
||||
timed("albums first page (frequent)") { dao.albumPages(1, 0, "", sort = AlbumSort.Frequent).firstPage() }
|
||||
timed("albums first page (recent)") { dao.albumPages(1, 0, "", sort = AlbumSort.Recent).firstPage() }
|
||||
timed("artists first page (name)") { dao.artistPages(1, 0, "", sort = ArtistSort.Name).firstPage() }
|
||||
timed("artists first page (name, downloaded)") {
|
||||
dao.artistPages(1, 0, "", sort = ArtistSort.Name, downloaded = true).firstPage()
|
||||
}
|
||||
timed("artists first page (frequent)") { dao.artistPages(1, 0, "", sort = ArtistSort.Frequent).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).firstPage() }
|
||||
timed("playlists first page (name, downloaded)") {
|
||||
dao.playlistPages(1, "", sort = PlaylistSort.Name, downloaded = true).firstPage()
|
||||
}
|
||||
timed("albums deep page") { dao.albumPages(1, 0, "", sort = AlbumSort.Name).deepPage(1000) }
|
||||
timed("albums search FTS") { dao.albumPages(1, 0, "Album 1", sort = AlbumSort.Name).firstPage() }
|
||||
timed("albums search short") { dao.albumPages(1, 0, "al", sort = AlbumSort.Name).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() }
|
||||
|
||||
@@ -208,6 +208,42 @@ class LibraryRepositoryTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theDownloadedAlbumGridOnlyPresentsDownloadedRows() =
|
||||
runBlocking {
|
||||
val sourceId = sourceRepository.addSource("nav", "http://localhost/", "u", "p", false)
|
||||
db.libraryDao().upsertAlbums(listOf(seedAlbum(sourceId, 1), seedAlbum(sourceId, 2)))
|
||||
db.libraryDao().upsertSongs(
|
||||
listOf(
|
||||
seedSong(sourceId).copy(id = "s1", albumId = "al-0001"),
|
||||
seedSong(sourceId).copy(id = "s2", albumId = "al-0002"),
|
||||
),
|
||||
)
|
||||
db.downloadDao().upsert(SongDownload(sourceId, "s1", DownloadStatus.Completed))
|
||||
|
||||
val presenterDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
val presenter = RecordingPresenter(presenterDispatcher)
|
||||
val job =
|
||||
launch {
|
||||
repository
|
||||
.albums(
|
||||
sourceId = sourceId,
|
||||
sort = AlbumSort.Name,
|
||||
descending = false,
|
||||
starred = StarredFilter.Any,
|
||||
search = "",
|
||||
downloaded = true,
|
||||
).collect { presenter.collectFrom(it) }
|
||||
}
|
||||
try {
|
||||
awaitRefreshes(presenter, 1)
|
||||
assertEquals(1, presented(presenter, presenterDispatcher))
|
||||
} finally {
|
||||
job.cancelAndJoin()
|
||||
presenterDispatcher.close()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun artistAlbumsSeparatesAlbumCreditsFromTrackAppearances() =
|
||||
runBlocking {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user