mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 15:29:15 +01:00
playback: preserve null keys and ranged duplicate ranks
This commit is contained in:
@@ -210,7 +210,7 @@ data class Reanchored(
|
|||||||
)
|
)
|
||||||
|
|
||||||
private interface QueueKindResolver {
|
private interface QueueKindResolver {
|
||||||
val supportsKeyset: Boolean get() = false
|
fun supportsKeyset(key: OrderKey): Boolean = false
|
||||||
|
|
||||||
suspend fun length(
|
suspend fun length(
|
||||||
dao: com.subtracks.data.db.QueueDao,
|
dao: com.subtracks.data.db.QueueDao,
|
||||||
@@ -247,9 +247,9 @@ private interface QueueKindResolver {
|
|||||||
): Long? = null
|
): Long? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolverFor(kind: QueueKind): QueueKindResolver =
|
private val queueKindResolvers =
|
||||||
when (kind) {
|
mapOf<QueueKind, QueueKindResolver>(
|
||||||
QueueKind.Song -> {
|
QueueKind.Song to
|
||||||
object : QueueKindResolver {
|
object : QueueKindResolver {
|
||||||
override suspend fun length(
|
override suspend fun length(
|
||||||
dao: com.subtracks.data.db.QueueDao,
|
dao: com.subtracks.data.db.QueueDao,
|
||||||
@@ -268,12 +268,10 @@ private fun resolverFor(kind: QueueKind): QueueKindResolver =
|
|||||||
entry: QueueEntry,
|
entry: QueueEntry,
|
||||||
songId: String,
|
songId: String,
|
||||||
) = if (entry.refId == songId && length(dao, entry) > 0) 0L else null
|
) = if (entry.refId == songId && length(dao, entry) > 0) 0L else null
|
||||||
}
|
},
|
||||||
}
|
QueueKind.Playlist to
|
||||||
|
|
||||||
QueueKind.Playlist -> {
|
|
||||||
object : QueueKindResolver {
|
object : QueueKindResolver {
|
||||||
override val supportsKeyset = true
|
override fun supportsKeyset(key: OrderKey) = key.position != null
|
||||||
|
|
||||||
override suspend fun length(
|
override suspend fun length(
|
||||||
dao: com.subtracks.data.db.QueueDao,
|
dao: com.subtracks.data.db.QueueDao,
|
||||||
@@ -308,12 +306,10 @@ private fun resolverFor(kind: QueueKind): QueueKindResolver =
|
|||||||
entry: QueueEntry,
|
entry: QueueEntry,
|
||||||
songId: String,
|
songId: String,
|
||||||
) = dao.playlistRank(entry.sourceId, entry.refId, songId)
|
) = dao.playlistRank(entry.sourceId, entry.refId, songId)
|
||||||
}
|
},
|
||||||
}
|
QueueKind.Album to
|
||||||
|
|
||||||
QueueKind.Album -> {
|
|
||||||
object : QueueKindResolver {
|
object : QueueKindResolver {
|
||||||
override val supportsKeyset = true
|
override fun supportsKeyset(key: OrderKey) = key.disc != null && key.track != null
|
||||||
|
|
||||||
override suspend fun length(
|
override suspend fun length(
|
||||||
dao: com.subtracks.data.db.QueueDao,
|
dao: com.subtracks.data.db.QueueDao,
|
||||||
@@ -348,10 +344,8 @@ private fun resolverFor(kind: QueueKind): QueueKindResolver =
|
|||||||
entry: QueueEntry,
|
entry: QueueEntry,
|
||||||
songId: String,
|
songId: String,
|
||||||
) = dao.albumRank(entry.sourceId, entry.refId, songId)
|
) = dao.albumRank(entry.sourceId, entry.refId, songId)
|
||||||
}
|
},
|
||||||
}
|
QueueKind.Artist to
|
||||||
|
|
||||||
QueueKind.Artist -> {
|
|
||||||
object : QueueKindResolver {
|
object : QueueKindResolver {
|
||||||
override suspend fun length(
|
override suspend fun length(
|
||||||
dao: com.subtracks.data.db.QueueDao,
|
dao: com.subtracks.data.db.QueueDao,
|
||||||
@@ -364,10 +358,8 @@ private fun resolverFor(kind: QueueKind): QueueKindResolver =
|
|||||||
offset: Long,
|
offset: Long,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
) = dao.artistSongs(entry.sourceId, entry.refId, offset, limit)
|
) = dao.artistSongs(entry.sourceId, entry.refId, offset, limit)
|
||||||
}
|
},
|
||||||
}
|
QueueKind.Genre to
|
||||||
|
|
||||||
QueueKind.Genre -> {
|
|
||||||
object : QueueKindResolver {
|
object : QueueKindResolver {
|
||||||
override suspend fun length(
|
override suspend fun length(
|
||||||
dao: com.subtracks.data.db.QueueDao,
|
dao: com.subtracks.data.db.QueueDao,
|
||||||
@@ -380,10 +372,8 @@ private fun resolverFor(kind: QueueKind): QueueKindResolver =
|
|||||||
offset: Long,
|
offset: Long,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
) = dao.genreSongs(entry.sourceId, entry.refId, offset, limit)
|
) = dao.genreSongs(entry.sourceId, entry.refId, offset, limit)
|
||||||
}
|
},
|
||||||
}
|
QueueKind.Downloaded to
|
||||||
|
|
||||||
QueueKind.Downloaded -> {
|
|
||||||
object : QueueKindResolver {
|
object : QueueKindResolver {
|
||||||
override suspend fun length(
|
override suspend fun length(
|
||||||
dao: com.subtracks.data.db.QueueDao,
|
dao: com.subtracks.data.db.QueueDao,
|
||||||
@@ -396,10 +386,8 @@ private fun resolverFor(kind: QueueKind): QueueKindResolver =
|
|||||||
offset: Long,
|
offset: Long,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
) = dao.downloadedSongs(entry.sourceId, offset, limit)
|
) = dao.downloadedSongs(entry.sourceId, offset, limit)
|
||||||
}
|
},
|
||||||
}
|
QueueKind.Starred to
|
||||||
|
|
||||||
QueueKind.Starred -> {
|
|
||||||
object : QueueKindResolver {
|
object : QueueKindResolver {
|
||||||
override suspend fun length(
|
override suspend fun length(
|
||||||
dao: com.subtracks.data.db.QueueDao,
|
dao: com.subtracks.data.db.QueueDao,
|
||||||
@@ -412,9 +400,10 @@ private fun resolverFor(kind: QueueKind): QueueKindResolver =
|
|||||||
offset: Long,
|
offset: Long,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
) = dao.starredSongs(entry.sourceId, offset, limit)
|
) = dao.starredSongs(entry.sourceId, offset, limit)
|
||||||
}
|
},
|
||||||
}
|
)
|
||||||
}
|
|
||||||
|
private fun resolverFor(kind: QueueKind): QueueKindResolver = queueKindResolvers.getValue(kind)
|
||||||
|
|
||||||
class QueueRepository(
|
class QueueRepository(
|
||||||
private val db: SubtracksDatabase,
|
private val db: SubtracksDatabase,
|
||||||
@@ -858,10 +847,12 @@ class QueueRepository(
|
|||||||
var start = 0L
|
var start = 0L
|
||||||
for (resolved in entries) {
|
for (resolved in entries) {
|
||||||
val entry = resolved.entry
|
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) {
|
if (rank != null && rank >= entry.offset && rank < entry.offset + resolved.length) {
|
||||||
return start + rank - entry.offset
|
return start + rank - entry.offset
|
||||||
}
|
}
|
||||||
|
val fallback = fallbackRank(entry, resolved.length, songId)
|
||||||
|
if (fallback != null) return start + fallback - entry.offset
|
||||||
start += resolved.length
|
start += resolved.length
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
@@ -869,16 +860,20 @@ class QueueRepository(
|
|||||||
|
|
||||||
private suspend fun fallbackRank(
|
private suspend fun fallbackRank(
|
||||||
entry: QueueEntry,
|
entry: QueueEntry,
|
||||||
|
length: Long,
|
||||||
songId: String,
|
songId: String,
|
||||||
): Long? {
|
): Long? {
|
||||||
var offset = 0L
|
var offset = entry.offset
|
||||||
while (true) {
|
val end = entry.offset + length
|
||||||
val batch = rows(entry, offset, QUEUE_CHUNK)
|
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 }
|
val index = batch.indexOfFirst { it.song.id == songId }
|
||||||
if (index >= 0) return offset + index
|
if (index >= 0) return offset + index
|
||||||
if (batch.size < QUEUE_CHUNK) return null
|
if (batch.size < limit) return null
|
||||||
offset += batch.size
|
offset += batch.size
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun contextPlayRange(
|
private suspend fun contextPlayRange(
|
||||||
@@ -994,7 +989,7 @@ class QueueRepository(
|
|||||||
limit: Int,
|
limit: Int,
|
||||||
cursor: KeyCursor,
|
cursor: KeyCursor,
|
||||||
): List<SongItem>? {
|
): List<SongItem>? {
|
||||||
if (!resolverFor(entry.kind).supportsKeyset) return null
|
if (!resolverFor(entry.kind).supportsKeyset(cursor.key)) return null
|
||||||
val from = cursor.ordinal
|
val from = cursor.ordinal
|
||||||
// A backward seek from a far cursor is not cheaper than the offset query: the album
|
// 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
|
// predicate is not sargable (it has to include NULL disc/track), so it scans the partition
|
||||||
|
|||||||
@@ -150,6 +150,18 @@ class QueueKeysetTest {
|
|||||||
assertTrue("backward keyset seeks", repository.keysetSeeks > beforeBackward)
|
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
|
@Test
|
||||||
fun scanningAnAlbumWithNullDiscAndTrackBackwardsResolvesEveryRow() =
|
fun scanningAnAlbumWithNullDiscAndTrackBackwardsResolvesEveryRow() =
|
||||||
runTest {
|
runTest {
|
||||||
|
|||||||
@@ -895,6 +895,24 @@ class QueueRepositoryTest {
|
|||||||
assertEquals(1L, repository.combinedContextIndexOf(snapshot, "s1"))
|
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
|
@Test
|
||||||
fun contextIndexFallsBackToBoundedRowsForGenre() =
|
fun contextIndexFallsBackToBoundedRowsForGenre() =
|
||||||
runTest {
|
runTest {
|
||||||
|
|||||||
Reference in New Issue
Block a user