Support bulk queuing with queueAll in /music/{guildId}/queue

Extended the queue endpoint to allow bulk queuing of all search candidates by specifying `queueAll=true`. Updated relevant request and response DTOs, OpenAPI documentation, and endpoint logic to handle bulk queuing while maintaining input validation for individual candidates.
This commit is contained in:
anthorob committed 2026-10-03 12:17:37 -04:00
1 parent 27aa0a1d55
commit 9cd1317659
3 files changed
+40 -11

No files matched your search

@@ -275,7 +275,7 @@ public final class MusicRoutes {
@OpenApi(
path = "/music/{guildId}/queue",
methods = HttpMethod.POST,
summary = "Queue a previously returned search candidate by passing the searchId/index returned by /music/{guildId}/search; the bot must already be connected to a voice channel in this guild",
summary = "Queue a previously returned search candidate by passing the searchId/index returned by /music/{guildId}/search, or queue every candidate at once by passing searchId with queueAll=true; the bot must already be connected to a voice channel in this guild",
tags = {"Music"},
pathParams = @OpenApiParam(name = "guildId", description = "The Discord guild id"),
requestBody = @OpenApiRequestBody(content = @OpenApiContent(from = QueueRequest.class)),
@@ -344,9 +344,10 @@ public final class MusicRoutes {
*/
private static void playSelect(Guild guild, MusicManager manager, QueueRequest body, String userId, Context ctx) {
String searchId = body.searchId();
boolean queueAll = Boolean.TRUE.equals(body.queueAll());
Integer index = body.index();
if (index == null) {
throw new BadRequestResponse("index is required when searchId is provided");
if (!queueAll && index == null) {
throw new BadRequestResponse("index is required when searchId is provided unless queueAll is true");
}
List<Track> candidates = PendingSearchResults.consume(searchId, guild.getId());
@@ -354,7 +355,7 @@ public final class MusicRoutes {
throw new NotFoundResponse("Unknown or expired searchId");
}
if (index < 0 || index >= candidates.size()) {
if (!queueAll && (index < 0 || index >= candidates.size())) {
throw new BadRequestResponse("index is out of range");
}
@@ -362,6 +363,16 @@ public final class MusicRoutes {
throw new BadRequestResponse("The bot isn't connected to a voice channel in this guild");
}
if (queueAll) {
ctx.future(() -> resolveMember(guild, userId)
.thenApply(member -> {
candidates.forEach(candidate -> manager.getQueueManager().queue(candidate, member));
ctx.json(queuedAllBody(candidates, manager.getQueueSize()));
return null;
}));
return;
}
Track track = candidates.get(index);
ctx.future(() -> resolveMember(guild, userId)
@@ -444,6 +455,18 @@ public final class MusicRoutes {
return body;
}
/**
* Builds a JSON-friendly body for a batch of tracks that were just queued via {@code
* queueAll}.
*/
private static Map<String, Object> queuedAllBody(List<Track> tracks, int queueSize) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("status", "queued_all");
body.put("tracks", tracks.stream().map(MusicRoutes::trackSummary).toList());
body.put("queue_position", queueSize);
return body;
}
/**
* Resolves the {@link MusicManager} for the guild path parameter, creating it on first use.
*/
@@ -11,11 +11,15 @@ package me.bobert.netneutrality.http.server.dto;
*
* @param userId the Discord user id to credit as the requester
* @param searchId a search id previously returned by {@code /music/{guildId}/search}
* @param index the index of the candidate to queue within the {@code searchId}'s results
* @param index the index of the candidate to queue within the {@code searchId}'s results;
* ignored when {@code queueAll} is {@code true}
* @param queueAll when {@code true}, queues every candidate of the {@code searchId}'s results
* instead of a single {@code index}
*/
public record QueueRequest(
String userId,
String searchId,
Integer index
Integer index,
Boolean queueAll
) {
}
@@ -14,11 +14,13 @@ import java.util.List;
*
* <p>Nothing is ever queued directly by {@code /music/{guildId}/search}: every resolved query is
* stashed as a pending candidate, to be confirmed via a follow-up {@code /music/{guildId}/queue}
* call carrying {@code searchId}/{@code index}. Fields are nullable since only a subset is populated depending
* on {@code status}: {@code queued} fills {@code track}/{@code queue_position} (confirmed
* selection), {@code pending_track} fills {@code search_id}/{@code search_id_ttl_seconds}/{@code track}, {@code
* pending_playlist} fills {@code search_id}/{@code search_id_ttl_seconds}/{@code tracks}/{@code truncated}, and
* {@code pending_search} fills {@code search_id}/{@code search_id_ttl_seconds}/{@code candidates}.
* call carrying {@code searchId}/{@code index} (or {@code queueAll}). Fields are nullable since only a subset is
* populated depending on {@code status}: {@code queued} fills {@code track}/{@code queue_position} (confirmed
* selection), {@code queued_all} fills {@code tracks}/{@code queue_position} (confirmed bulk selection; {@code
* queue_position} is the resulting queue size), {@code pending_track} fills {@code search_id}/{@code
* search_id_ttl_seconds}/{@code track}, {@code pending_playlist} fills {@code search_id}/{@code
* search_id_ttl_seconds}/{@code tracks}/{@code truncated}, and {@code pending_search} fills {@code search_id}/{@code
* search_id_ttl_seconds}/{@code candidates}.
* {@code search_id_ttl_seconds} indicates how many seconds remain before the {@code search_id} expires.
*/
public record QueueResponse(