diff --git a/src/main/java/me/bobert/netneutrality/http/server/MusicRoutes.java b/src/main/java/me/bobert/netneutrality/http/server/MusicRoutes.java index 6057f8b..9653f79 100644 --- a/src/main/java/me/bobert/netneutrality/http/server/MusicRoutes.java +++ b/src/main/java/me/bobert/netneutrality/http/server/MusicRoutes.java @@ -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 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 queuedAllBody(List tracks, int queueSize) { + Map 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. */ diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/QueueRequest.java b/src/main/java/me/bobert/netneutrality/http/server/dto/QueueRequest.java index 2e0bdc1..0daf99a 100644 --- a/src/main/java/me/bobert/netneutrality/http/server/dto/QueueRequest.java +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/QueueRequest.java @@ -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 ) { } diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/QueueResponse.java b/src/main/java/me/bobert/netneutrality/http/server/dto/QueueResponse.java index 96efed6..aaa4c88 100644 --- a/src/main/java/me/bobert/netneutrality/http/server/dto/QueueResponse.java +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/QueueResponse.java @@ -14,11 +14,13 @@ import java.util.List; * *

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(