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:
1 parent
27aa0a1d55
commit
9cd1317659
3 files changed
+40
-11
No files matched your search
@@ -275,7 +275,7 @@ public final class MusicRoutes {
|
|||||||
@OpenApi(
|
@OpenApi(
|
||||||
path = "/music/{guildId}/queue",
|
path = "/music/{guildId}/queue",
|
||||||
methods = HttpMethod.POST,
|
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"},
|
tags = {"Music"},
|
||||||
pathParams = @OpenApiParam(name = "guildId", description = "The Discord guild id"),
|
pathParams = @OpenApiParam(name = "guildId", description = "The Discord guild id"),
|
||||||
requestBody = @OpenApiRequestBody(content = @OpenApiContent(from = QueueRequest.class)),
|
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) {
|
private static void playSelect(Guild guild, MusicManager manager, QueueRequest body, String userId, Context ctx) {
|
||||||
String searchId = body.searchId();
|
String searchId = body.searchId();
|
||||||
|
boolean queueAll = Boolean.TRUE.equals(body.queueAll());
|
||||||
Integer index = body.index();
|
Integer index = body.index();
|
||||||
if (index == null) {
|
if (!queueAll && index == null) {
|
||||||
throw new BadRequestResponse("index is required when searchId is provided");
|
throw new BadRequestResponse("index is required when searchId is provided unless queueAll is true");
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Track> candidates = PendingSearchResults.consume(searchId, guild.getId());
|
List<Track> candidates = PendingSearchResults.consume(searchId, guild.getId());
|
||||||
@@ -354,7 +355,7 @@ public final class MusicRoutes {
|
|||||||
throw new NotFoundResponse("Unknown or expired searchId");
|
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");
|
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");
|
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);
|
Track track = candidates.get(index);
|
||||||
|
|
||||||
ctx.future(() -> resolveMember(guild, userId)
|
ctx.future(() -> resolveMember(guild, userId)
|
||||||
@@ -444,6 +455,18 @@ public final class MusicRoutes {
|
|||||||
return body;
|
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.
|
* 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 userId the Discord user id to credit as the requester
|
||||||
* @param searchId a search id previously returned by {@code /music/{guildId}/search}
|
* @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(
|
public record QueueRequest(
|
||||||
String userId,
|
String userId,
|
||||||
String searchId,
|
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
|
* <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}
|
* 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
|
* call carrying {@code searchId}/{@code index} (or {@code queueAll}). Fields are nullable since only a subset is
|
||||||
* on {@code status}: {@code queued} fills {@code track}/{@code queue_position} (confirmed
|
* 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
|
* selection), {@code queued_all} fills {@code tracks}/{@code queue_position} (confirmed bulk selection; {@code
|
||||||
* pending_playlist} fills {@code search_id}/{@code search_id_ttl_seconds}/{@code tracks}/{@code truncated}, and
|
* queue_position} is the resulting queue size), {@code pending_track} fills {@code search_id}/{@code
|
||||||
* {@code pending_search} fills {@code search_id}/{@code search_id_ttl_seconds}/{@code candidates}.
|
* 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.
|
* {@code search_id_ttl_seconds} indicates how many seconds remain before the {@code search_id} expires.
|
||||||
*/
|
*/
|
||||||
public record QueueResponse(
|
public record QueueResponse(
|
||||||
|
|||||||
Reference in new issue
Block a user