Refactor REST API music playback flow

Replaced `/play` and `/play/select` endpoints with more streamlined `/search` and `/queue` routes. Updated `MusicManager` and related components to separate query resolution from playback queuing, requiring explicit confirmation for each candidate. Introduced `SearchRequest`, `SearchResultHandler`, and revamped `PlayOutcome` to improve clarity and flexibility.
This commit is contained in:
anthorob committed 2026-10-03 11:21:31 -04:00
1 parent 0e68886eb2
commit a605a834b4
8 files changed
+173 -115

No files matched your search

@@ -0,0 +1,111 @@
package me.bobert.netneutrality.music;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import dev.arbjerg.lavalink.client.AbstractAudioLoadResultHandler;
import dev.arbjerg.lavalink.client.player.LoadFailed;
import dev.arbjerg.lavalink.client.player.PlaylistLoaded;
import dev.arbjerg.lavalink.client.player.SearchResult;
import dev.arbjerg.lavalink.client.player.Track;
import dev.arbjerg.lavalink.client.player.TrackLoaded;
import me.bobert.netneutrality.core.LogHandler;
import me.bobert.netneutrality.db.GSettings;
import net.dv8tion.jda.api.entities.Guild;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* Translates a Lavalink load result into a {@link PlayOutcome} without ever queuing anything, so
* that every REST load request, not only ambiguous text searches, requires a separate
* {@code /play} call with {@code searchId}/{@code index} to actually queue a track.
*
* <p>A single resolved track, the tracks of a playlist, and ambiguous search results are each
* stashed as candidates in {@link PendingSearchResults} and reported back with their own
* {@link PlayOutcome.PendingTrack}, {@link PlayOutcome.PendingPlaylist}, or
* {@link PlayOutcome.PendingSearch} outcome, respectively.
*/
public class SearchResultHandler extends AbstractAudioLoadResultHandler {
private static final Logger logger = LogManager.getLogger(SearchResultHandler.class);
private static final int SEARCH_RESULTS_LIMIT = 5;
private final String identifier;
private final Guild guild;
private final MusicManager manager;
private final CompletableFuture<PlayOutcome> future;
public SearchResultHandler(MusicManager manager, String identifier, CompletableFuture<PlayOutcome> future) {
this.identifier = identifier;
this.guild = manager.getGuild();
this.manager = manager;
this.future = future;
}
@Override
public void ontrackLoaded(TrackLoaded result) {
Track track = result.getTrack();
logger.debug("Guild {}: track resolved: {}", guild.getId(), track.getInfo().getIdentifier());
String searchId = PendingSearchResults.put(guild.getId(), List.of(track));
future.complete(new PlayOutcome.PendingTrack(searchId, track));
}
@Override
public void onPlaylistLoaded(PlaylistLoaded result) {
logger.debug("Guild {}: playlist resolved: {}", guild.getId(), identifier);
List<Track> tracks = result.getTracks();
int maxPlaylistSize = parseMaxPlaylistSize();
boolean truncated = tracks.size() > maxPlaylistSize;
List<Track> candidates = truncated ? tracks.subList(0, maxPlaylistSize) : tracks;
String searchId = PendingSearchResults.put(guild.getId(), candidates);
future.complete(new PlayOutcome.PendingPlaylist(searchId, candidates, truncated));
}
@Override
public void onSearchResultLoaded(SearchResult result) {
List<Track> tracks = result.getTracks();
logger.debug("Guild {}: search returned {} results for {}", guild.getId(), tracks.size(), identifier);
if (tracks.isEmpty()) {
noMatches();
return;
}
List<Track> candidates = tracks.stream().limit(SEARCH_RESULTS_LIMIT).toList();
String searchId = PendingSearchResults.put(guild.getId(), candidates);
future.complete(new PlayOutcome.PendingSearch(searchId, candidates));
}
@Override
public void noMatches() {
logger.debug("Guild {}: no matches for identifier {}", guild.getId(), identifier);
future.complete(new PlayOutcome.NoMatches());
}
@Override
public void loadFailed(LoadFailed result) {
logger.debug("Guild {}: load failed for identifier {}", guild.getId(), identifier);
RuntimeException exception = new RuntimeException("Failed to load track: " + result.getException().getMessage());
LogHandler.log(exception);
future.complete(new PlayOutcome.Failed(exception.getMessage()));
}
private int parseMaxPlaylistSize() {
try {
return Integer.parseInt(manager.getGuildService().getGuildSettingService().getOrDefault(GSettings.MUSIC_MAXPLAYLISTSIZE));
} catch (NumberFormatException e) {
return Integer.parseInt(GSettings.MUSIC_MAXPLAYLISTSIZE.getDefaultValue());
}
}
}