From 031db907ae9d44bfc8fa04c89067840d530cc846 Mon Sep 17 00:00:00 2001 From: Boof2015 <75185879+Boof2015@users.noreply.github.com> Date: Fri, 24 Jul 2026 02:26:36 -0400 Subject: [PATCH] fix repeating end of queue bug --- .../data/RoomLibraryRepositoryTest.kt | 9 ++++ .../data/AstraLibraryRepository.kt | 16 +++++-- src/audio/playbackController.ts | 45 +++++++++++++------ src/components/queue/QueueTray.tsx | 23 +++++++--- 4 files changed, 72 insertions(+), 21 deletions(-) diff --git a/modules/astra-library-scanner/android/src/androidTest/java/expo/modules/astralibraryscanner/data/RoomLibraryRepositoryTest.kt b/modules/astra-library-scanner/android/src/androidTest/java/expo/modules/astralibraryscanner/data/RoomLibraryRepositoryTest.kt index 403780c..6de27bb 100644 --- a/modules/astra-library-scanner/android/src/androidTest/java/expo/modules/astralibraryscanner/data/RoomLibraryRepositoryTest.kt +++ b/modules/astra-library-scanner/android/src/androidTest/java/expo/modules/astralibraryscanner/data/RoomLibraryRepositoryTest.kt @@ -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() diff --git a/modules/astra-library-scanner/android/src/main/java/expo/modules/astralibraryscanner/data/AstraLibraryRepository.kt b/modules/astra-library-scanner/android/src/main/java/expo/modules/astralibraryscanner/data/AstraLibraryRepository.kt index 6345f37..590f5e9 100644 --- a/modules/astra-library-scanner/android/src/main/java/expo/modules/astralibraryscanner/data/AstraLibraryRepository.kt +++ b/modules/astra-library-scanner/android/src/main/java/expo/modules/astralibraryscanner/data/AstraLibraryRepository.kt @@ -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() 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, diff --git a/src/audio/playbackController.ts b/src/audio/playbackController.ts index ed0d243..c68b165 100644 --- a/src/audio/playbackController.ts +++ b/src/audio/playbackController.ts @@ -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 = { 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 { 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 { 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,14 +536,17 @@ 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, - }, - queuePosition: 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, totalCount: window.totalCount, }; @@ -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); diff --git a/src/components/queue/QueueTray.tsx b/src/components/queue/QueueTray.tsx index 3f3b79b..1a58784 100644 --- a/src/components/queue/QueueTray.tsx +++ b/src/components/queue/QueueTray.tsx @@ -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;