address third review: rebuild the window when the shuffle is dropped, atomic queue version

This commit is contained in:
austinried
2026-10-04 00:29:55 +00:00
parent f982293fd1
commit 59d20d4eda
3 changed files with 31 additions and 8 deletions
@@ -10,6 +10,7 @@ import com.subtracks.data.model.QueueEntry
import com.subtracks.data.model.QueueKind import com.subtracks.data.model.QueueKind
import com.subtracks.data.model.ShuffleOrder import com.subtracks.data.model.ShuffleOrder
import com.subtracks.data.model.SongListItem import com.subtracks.data.model.SongListItem
import java.util.concurrent.atomic.AtomicLong
const val QUEUE_CHUNK = 60 const val QUEUE_CHUNK = 60
@@ -62,8 +63,7 @@ class QueueRepository(
@Volatile @Volatile
private var cachedShuffleOrder: LongArray? = null private var cachedShuffleOrder: LongArray? = null
@Volatile private val queueVersion = AtomicLong()
private var queueVersion = 0L
@Volatile @Volatile
private var cachedFlatIds: List<String>? = null private var cachedFlatIds: List<String>? = null
@@ -98,6 +98,7 @@ class QueueRepository(
} }
suspend fun snapshot(): QueueSnapshot { suspend fun snapshot(): QueueSnapshot {
val version = queueVersion.get()
val loaded = val loaded =
db.useReaderConnection { transactor -> db.useReaderConnection { transactor ->
transactor.deferredTransaction { transactor.deferredTransaction {
@@ -110,7 +111,7 @@ class QueueRepository(
} }
val (entries, order, shuffled) = loaded val (entries, order, shuffled) = loaded
val valid = if (shuffled && order == null) null else order val valid = if (shuffled && order == null) null else order
return QueueSnapshot(entries, valid, queueVersion) return QueueSnapshot(entries, valid, version)
} }
suspend fun modes(): QueueModes { suspend fun modes(): QueueModes {
@@ -138,7 +139,7 @@ class QueueRepository(
fun invalidateLibraryCache() { fun invalidateLibraryCache() {
cachedFlatIds = null cachedFlatIds = null
queueVersion++ queueVersion.incrementAndGet()
} }
suspend fun setRepeat(mode: Int) = dao.setCursor(cursorRow().copy(repeatMode = mode)) suspend fun setRepeat(mode: Int) = dao.setCursor(cursorRow().copy(repeatMode = mode))
@@ -173,6 +174,15 @@ class QueueRepository(
return rows(entry, entry.offset + offset, 1).firstOrNull() return rows(entry, entry.offset + offset, 1).firstOrNull()
} }
suspend fun flatIndexOf(
snapshot: QueueSnapshot,
songId: String,
): Long? {
if (!snapshot.shuffled) return null
val index = flatIds(snapshot).indexOf(songId)
return if (index >= 0) index.toLong() else null
}
suspend fun range( suspend fun range(
snapshot: QueueSnapshot, snapshot: QueueSnapshot,
first: Long, first: Long,
@@ -271,7 +281,7 @@ class QueueRepository(
dao.insert(entries.mapIndexed { index, entry -> entry.copy(id = 0, position = index.toLong()) }) dao.insert(entries.mapIndexed { index, entry -> entry.copy(id = 0, position = index.toLong()) })
} }
} }
queueVersion++ queueVersion.incrementAndGet()
cachedFlatIds = null cachedFlatIds = null
} }
@@ -398,8 +398,10 @@ class PlaybackController(
fun previous() { fun previous() {
val player = player ?: return val player = player ?: return
if (player.currentPositionMs > RESTART_THRESHOLD_MS) { if (player.currentPositionMs > RESTART_THRESHOLD_MS) {
player.seekTo(0) scope.launch {
startLock.withLock { player.seekTo(0) }
refresh() refresh()
}
return return
} }
val snapshot = snapshot ?: return val snapshot = snapshot ?: return
@@ -657,9 +659,20 @@ class PlaybackController(
private suspend fun shiftWindowLocked(center: Long) { private suspend fun shiftWindowLocked(center: Long) {
val player = player ?: return val player = player ?: return
val previous = snapshot
val snapshot = readSnapshot() val snapshot = readSnapshot()
this.snapshot = snapshot this.snapshot = snapshot
if (snapshot.size == 0L) return if (snapshot.size == 0L) return
if (previous != null && previous.shuffled != snapshot.shuffled) {
val currentId = player.currentItem?.id
val position =
(currentId?.let { queueRepository.flatIndexOf(snapshot, it) } ?: center)
.coerceIn(0, snapshot.size - 1)
queueRepository.setCursor(position)
rebuildWindow(queueRepository.window(snapshot, position, QUEUE_WINDOW_RADIUS), position)
refresh(position)
return
}
val target = center.coerceIn(0, snapshot.size - 1) val target = center.coerceIn(0, snapshot.size - 1)
val desiredStart = (target - QUEUE_WINDOW_RADIUS).coerceAtLeast(0) val desiredStart = (target - QUEUE_WINDOW_RADIUS).coerceAtLeast(0)
val desiredEnd = (target + QUEUE_WINDOW_RADIUS).coerceAtMost(snapshot.size - 1) val desiredEnd = (target + QUEUE_WINDOW_RADIUS).coerceAtMost(snapshot.size - 1)
@@ -680,7 +680,7 @@ class PlaybackControllerTest {
handle.positionMs = 5_000L handle.positionMs = 5_000L
controller.previous() controller.previous()
assertTrue(handle.operations.contains("seekTo(0)")) await { handle.operations.contains("seekTo(0)") }
assertEquals( assertEquals(
"s2", "s2",
controller.state.value.item controller.state.value.item