Files
netneutrality/src/main/java/me/bobert/netneutrality/music/RestTrackLoadingHandler.java
T
anthorob d53cbbecd4 Add REST-driven music playback and pause/resume support
Introduced `/music/{guildId}/play` and `/music/{guildId}/play/select/{searchId}` POST routes for initiating and selecting music playback via the REST API. Added `/music/{guildId}/pause` and `/music/{guildId}/resume` POST routes to control playback state. Updated the music system with `RestTrackLoadingHandler`, `PlayOutcome`, `PendingSearchResults`, and DTOs for OpenAPI integration. Enhanced `MusicManager` to support REST-driven play requests with robust error handling and improved track loading logic.
2026-10-02 21:17:36 -04:00

122 lines
4.6 KiB
Java

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 net.dv8tion.jda.api.entities.Member;
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 queue operations and a {@link PlayOutcome}, without any
* Discord message context.
*
* <p>Mirrors {@link TrackLoadingHandler}'s branching but completes a future instead of replying
* on a {@link me.bobert.netneutrality.core.context.CommandContext}: direct tracks and playlists
* are queued immediately, while ambiguous search results are stashed in
* {@link PendingSearchResults} for a later {@code /play/select} call instead of being queued.
*/
public class RestTrackLoadingHandler extends AbstractAudioLoadResultHandler {
private static final Logger logger = LogManager.getLogger(RestTrackLoadingHandler.class);
private static final int SEARCH_RESULTS_LIMIT = 5;
private final String identifier;
private final Guild guild;
private final QueueManager queueManager;
private final Member member;
private final MusicManager manager;
private final CompletableFuture<PlayOutcome> future;
public RestTrackLoadingHandler(Member member, MusicManager manager, String identifier, CompletableFuture<PlayOutcome> future) {
this.member = member;
this.manager = manager;
this.identifier = identifier;
this.guild = manager.getGuild();
this.queueManager = manager.getQueueManager();
this.future = future;
}
@Override
public void ontrackLoaded(TrackLoaded result) {
Track track = result.getTrack();
logger.debug("Guild {}: track loaded: {}", guild.getId(), track.getInfo().getIdentifier());
queueManager.queue(track, member);
future.complete(new PlayOutcome.Queued(track, queueManager.getQueueSize()));
}
@Override
public void onPlaylistLoaded(PlaylistLoaded result) {
logger.debug("Guild {}: playlist loaded: {}", guild.getId(), identifier);
List<Track> tracks = result.getTracks();
int maxPlaylistSize = parseMaxPlaylistSize();
boolean truncated = tracks.size() > maxPlaylistSize;
List<Track> toQueue = truncated ? tracks.subList(0, maxPlaylistSize) : tracks;
queueManager.queue(toQueue, member);
future.complete(new PlayOutcome.PlaylistQueued(toQueue.size(), truncated));
}
/**
* Search results are not queued: the candidates are stashed under a generated search id and
* left for a REST caller to confirm via {@code /play/select}, since there is no interactive
* UI to pick one over HTTP.
*/
@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.SearchCandidates(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());
}
}
}