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