Compare commits

...
Author SHA1 Message Date
anthorob 363e02f409 Delay autoplay triggering until the current track nears its end. 2026-10-31 17:09:46 -04:00
2 changed files with 31 additions and 13 deletions

No files matched your search

@@ -37,6 +37,7 @@ public class TrackScheduler {
private final StuckTrackRecoveryHandler stuckTrackRecoveryHandler; private final StuckTrackRecoveryHandler stuckTrackRecoveryHandler;
private static final long POSITION_THROTTLE_MS = 5000; private static final long POSITION_THROTTLE_MS = 5000;
private volatile long lastPositionSent = Long.MIN_VALUE; private volatile long lastPositionSent = Long.MIN_VALUE;
private static final long AUTOPLAY_REMAINING_MS = 30_000;
private final AutoplayService autoplayService; private final AutoplayService autoplayService;
private volatile boolean autoplayEnabled; private volatile boolean autoplayEnabled;
private final AtomicBoolean autoplayLoading = new AtomicBoolean(); private final AtomicBoolean autoplayLoading = new AtomicBoolean();
@@ -91,14 +92,11 @@ public class TrackScheduler {
} }
/** /**
* Enables or disables autoplay. Enabling it while the last track of the queue is already * Enables or disables autoplay. Recommendations are only loaded once the last track of the
* playing loads its recommendations immediately. * queue is close to its end, so users still get a chance to queue their own tracks.
*/ */
public void setAutoplayEnabled(boolean enabled) { public void setAutoplayEnabled(boolean enabled) {
this.autoplayEnabled = enabled; this.autoplayEnabled = enabled;
if (enabled) {
maybeAutoplay();
}
} }
boolean tryBeginAutoplayLoad() { boolean tryBeginAutoplayLoad() {
@@ -109,9 +107,13 @@ public class TrackScheduler {
autoplayLoading.set(false); autoplayLoading.set(false);
} }
private void maybeAutoplay() { private void maybeAutoplay(long position) {
Track track = currentTrack; Track track = currentTrack;
if (autoplayEnabled && track != null && queueManager.getQueueSize() <= 1) { if (!autoplayEnabled || track == null || queueManager.getQueueSize() > 1) {
return;
}
long length = track.getInfo().getLength();
if (length > 0 && length - position <= AUTOPLAY_REMAINING_MS) {
autoplayService.loadFor(this, track); autoplayService.loadFor(this, track);
} }
} }
@@ -146,7 +148,6 @@ public class TrackScheduler {
currentTrack = track; currentTrack = track;
stuckTrackRecoveryHandler.reset(); stuckTrackRecoveryHandler.reset();
publish(MusicEvent.Type.TRACK_START, track, null); publish(MusicEvent.Type.TRACK_START, track, null);
maybeAutoplay();
} }
public void onTrackEnd(Track track, TrackEndEvent.AudioTrackEndReason endReason) { public void onTrackEnd(Track track, TrackEndEvent.AudioTrackEndReason endReason) {
@@ -236,6 +237,7 @@ public class TrackScheduler {
} }
void onPlayerUpdate(long timestamp, long position, boolean connected, long ping, long now) { void onPlayerUpdate(long timestamp, long position, boolean connected, long ping, long now) {
maybeAutoplay(position);
if (manager.getEvents() == null || currentTrack == null) { if (manager.getEvents() == null || currentTrack == null) {
return; return;
} }
@@ -147,13 +147,29 @@ public class TrackSchedulerTest {
} }
@Test @Test
public void onTrackStartLoadsAutoplayWhenEnabledAndLastTrack() { public void onTrackStartDoesNotLoadAutoplayImmediately() {
AutoplayService service = mock(AutoplayService.class); AutoplayService service = mock(AutoplayService.class);
TrackScheduler autoplay = new TrackScheduler(musicManager, service); TrackScheduler autoplay = new TrackScheduler(musicManager, service);
Track track = QueueManagerTest.mockTrack("t1"); Track track = QueueManagerTest.mockTrack("t1");
when(track.getInfo().getLength()).thenReturn(200_000L);
autoplay.setAutoplayEnabled(true); autoplay.setAutoplayEnabled(true);
autoplay.onTrackStart(track); autoplay.onTrackStart(track);
autoplay.onPlayerUpdate(1L, 1_000L, true, 20L, 1000L);
verify(service, never()).loadFor(any(), any());
}
@Test
public void autoplayLoadsWhenLastTrackNearsItsEnd() {
AutoplayService service = mock(AutoplayService.class);
TrackScheduler autoplay = new TrackScheduler(musicManager, service);
Track track = QueueManagerTest.mockTrack("t1");
when(track.getInfo().getLength()).thenReturn(200_000L);
autoplay.setAutoplayEnabled(true);
autoplay.onTrackStart(track);
autoplay.onPlayerUpdate(1L, 175_000L, true, 20L, 1000L);
verify(service).loadFor(autoplay, track); verify(service).loadFor(autoplay, track);
} }
@@ -180,20 +196,20 @@ public class TrackSchedulerTest {
clearInvocations(service); clearInvocations(service);
autoplay.onTrackStart(t1); autoplay.onTrackStart(t1);
autoplay.onPlayerUpdate(1L, 999_999L, true, 20L, 1000L);
verify(service, never()).loadFor(any(), any()); verify(service, never()).loadFor(any(), any());
} }
@Test @Test
public void enablingAutoplayWhileLastTrackPlayingTriggersLoad() { public void enablingAutoplayDoesNotLoadImmediately() {
AutoplayService service = mock(AutoplayService.class); AutoplayService service = mock(AutoplayService.class);
TrackScheduler autoplay = new TrackScheduler(musicManager, service); TrackScheduler autoplay = new TrackScheduler(musicManager, service);
Track track = QueueManagerTest.mockTrack("t1"); autoplay.onTrackStart(QueueManagerTest.mockTrack("t1"));
autoplay.onTrackStart(track);
autoplay.setAutoplayEnabled(true); autoplay.setAutoplayEnabled(true);
verify(service).loadFor(autoplay, track); verify(service, never()).loadFor(any(), any());
} }
@Test @Test