diff --git a/app/src/main/kotlin/com/subtracks/data/repo/QueueRepository.kt b/app/src/main/kotlin/com/subtracks/data/repo/QueueRepository.kt index f6ef7bb5..1febde46 100644 --- a/app/src/main/kotlin/com/subtracks/data/repo/QueueRepository.kt +++ b/app/src/main/kotlin/com/subtracks/data/repo/QueueRepository.kt @@ -210,7 +210,7 @@ data class Reanchored( ) private interface QueueKindResolver { - val supportsKeyset: Boolean get() = false + fun supportsKeyset(key: OrderKey): Boolean = false suspend fun length( dao: com.subtracks.data.db.QueueDao, @@ -247,9 +247,9 @@ private interface QueueKindResolver { ): Long? = null } -private fun resolverFor(kind: QueueKind): QueueKindResolver = - when (kind) { - QueueKind.Song -> { +private val queueKindResolvers = + mapOf( + QueueKind.Song to object : QueueKindResolver { override suspend fun length( dao: com.subtracks.data.db.QueueDao, @@ -268,12 +268,10 @@ private fun resolverFor(kind: QueueKind): QueueKindResolver = entry: QueueEntry, songId: String, ) = if (entry.refId == songId && length(dao, entry) > 0) 0L else null - } - } - - QueueKind.Playlist -> { + }, + QueueKind.Playlist to object : QueueKindResolver { - override val supportsKeyset = true + override fun supportsKeyset(key: OrderKey) = key.position != null override suspend fun length( dao: com.subtracks.data.db.QueueDao, @@ -308,12 +306,10 @@ private fun resolverFor(kind: QueueKind): QueueKindResolver = entry: QueueEntry, songId: String, ) = dao.playlistRank(entry.sourceId, entry.refId, songId) - } - } - - QueueKind.Album -> { + }, + QueueKind.Album to object : QueueKindResolver { - override val supportsKeyset = true + override fun supportsKeyset(key: OrderKey) = key.disc != null && key.track != null override suspend fun length( dao: com.subtracks.data.db.QueueDao, @@ -348,10 +344,8 @@ private fun resolverFor(kind: QueueKind): QueueKindResolver = entry: QueueEntry, songId: String, ) = dao.albumRank(entry.sourceId, entry.refId, songId) - } - } - - QueueKind.Artist -> { + }, + QueueKind.Artist to object : QueueKindResolver { override suspend fun length( dao: com.subtracks.data.db.QueueDao, @@ -364,10 +358,8 @@ private fun resolverFor(kind: QueueKind): QueueKindResolver = offset: Long, limit: Int, ) = dao.artistSongs(entry.sourceId, entry.refId, offset, limit) - } - } - - QueueKind.Genre -> { + }, + QueueKind.Genre to object : QueueKindResolver { override suspend fun length( dao: com.subtracks.data.db.QueueDao, @@ -380,10 +372,8 @@ private fun resolverFor(kind: QueueKind): QueueKindResolver = offset: Long, limit: Int, ) = dao.genreSongs(entry.sourceId, entry.refId, offset, limit) - } - } - - QueueKind.Downloaded -> { + }, + QueueKind.Downloaded to object : QueueKindResolver { override suspend fun length( dao: com.subtracks.data.db.QueueDao, @@ -396,10 +386,8 @@ private fun resolverFor(kind: QueueKind): QueueKindResolver = offset: Long, limit: Int, ) = dao.downloadedSongs(entry.sourceId, offset, limit) - } - } - - QueueKind.Starred -> { + }, + QueueKind.Starred to object : QueueKindResolver { override suspend fun length( dao: com.subtracks.data.db.QueueDao, @@ -412,9 +400,10 @@ private fun resolverFor(kind: QueueKind): QueueKindResolver = offset: Long, limit: Int, ) = dao.starredSongs(entry.sourceId, offset, limit) - } - } - } + }, + ) + +private fun resolverFor(kind: QueueKind): QueueKindResolver = queueKindResolvers.getValue(kind) class QueueRepository( private val db: SubtracksDatabase, @@ -858,10 +847,12 @@ class QueueRepository( var start = 0L for (resolved in entries) { val entry = resolved.entry - val rank = resolverFor(entry.kind).rank(dao, entry, songId) ?: fallbackRank(entry, songId) + val rank = resolverFor(entry.kind).rank(dao, entry, songId) if (rank != null && rank >= entry.offset && rank < entry.offset + resolved.length) { return start + rank - entry.offset } + val fallback = fallbackRank(entry, resolved.length, songId) + if (fallback != null) return start + fallback - entry.offset start += resolved.length } return null @@ -869,16 +860,20 @@ class QueueRepository( private suspend fun fallbackRank( entry: QueueEntry, + length: Long, songId: String, ): Long? { - var offset = 0L - while (true) { - val batch = rows(entry, offset, QUEUE_CHUNK) + var offset = entry.offset + val end = entry.offset + length + while (offset < end) { + val limit = minOf(QUEUE_CHUNK.toLong(), end - offset).toInt() + val batch = rows(entry, offset, limit) val index = batch.indexOfFirst { it.song.id == songId } if (index >= 0) return offset + index - if (batch.size < QUEUE_CHUNK) return null + if (batch.size < limit) return null offset += batch.size } + return null } private suspend fun contextPlayRange( @@ -994,7 +989,7 @@ class QueueRepository( limit: Int, cursor: KeyCursor, ): List? { - if (!resolverFor(entry.kind).supportsKeyset) return null + if (!resolverFor(entry.kind).supportsKeyset(cursor.key)) return null val from = cursor.ordinal // A backward seek from a far cursor is not cheaper than the offset query: the album // predicate is not sargable (it has to include NULL disc/track), so it scans the partition diff --git a/app/src/test/kotlin/com/subtracks/data/repo/QueueKeysetTest.kt b/app/src/test/kotlin/com/subtracks/data/repo/QueueKeysetTest.kt index f1b28a7f..d12a8ed8 100644 --- a/app/src/test/kotlin/com/subtracks/data/repo/QueueKeysetTest.kt +++ b/app/src/test/kotlin/com/subtracks/data/repo/QueueKeysetTest.kt @@ -150,6 +150,18 @@ class QueueKeysetTest { assertTrue("backward keyset seeks", repository.keysetSeeks > beforeBackward) } + @Test + fun scanningAnAlbumWithNullDiscAndTrackForwardFallsBackToOffsetRows() = + runTest { + seedLibrary() + repository.replace(listOf(repository.albumEntry(1, "mixed"))) + val snapshot = repository.snapshot() + + val resolved = (0 until snapshot.size).map { repository.itemAt(snapshot, it)?.song?.id } + + assertEquals(listOf("n1", "n2", "s1", "s2"), resolved) + } + @Test fun scanningAnAlbumWithNullDiscAndTrackBackwardsResolvesEveryRow() = runTest { diff --git a/app/src/test/kotlin/com/subtracks/data/repo/QueueRepositoryTest.kt b/app/src/test/kotlin/com/subtracks/data/repo/QueueRepositoryTest.kt index 82ce400f..89b4e236 100644 --- a/app/src/test/kotlin/com/subtracks/data/repo/QueueRepositoryTest.kt +++ b/app/src/test/kotlin/com/subtracks/data/repo/QueueRepositoryTest.kt @@ -895,6 +895,24 @@ class QueueRepositoryTest { assertEquals(1L, repository.combinedContextIndexOf(snapshot, "s1")) } + @Test + fun contextIndexFindsADuplicateInsideTheEntryRange() = + runTest { + seedLibrary() + db.libraryDao().upsertPlaylists(listOf(playlist("duplicates", "Duplicates"))) + db.libraryDao().upsertPlaylistSongs( + listOf( + PlaylistSong(1, "duplicates", "s2", 10), + PlaylistSong(1, "duplicates", "s1", 20), + PlaylistSong(1, "duplicates", "s2", 30), + ), + ) + val entry = repository.playlistEntry(1, "duplicates").copy(rangeStart = 1, rangeEnd = 2) + val snapshot = repository.snapshotAfter(listOf(entry)) + + assertEquals(1L, repository.combinedContextIndexOf(snapshot, "s2")) + } + @Test fun contextIndexFallsBackToBoundedRowsForGenre() = runTest {