fix repeating end of queue bug

This commit is contained in:
Boof2015
2026-07-24 02:26:36 -04:00
parent a10ecf4a0f
commit 031db907ae
4 changed files with 72 additions and 21 deletions
@@ -171,6 +171,15 @@ class RoomLibraryRepositoryTest {
assertEquals(2L, dao.countQueueEntries(session.id))
}
@Test
fun playbackWindowNeverClampsPastTheEndBackToTheLastTrack() {
assertEquals(0L, boundedPlaybackWindowStart(-10, 3))
assertEquals(2L, boundedPlaybackWindowStart(2, 3))
assertNull(boundedPlaybackWindowStart(3, 3))
assertNull(boundedPlaybackWindowStart(99, 3))
assertNull(boundedPlaybackWindowStart(0, 0))
}
@Test
fun userSnapshotsRotateRejectDamageAndRestoreTheNewestValidCopy() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
@@ -37,6 +37,11 @@ private const val ACTIVE_PLAYBACK_CONTEXT_ID = "active-context"
class StaleRevisionException : IllegalStateException("STALE_REVISION")
internal fun boundedPlaybackWindowStart(start: Long, total: Long): Long? {
val normalized = start.coerceAtLeast(0)
return normalized.takeIf { it < total }
}
private data class RemoteSyncHandle(
val syncId: String,
val sourceKey: String,
@@ -2208,8 +2213,13 @@ class AstraLibraryRepository private constructor(
val session = userDao.getPlaybackSession(sessionId)
?: error("Playback context $sessionId does not exist")
val total = userDao.countQueueEntries(sessionId)
val boundedStart = if (total == 0L) 0L else start.coerceIn(0, total - 1)
val entries = userDao.getQueueWindow(sessionId, boundedStart, limit)
val requestedStart = start.coerceAtLeast(0)
val boundedStart = boundedPlaybackWindowStart(requestedStart, total)
val entries = if (boundedStart == null) {
emptyList()
} else {
userDao.getQueueWindow(sessionId, boundedStart, limit)
}
val tracks = LinkedHashMap<String, ActiveTrackView>()
for (chunk in entries.map(PlaybackQueueEntryEntity::trackPath).distinct().chunked(MAX_PAGE_SIZE)) {
requireCatalog().catalogDao().getActiveTracks(chunk).forEach { tracks[it.path] = it }
@@ -2222,7 +2232,7 @@ class AstraLibraryRepository private constructor(
return mapOf(
"sessionId" to session.id,
"items" to items,
"windowStart" to boundedStart.toDouble(),
"windowStart" to (boundedStart ?: requestedStart).toDouble(),
"activePosition" to session.activePosition.toDouble(),
"totalCount" to total.toDouble(),
"contextJson" to session.contextJson,
+30 -11
View File
@@ -76,6 +76,15 @@ export interface PlaybackStartOptions {
source: PlaybackSource;
}
function toVirtualRntpTrack(
item: DbTrack & { queuePosition: number },
): RntpTrack {
return {
...toRntpTrack(dbTrackToTrack(item)),
astraQueuePosition: item.queuePosition,
};
}
const NEXT_REPEAT: Record<RepeatModeStr, RepeatModeStr> = {
none: 'all',
all: 'one',
@@ -337,7 +346,7 @@ export function restoreVirtualPlaybackContext(
restorePlaybackSession(null);
return;
}
const queueTracks = tracks.map(toRntpTrack);
const queueTracks = window.items.map(toVirtualRntpTrack);
const activeIndex = Math.max(
0,
Math.min(queueTracks.length - 1, window.activePosition - window.windowStart),
@@ -426,7 +435,7 @@ async function startVirtualWindow(
shuffle: boolean,
): Promise<void> {
const tracks = window.items.map(dbTrackToTrack);
const queueTracks = tracks.map(toRntpTrack);
const queueTracks = window.items.map(toVirtualRntpTrack);
const startIndex = Math.max(
0,
Math.min(queueTracks.length - 1, window.activePosition - window.windowStart),
@@ -496,14 +505,22 @@ async function replenishVirtualContext(): Promise<void> {
100,
);
if (virtualContext !== context || next.items.length === 0) return;
const additions = next.items.map(dbTrackToTrack).map(toRntpTrack);
const additions = next.items
.filter((item) => (
item.queuePosition >= context.loadedEnd &&
item.queuePosition < context.totalCount
))
.map(toVirtualRntpTrack);
if (additions.length === 0) return;
const nextLoadedEnd = Number(additions[additions.length - 1].astraQueuePosition) + 1;
if (!Number.isFinite(nextLoadedEnd) || nextLoadedEnd <= context.loadedEnd) return;
const before = useQueueStore.getState();
await appendUpcomingChunked(additions, before.tracks.length);
useQueueStore.getState().setSnapshot(
[...before.tracks, ...additions],
localIndex,
);
context.loadedEnd = next.items[next.items.length - 1].queuePosition + 1;
context.loadedEnd = Math.min(context.totalCount, nextLoadedEnd);
}
/** Returns a bounded page from the native virtual queue, or null for ordinary queues. */
@@ -519,12 +536,15 @@ export async function getVirtualQueuePage(
Math.max(1, Math.min(100, limit)),
);
if (virtualContext !== context) return null;
const boundedStart = Math.max(0, start);
return {
items: window.items.map((item) => ({
track: {
...toRntpTrack(dbTrackToTrack(item)),
astraQueuePosition: item.queuePosition,
},
items: window.items
.filter((item) => (
item.queuePosition >= boundedStart &&
item.queuePosition < window.totalCount
))
.map((item) => ({
track: toVirtualRntpTrack(item),
queuePosition: item.queuePosition,
})),
activePosition: window.activePosition,
@@ -599,8 +619,7 @@ async function mutateVirtualQueue(
const boundedActive = Math.max(0, activeLocal);
const upcoming = window.items
.filter((item) => item.queuePosition > window.activePosition)
.map(dbTrackToTrack)
.map(toRntpTrack);
.map(toVirtualRntpTrack);
const before = useQueueStore.getState();
const prefix = before.tracks.slice(0, boundedActive + 1);
+18 -5
View File
@@ -212,7 +212,6 @@ export const QueueTray = memo(function QueueTray({ onClose, embedded = false }:
const loadVirtualPage = useCallback(async (reset: boolean) => {
const state = getVirtualQueueState();
if (!state || (!reset && virtualLoading.current)) return;
virtualLoading.current = true;
const generation = reset ? ++virtualLoadGeneration.current : virtualLoadGeneration.current;
const existing = reset ? [] : virtualTracksRef.current;
const lastPosition = existing.length > 0
@@ -221,6 +220,15 @@ export const QueueTray = memo(function QueueTray({ onClose, embedded = false }:
const start = typeof lastPosition === 'number'
? lastPosition + 1
: state.activePosition + 1;
if (start >= state.totalCount) {
if (reset) {
virtualLoading.current = false;
virtualTracksRef.current = [];
setVirtualTracks([]);
}
return;
}
virtualLoading.current = true;
try {
const page = await getVirtualQueuePage(start, 100);
if (
@@ -228,10 +236,15 @@ export const QueueTray = memo(function QueueTray({ onClose, embedded = false }:
generation !== virtualLoadGeneration.current ||
getVirtualQueueState()?.sessionId !== state.sessionId
) return;
const next = reset ? page.items.map((item) => item.track) : [
...existing,
...page.items.map((item) => item.track),
];
const existingPositions = new Set(
existing.map((track) => track.astraQueuePosition).filter(
(position): position is number => typeof position === 'number'
)
);
const incoming = page.items
.filter((item) => item.queuePosition >= start && !existingPositions.has(item.queuePosition))
.map((item) => item.track);
const next = reset ? incoming : [...existing, ...incoming];
// Keep no more than five tray pages in JS.
const bounded = next.slice(-500);
virtualTracksRef.current = bounded;