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.
122 lines
4.6 KiB
Java
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());
|
|
}
|
|
}
|
|
}
|