mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 15:29:15 +01:00
address review: dedupe and trim genres, add a starred queue reference, fix the offline source switch
This commit is contained in:
@@ -801,6 +801,19 @@ interface LibraryDao {
|
|||||||
id: String,
|
id: String,
|
||||||
): Long
|
): Long
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"SELECT COUNT(*) FROM songs s " +
|
||||||
|
"WHERE s.sourceId = :sourceId AND s.starred IS NOT NULL " +
|
||||||
|
"AND (s.starred > (SELECT starred FROM songs WHERE sourceId = :sourceId AND id = :id) " +
|
||||||
|
"OR (s.starred = (SELECT starred FROM songs WHERE sourceId = :sourceId AND id = :id) " +
|
||||||
|
"AND (s.title COLLATE NOCASE < (SELECT title FROM songs WHERE sourceId = :sourceId AND id = :id) " +
|
||||||
|
"OR (s.title COLLATE NOCASE = (SELECT title FROM songs WHERE sourceId = :sourceId AND id = :id) AND s.id < :id))))",
|
||||||
|
)
|
||||||
|
suspend fun starredSongOrdinal(
|
||||||
|
sourceId: Long,
|
||||||
|
id: String,
|
||||||
|
): Long
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT * FROM albums WHERE sourceId = :sourceId AND year >= :start AND year < :end " +
|
"SELECT * FROM albums WHERE sourceId = :sourceId AND year >= :start AND year < :end " +
|
||||||
"ORDER BY year DESC, name COLLATE NOCASE, id",
|
"ORDER BY year DESC, name COLLATE NOCASE, id",
|
||||||
@@ -859,17 +872,6 @@ interface LibraryDao {
|
|||||||
songId: String,
|
songId: String,
|
||||||
): Flow<List<String>>
|
): Flow<List<String>>
|
||||||
|
|
||||||
@Query(
|
|
||||||
"SELECT DISTINCT song_genres.genre FROM song_genres " +
|
|
||||||
"JOIN songs ON songs.sourceId = song_genres.sourceId AND songs.id = song_genres.songId " +
|
|
||||||
"WHERE songs.sourceId = :sourceId AND songs.albumId = :albumId " +
|
|
||||||
"ORDER BY song_genres.genre COLLATE NOCASE",
|
|
||||||
)
|
|
||||||
fun albumGenres(
|
|
||||||
sourceId: Long,
|
|
||||||
albumId: String,
|
|
||||||
): Flow<List<String>>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM albums WHERE sourceId = :sourceId AND artistId = :artistId ORDER BY year DESC, name COLLATE NOCASE, id")
|
@Query("SELECT * FROM albums WHERE sourceId = :sourceId AND artistId = :artistId ORDER BY year DESC, name COLLATE NOCASE, id")
|
||||||
fun albumsForArtist(
|
fun albumsForArtist(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
|
|||||||
@@ -344,8 +344,8 @@ val MIGRATION_23_24 =
|
|||||||
override suspend fun migrate(connection: SQLiteConnection) {
|
override suspend fun migrate(connection: SQLiteConnection) {
|
||||||
connection.execSQL(
|
connection.execSQL(
|
||||||
"INSERT INTO `song_genres` (`sourceId`, `songId`, `position`, `genre`) " +
|
"INSERT INTO `song_genres` (`sourceId`, `songId`, `position`, `genre`) " +
|
||||||
"SELECT `sourceId`, `id`, 0, `genre` FROM `songs` " +
|
"SELECT `sourceId`, `id`, 0, trim(`genre`) FROM `songs` " +
|
||||||
"WHERE `genre` IS NOT NULL AND `genre` != '' " +
|
"WHERE `genre` IS NOT NULL AND trim(`genre`) != '' " +
|
||||||
"AND NOT EXISTS (SELECT 1 FROM `song_genres` sg " +
|
"AND NOT EXISTS (SELECT 1 FROM `song_genres` sg " +
|
||||||
"WHERE sg.`sourceId` = `songs`.`sourceId` AND sg.`songId` = `songs`.`id`)",
|
"WHERE sg.`sourceId` = `songs`.`sourceId` AND sg.`songId` = `songs`.`id`)",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -254,6 +254,27 @@ interface QueueDao {
|
|||||||
@Query("SELECT COUNT(*) FROM song_downloads WHERE sourceId = :sourceId AND status = 'Completed'")
|
@Query("SELECT COUNT(*) FROM song_downloads WHERE sourceId = :sourceId AND status = 'Completed'")
|
||||||
suspend fun downloadedLength(sourceId: Long): Long
|
suspend fun downloadedLength(sourceId: Long): Long
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) FROM songs WHERE sourceId = :sourceId AND starred IS NOT NULL")
|
||||||
|
suspend fun starredLength(sourceId: Long): Long
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
|
||||||
|
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
|
||||||
|
"WHERE songs.sourceId = :sourceId AND songs.starred IS NOT NULL " +
|
||||||
|
"ORDER BY songs.starred DESC, songs.title COLLATE NOCASE, songs.id LIMIT :limit OFFSET :offset",
|
||||||
|
)
|
||||||
|
suspend fun starredSongs(
|
||||||
|
sourceId: Long,
|
||||||
|
offset: Long,
|
||||||
|
limit: Int,
|
||||||
|
): List<AlbumSongItem>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"SELECT songs.id FROM songs WHERE sourceId = :sourceId AND starred IS NOT NULL " +
|
||||||
|
"ORDER BY starred DESC, title COLLATE NOCASE, id",
|
||||||
|
)
|
||||||
|
suspend fun starredSongIds(sourceId: Long): List<String>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT songs.*, albums.coverArt AS coverArt FROM song_downloads d " +
|
"SELECT songs.*, albums.coverArt AS coverArt FROM song_downloads d " +
|
||||||
"JOIN songs ON songs.sourceId = d.sourceId AND songs.id = d.songId " +
|
"JOIN songs ON songs.sourceId = d.sourceId AND songs.id = d.songId " +
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ data class DiscKey(
|
|||||||
val disc: Long,
|
val disc: Long,
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class QueueKind { Playlist, Album, Song, Artist, Genre, Downloaded }
|
enum class QueueKind { Playlist, Album, Song, Artist, Genre, Downloaded, Starred }
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "queue_entries",
|
tableName = "queue_entries",
|
||||||
|
|||||||
@@ -88,12 +88,8 @@ class LibraryRepository(
|
|||||||
|
|
||||||
fun starredSongs(sourceId: Long): Flow<PagingData<AlbumSongItem>> = pager(60) { db.libraryDao().starredSongs(sourceId) }
|
fun starredSongs(sourceId: Long): Flow<PagingData<AlbumSongItem>> = pager(60) { db.libraryDao().starredSongs(sourceId) }
|
||||||
|
|
||||||
suspend fun starredSongIds(sourceId: Long): List<String> = db.libraryDao().starredSongIds(sourceId)
|
|
||||||
|
|
||||||
fun downloadedSongs(sourceId: Long): Flow<List<AlbumSongItem>> = db.libraryDao().downloadedSongs(sourceId)
|
fun downloadedSongs(sourceId: Long): Flow<List<AlbumSongItem>> = db.libraryDao().downloadedSongs(sourceId)
|
||||||
|
|
||||||
suspend fun downloadedSongIds(sourceId: Long): List<String> = db.libraryDao().downloadedSongIds(sourceId)
|
|
||||||
|
|
||||||
fun downloadedSongsPage(sourceId: Long): Flow<PagingData<AlbumSongItem>> = pager(60) { db.libraryDao().homeDownloadedSongs(sourceId) }
|
fun downloadedSongsPage(sourceId: Long): Flow<PagingData<AlbumSongItem>> = pager(60) { db.libraryDao().homeDownloadedSongs(sourceId) }
|
||||||
|
|
||||||
fun downloadedAlbumsPage(sourceId: Long): Flow<PagingData<Album>> = pager(40) { db.libraryDao().downloadedAlbums(sourceId) }
|
fun downloadedAlbumsPage(sourceId: Long): Flow<PagingData<Album>> = pager(40) { db.libraryDao().downloadedAlbums(sourceId) }
|
||||||
@@ -105,11 +101,6 @@ class LibraryRepository(
|
|||||||
genre: String,
|
genre: String,
|
||||||
): Flow<PagingData<AlbumSongItem>> = pager(60) { db.libraryDao().songsByGenre(sourceId, genre) }
|
): Flow<PagingData<AlbumSongItem>> = pager(60) { db.libraryDao().songsByGenre(sourceId, genre) }
|
||||||
|
|
||||||
suspend fun genreSongIds(
|
|
||||||
sourceId: Long,
|
|
||||||
genre: String,
|
|
||||||
): List<String> = db.libraryDao().songsByGenreIds(sourceId, genre)
|
|
||||||
|
|
||||||
suspend fun genreSongOrdinal(
|
suspend fun genreSongOrdinal(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
genre: String,
|
genre: String,
|
||||||
@@ -122,6 +113,11 @@ class LibraryRepository(
|
|||||||
id: String,
|
id: String,
|
||||||
): Long = db.libraryDao().downloadedSongOrdinal(sourceId, id)
|
): Long = db.libraryDao().downloadedSongOrdinal(sourceId, id)
|
||||||
|
|
||||||
|
suspend fun starredSongOrdinal(
|
||||||
|
sourceId: Long,
|
||||||
|
id: String,
|
||||||
|
): Long = db.libraryDao().starredSongOrdinal(sourceId, id)
|
||||||
|
|
||||||
fun albumsByDecade(
|
fun albumsByDecade(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
decade: Long,
|
decade: Long,
|
||||||
|
|||||||
@@ -262,6 +262,8 @@ class QueueRepository(
|
|||||||
|
|
||||||
fun downloadedEntry(sourceId: Long) = QueueEntry(position = 0, sourceId = sourceId, kind = QueueKind.Downloaded, refId = "")
|
fun downloadedEntry(sourceId: Long) = QueueEntry(position = 0, sourceId = sourceId, kind = QueueKind.Downloaded, refId = "")
|
||||||
|
|
||||||
|
fun starredEntry(sourceId: Long) = QueueEntry(position = 0, sourceId = sourceId, kind = QueueKind.Starred, refId = "")
|
||||||
|
|
||||||
suspend fun sourceName(
|
suspend fun sourceName(
|
||||||
kind: QueueKind,
|
kind: QueueKind,
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
@@ -310,6 +312,10 @@ class QueueRepository(
|
|||||||
QueueKind.Downloaded -> {
|
QueueKind.Downloaded -> {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueueKind.Starred -> {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -663,6 +669,7 @@ class QueueRepository(
|
|||||||
QueueKind.Artist -> dao.artistSongIds(ref.sourceId, ref.refId)
|
QueueKind.Artist -> dao.artistSongIds(ref.sourceId, ref.refId)
|
||||||
QueueKind.Genre -> dao.genreSongIds(ref.sourceId, ref.refId)
|
QueueKind.Genre -> dao.genreSongIds(ref.sourceId, ref.refId)
|
||||||
QueueKind.Downloaded -> dao.downloadedSongIds(ref.sourceId)
|
QueueKind.Downloaded -> dao.downloadedSongIds(ref.sourceId)
|
||||||
|
QueueKind.Starred -> dao.starredSongIds(ref.sourceId)
|
||||||
}
|
}
|
||||||
val from = ref.offset.toInt().coerceIn(0, entryIds.size)
|
val from = ref.offset.toInt().coerceIn(0, entryIds.size)
|
||||||
val to = (ref.offset + entry.length).toInt().coerceIn(from, entryIds.size)
|
val to = (ref.offset + entry.length).toInt().coerceIn(from, entryIds.size)
|
||||||
@@ -765,6 +772,7 @@ class QueueRepository(
|
|||||||
QueueKind.Artist -> dao.artistSongs(entry.sourceId, entry.refId, offset, limit)
|
QueueKind.Artist -> dao.artistSongs(entry.sourceId, entry.refId, offset, limit)
|
||||||
QueueKind.Genre -> dao.genreSongs(entry.sourceId, entry.refId, offset, limit)
|
QueueKind.Genre -> dao.genreSongs(entry.sourceId, entry.refId, offset, limit)
|
||||||
QueueKind.Downloaded -> dao.downloadedSongs(entry.sourceId, offset, limit)
|
QueueKind.Downloaded -> dao.downloadedSongs(entry.sourceId, offset, limit)
|
||||||
|
QueueKind.Starred -> dao.starredSongs(entry.sourceId, offset, limit)
|
||||||
QueueKind.Song -> emptyList()
|
QueueKind.Song -> emptyList()
|
||||||
}
|
}
|
||||||
if (fresh.isNotEmpty()) remember(ref, offset, fresh)
|
if (fresh.isNotEmpty()) remember(ref, offset, fresh)
|
||||||
@@ -790,6 +798,7 @@ class QueueRepository(
|
|||||||
QueueKind.Artist -> OrderKey(row.song.id)
|
QueueKind.Artist -> OrderKey(row.song.id)
|
||||||
QueueKind.Genre -> OrderKey(row.song.id)
|
QueueKind.Genre -> OrderKey(row.song.id)
|
||||||
QueueKind.Downloaded -> OrderKey(row.song.id)
|
QueueKind.Downloaded -> OrderKey(row.song.id)
|
||||||
|
QueueKind.Starred -> OrderKey(row.song.id)
|
||||||
QueueKind.Song -> OrderKey(row.song.id)
|
QueueKind.Song -> OrderKey(row.song.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -832,6 +841,7 @@ class QueueRepository(
|
|||||||
QueueKind.Artist -> emptyList()
|
QueueKind.Artist -> emptyList()
|
||||||
QueueKind.Genre -> emptyList()
|
QueueKind.Genre -> emptyList()
|
||||||
QueueKind.Downloaded -> emptyList()
|
QueueKind.Downloaded -> emptyList()
|
||||||
|
QueueKind.Starred -> emptyList()
|
||||||
QueueKind.Song -> emptyList()
|
QueueKind.Song -> emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -847,10 +857,10 @@ class QueueRepository(
|
|||||||
QueueKind.Artist -> emptyList()
|
QueueKind.Artist -> emptyList()
|
||||||
QueueKind.Genre -> emptyList()
|
QueueKind.Genre -> emptyList()
|
||||||
QueueKind.Downloaded -> emptyList()
|
QueueKind.Downloaded -> emptyList()
|
||||||
|
QueueKind.Starred -> emptyList()
|
||||||
QueueKind.Song -> emptyList()
|
QueueKind.Song -> emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Artist songs span albums, so there is no single keyset to seek on; artist pages use OFFSET.
|
|
||||||
private fun OrderKey.seekable(kind: QueueKind): Boolean =
|
private fun OrderKey.seekable(kind: QueueKind): Boolean =
|
||||||
when (kind) {
|
when (kind) {
|
||||||
QueueKind.Playlist -> position != null
|
QueueKind.Playlist -> position != null
|
||||||
@@ -858,6 +868,7 @@ class QueueRepository(
|
|||||||
QueueKind.Artist -> false
|
QueueKind.Artist -> false
|
||||||
QueueKind.Genre -> false
|
QueueKind.Genre -> false
|
||||||
QueueKind.Downloaded -> false
|
QueueKind.Downloaded -> false
|
||||||
|
QueueKind.Starred -> false
|
||||||
QueueKind.Song -> false
|
QueueKind.Song -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -909,6 +920,10 @@ class QueueRepository(
|
|||||||
QueueKind.Downloaded -> {
|
QueueKind.Downloaded -> {
|
||||||
dao.downloadedLength(sourceId)
|
dao.downloadedLength(sourceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueueKind.Starred -> {
|
||||||
|
dao.starredLength(sourceId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val available = (total - offset).coerceAtLeast(0)
|
val available = (total - offset).coerceAtLeast(0)
|
||||||
return count?.coerceAtMost(available) ?: available
|
return count?.coerceAtMost(available) ?: available
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ private val songColumns =
|
|||||||
|
|
||||||
private val playlistColumns = listOf("sourceId", "id", "name", "comment", "coverArt", "songCount", "created", "changed", "duration")
|
private val playlistColumns = listOf("sourceId", "id", "name", "comment", "coverArt", "songCount", "created", "changed", "duration")
|
||||||
|
|
||||||
private fun Song.genreNames(): List<String> = genres.ifEmpty { listOfNotNull(genre?.trim()?.takeIf { it.isNotEmpty() }) }
|
private fun Song.genreNames(): List<String> = genres.distinct().ifEmpty { listOfNotNull(genre?.trim()?.takeIf { it.isNotEmpty() }) }
|
||||||
|
|
||||||
private val songGenreColumns = listOf("sourceId", "songId", "position", "genre")
|
private val songGenreColumns = listOf("sourceId", "songId", "position", "genre")
|
||||||
|
|
||||||
|
|||||||
@@ -210,16 +210,6 @@ class PlaybackController(
|
|||||||
songId: String,
|
songId: String,
|
||||||
) = playContext(queueRepository.songEntry(sourceId, songId), 0)
|
) = playContext(queueRepository.songEntry(sourceId, songId), 0)
|
||||||
|
|
||||||
fun playSongs(
|
|
||||||
sourceId: Long,
|
|
||||||
songIds: List<String>,
|
|
||||||
startIndex: Int,
|
|
||||||
) {
|
|
||||||
if (songIds.isEmpty()) return
|
|
||||||
val entries = songIds.map { id -> QueueEntry(position = 0, sourceId = sourceId, kind = QueueKind.Song, refId = id) }
|
|
||||||
play(entries, startIndex.toLong().coerceIn(0L, songIds.lastIndex.toLong()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun playPlaylist(
|
fun playPlaylist(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
playlistId: String,
|
playlistId: String,
|
||||||
@@ -237,6 +227,11 @@ class PlaybackController(
|
|||||||
startOrdinal: Long,
|
startOrdinal: Long,
|
||||||
) = playContext(queueRepository.downloadedEntry(sourceId), startOrdinal)
|
) = playContext(queueRepository.downloadedEntry(sourceId), startOrdinal)
|
||||||
|
|
||||||
|
fun playStarred(
|
||||||
|
sourceId: Long,
|
||||||
|
startOrdinal: Long,
|
||||||
|
) = playContext(queueRepository.starredEntry(sourceId), startOrdinal)
|
||||||
|
|
||||||
fun playAlbumInOrder(
|
fun playAlbumInOrder(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
albumId: String,
|
albumId: String,
|
||||||
@@ -1250,6 +1245,12 @@ class PlaybackController(
|
|||||||
loadWindow(position, autoplay = player.playWhenReady, startPositionMs = player.currentPositionMs)
|
loadWindow(position, autoplay = player.playWhenReady, startPositionMs = player.currentPositionMs)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// A track downloaded while it was streaming still holds a stream URL; fix the current item's
|
||||||
|
// source in place (without resetting the window) so an offline switch can play the local file.
|
||||||
|
val current = ordered[index]
|
||||||
|
val wantsLocal = downloads.localUri(current.item.song.id) != null
|
||||||
|
val isLocal = player.itemUris().getOrNull(player.currentIndex)?.startsWith(FILE_SCHEME) == true
|
||||||
|
if (wantsLocal != isLocal) player.replaceItem(player.currentIndex, current.item.toQueueItem())
|
||||||
windowPositions = LongArray(ordered.size) { ordered[it].position }
|
windowPositions = LongArray(ordered.size) { ordered[it].position }
|
||||||
windowFiltered = offline
|
windowFiltered = offline
|
||||||
windowStart = ordered.first().position
|
windowStart = ordered.first().position
|
||||||
|
|||||||
@@ -206,8 +206,8 @@ internal fun CoroutineScope.playStarredList(
|
|||||||
song: Song,
|
song: Song,
|
||||||
) {
|
) {
|
||||||
launch {
|
launch {
|
||||||
val ids = libraryRepository.starredSongIds(song.sourceId)
|
val ordinal = libraryRepository.starredSongOrdinal(song.sourceId, song.id)
|
||||||
playbackController.playSongs(song.sourceId, ids, ids.indexOf(song.id).coerceAtLeast(0))
|
playbackController.playStarred(song.sourceId, ordinal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -406,7 +406,11 @@ fun LibraryScreen(
|
|||||||
if (pagerState.currentPage != selectedTab.ordinal) {
|
if (pagerState.currentPage != selectedTab.ordinal) {
|
||||||
pagerState.animateScrollToPage(selectedTab.ordinal)
|
pagerState.animateScrollToPage(selectedTab.ordinal)
|
||||||
}
|
}
|
||||||
if (search.isNotEmpty()) searchActive = true
|
if (selectedTab == LibraryTab.Home) {
|
||||||
|
searchActive = false
|
||||||
|
} else if (search.isNotEmpty()) {
|
||||||
|
searchActive = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dismissSearch() {
|
fun dismissSearch() {
|
||||||
|
|||||||
@@ -472,6 +472,7 @@ private fun QueueKind.label(): String =
|
|||||||
QueueKind.Artist -> "artist"
|
QueueKind.Artist -> "artist"
|
||||||
QueueKind.Genre -> "genre"
|
QueueKind.Genre -> "genre"
|
||||||
QueueKind.Downloaded -> "downloads"
|
QueueKind.Downloaded -> "downloads"
|
||||||
|
QueueKind.Starred -> "starred"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatTime(milliseconds: Long): String {
|
private fun formatTime(milliseconds: Long): String {
|
||||||
|
|||||||
@@ -217,6 +217,27 @@ class LibraryHomeDaoTest {
|
|||||||
assertEquals(page.map { it.song.id }, dao.songsByGenreIds(sourceId, "Rock"))
|
assertEquals(page.map { it.song.id }, dao.songsByGenreIds(sourceId, "Rock"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun genreSongOrdinalMatchesTheListOrder() =
|
||||||
|
runTest {
|
||||||
|
val sourceId = source()
|
||||||
|
val dao = db.libraryDao()
|
||||||
|
dao.upsertSongs(
|
||||||
|
listOf(
|
||||||
|
song(sourceId, "s1", "Alpha"),
|
||||||
|
song(sourceId, "s2", "Bravo"),
|
||||||
|
song(sourceId, "s3", "Charlie"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
insertGenre(sourceId, "s1", 0, "Rock")
|
||||||
|
insertGenre(sourceId, "s2", 0, "Rock")
|
||||||
|
insertGenre(sourceId, "s3", 0, "Rock")
|
||||||
|
|
||||||
|
assertEquals(0L, dao.genreSongOrdinal(sourceId, "Rock", "Alpha", "s1"))
|
||||||
|
assertEquals(1L, dao.genreSongOrdinal(sourceId, "Rock", "Bravo", "s2"))
|
||||||
|
assertEquals(2L, dao.genreSongOrdinal(sourceId, "Rock", "Charlie", "s3"))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun downloadedSongsOrderByNewestDownload() =
|
fun downloadedSongsOrderByNewestDownload() =
|
||||||
runTest {
|
runTest {
|
||||||
|
|||||||
@@ -134,6 +134,25 @@ class MigrationsTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun theGenreBackfillTrimsAndSkipsBlankSingularGenres() =
|
||||||
|
runTest {
|
||||||
|
driver.open(":memory:").use { migrated ->
|
||||||
|
applySchema(migrated, versions.first())
|
||||||
|
MIGRATIONS.filter { it.endVersion <= 23 }.sortedBy { it.startVersion }.forEach { it.migrate(migrated) }
|
||||||
|
migrated.execSQL(
|
||||||
|
"INSERT INTO sources (id, name, address, isActive, createdAt) VALUES (1, 's', 'http://x', 1, 0)",
|
||||||
|
)
|
||||||
|
migrated.execSQL("INSERT INTO songs (sourceId, id, title, genre) VALUES (1, 's1', 'Song', ' Rock ')")
|
||||||
|
migrated.execSQL("INSERT INTO songs (sourceId, id, title, genre) VALUES (1, 's2', 'Blank', ' ')")
|
||||||
|
|
||||||
|
MIGRATIONS.first { it.endVersion == 24 }.migrate(migrated)
|
||||||
|
|
||||||
|
assertEquals("Rock", text(migrated, "SELECT genre FROM song_genres WHERE songId = 's1'"))
|
||||||
|
assertEquals(0L, long(migrated, "SELECT COUNT(*) FROM song_genres WHERE songId = 's2'"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun text(
|
private fun text(
|
||||||
connection: SQLiteConnection,
|
connection: SQLiteConnection,
|
||||||
sql: String,
|
sql: String,
|
||||||
|
|||||||
@@ -87,6 +87,24 @@ class QueueRepositoryTest {
|
|||||||
assertEquals(listOf("s1", "s3"), resolveAll(snapshot))
|
assertEquals(listOf("s1", "s3"), resolveAll(snapshot))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun starredEntryResolvesStarredTracksNewestFirst() =
|
||||||
|
runTest {
|
||||||
|
seedLibrary()
|
||||||
|
db.libraryDao().upsertSongs(
|
||||||
|
listOf(
|
||||||
|
song("s1", "al1", track = 1, starred = 100),
|
||||||
|
song("s2", "al1", track = 2, starred = 300),
|
||||||
|
song("s3", "al1", track = 3, starred = 200),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val snapshot = repository.snapshotAfter(listOf(repository.starredEntry(1)))
|
||||||
|
|
||||||
|
assertEquals(3, snapshot.size)
|
||||||
|
assertEquals(listOf("s2", "s3", "s1"), resolveAll(snapshot))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun rangeLimitsAnEntryToASlice() =
|
fun rangeLimitsAnEntryToASlice() =
|
||||||
runTest {
|
runTest {
|
||||||
|
|||||||
@@ -326,7 +326,7 @@ class SyncServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun albumGenresUnionsSongGenresAndFallsBackToTheSingleValue() =
|
fun syncNormalizesSingularGenresIntoTheGenreRows() =
|
||||||
runTest {
|
runTest {
|
||||||
insertSource()
|
insertSource()
|
||||||
val source =
|
val source =
|
||||||
@@ -342,7 +342,24 @@ class SyncServiceTest {
|
|||||||
|
|
||||||
SyncService(db, source).sync()
|
SyncService(db, source).sync()
|
||||||
|
|
||||||
assertEquals(listOf("Jazz", "Pop", "Rock"), db.libraryDao().albumGenres(1, "al1").first())
|
assertEquals(listOf("Rock", "Pop"), db.libraryDao().songGenres(1, "s1").first())
|
||||||
|
assertEquals(listOf("Jazz"), db.libraryDao().songGenres(1, "s2").first())
|
||||||
|
assertEquals(listOf("Rock"), db.libraryDao().songGenres(1, "s3").first())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun thePluralGenresWinOverTheSingularAttribute() =
|
||||||
|
runTest {
|
||||||
|
insertSource()
|
||||||
|
val source =
|
||||||
|
FakeMusicSource(
|
||||||
|
albums = listOf(album("al1")),
|
||||||
|
songs = listOf(song("s1").copy(genres = listOf("Rock", "Pop"), genre = "Jazz")),
|
||||||
|
)
|
||||||
|
|
||||||
|
SyncService(db, source).sync()
|
||||||
|
|
||||||
|
assertEquals(listOf("Rock", "Pop"), db.libraryDao().songGenres(1, "s1").first())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -2537,6 +2537,7 @@ class PlaybackControllerTest {
|
|||||||
|
|
||||||
await { handle.operations.any { it.startsWith("add") } }
|
await { handle.operations.any { it.startsWith("add") } }
|
||||||
assertFalse(handle.operations.any { it.startsWith("setWindow") })
|
assertFalse(handle.operations.any { it.startsWith("setWindow") })
|
||||||
|
assertFalse(handle.operations.any { it == "prepare" || it == "play" || it == "pause" })
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"s5",
|
"s5",
|
||||||
controller.state.value.item
|
controller.state.value.item
|
||||||
@@ -2804,6 +2805,7 @@ class PlaybackControllerTest {
|
|||||||
|
|
||||||
await { handle.operations.any { it.startsWith("remove") || it.startsWith("add") } }
|
await { handle.operations.any { it.startsWith("remove") || it.startsWith("add") } }
|
||||||
assertFalse(handle.operations.any { it.startsWith("setWindow") })
|
assertFalse(handle.operations.any { it.startsWith("setWindow") })
|
||||||
|
assertFalse(handle.operations.any { it == "prepare" || it == "play" || it == "pause" })
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"s2",
|
"s2",
|
||||||
controller.state.value.item
|
controller.state.value.item
|
||||||
|
|||||||
Reference in New Issue
Block a user