**Add playlist metadata support to queue and events**
Enhanced queueing functionality to include playlist metadata, such as name, identifier, and track count. Updated event payloads to handle playlist information and introduced a new `playlist_added` event for streamlined notifications. Adjusted tests and documentation to reflect these changes.
This commit is contained in:
1 parent
d376e245b7
commit
142cc77ec8
9 files changed
+173
-13
No files matched your search
@@ -41,6 +41,7 @@ import me.bobert.netneutrality.music.MusicManager;
|
||||
import me.bobert.netneutrality.music.MusicService;
|
||||
import me.bobert.netneutrality.music.PendingSearchResults;
|
||||
import me.bobert.netneutrality.music.PlayOutcome;
|
||||
import me.bobert.netneutrality.music.PlaylistInfo;
|
||||
import me.bobert.netneutrality.music.QueueActionResult;
|
||||
import me.bobert.netneutrality.music.SourceFinder;
|
||||
import me.bobert.netneutrality.music.TrackInfo;
|
||||
@@ -490,10 +491,12 @@ public final class MusicRoutes {
|
||||
throw new BadRequestResponse("index is required when searchId is provided unless queueAll is true");
|
||||
}
|
||||
|
||||
List<Track> candidates = PendingSearchResults.consume(searchId, guild.getId());
|
||||
if (candidates == null) {
|
||||
PendingSearchResults.Consumed consumed = PendingSearchResults.consumeWithPlaylist(searchId, guild.getId());
|
||||
if (consumed == null) {
|
||||
throw new NotFoundResponse("Unknown or expired searchId");
|
||||
}
|
||||
List<Track> candidates = consumed.candidates();
|
||||
PlaylistInfo playlist = consumed.playlist();
|
||||
|
||||
if (!queueAll && (index < 0 || index >= candidates.size())) {
|
||||
throw new BadRequestResponse("index is out of range");
|
||||
@@ -506,7 +509,7 @@ public final class MusicRoutes {
|
||||
if (queueAll) {
|
||||
ctx.future(() -> resolveMember(guild, userId)
|
||||
.thenApply(member -> {
|
||||
candidates.forEach(candidate -> manager.getQueueManager().queue(candidate, member));
|
||||
manager.getQueueManager().queue(candidates, member, playlist);
|
||||
announce(manager, String.format("Queued %d track%s requested by %s",
|
||||
candidates.size(), candidates.size() > 1 ? "s" : "", member.getEffectiveName()));
|
||||
ctx.json(queuedAllBody(candidates, manager.getQueueSize()));
|
||||
@@ -780,6 +783,16 @@ public final class MusicRoutes {
|
||||
Map<String, Object> summary = trackSummary(info.getTrack());
|
||||
summary.put("requester_id", info.getRequester().getId());
|
||||
summary.put("skip_count", info.getSkipCount());
|
||||
PlaylistInfo playlist = info.getPlaylist();
|
||||
if (playlist == null) {
|
||||
summary.put("playlist", null);
|
||||
} else {
|
||||
Map<String, Object> playlistMap = new LinkedHashMap<>();
|
||||
playlistMap.put("name", playlist.name());
|
||||
playlistMap.put("identifier", playlist.identifier());
|
||||
playlistMap.put("track_count", playlist.trackCount());
|
||||
summary.put("playlist", playlistMap);
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
@@ -46,10 +46,17 @@ public final class PendingSearchResults {
|
||||
* @return the generated search id
|
||||
*/
|
||||
public static String put(String guildId, List<Track> candidates) {
|
||||
return put(guildId, candidates, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the given candidates together with the playlist they came from (may be null).
|
||||
*/
|
||||
public static String put(String guildId, List<Track> candidates, PlaylistInfo playlist) {
|
||||
sweep();
|
||||
|
||||
String searchId = UUID.randomUUID().toString();
|
||||
entries.put(searchId, new Entry(guildId, candidates, Instant.now().plus(TTL)));
|
||||
entries.put(searchId, new Entry(guildId, candidates, playlist, Instant.now().plus(TTL)));
|
||||
return searchId;
|
||||
}
|
||||
|
||||
@@ -62,6 +69,14 @@ public final class PendingSearchResults {
|
||||
* belongs to a different guild
|
||||
*/
|
||||
public static List<Track> consume(String searchId, String guildId) {
|
||||
Consumed consumed = consumeWithPlaylist(searchId, guildId);
|
||||
return consumed == null ? null : consumed.candidates();
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link #consume(String, String)} but also returns the originating playlist, if any.
|
||||
*/
|
||||
public static Consumed consumeWithPlaylist(String searchId, String guildId) {
|
||||
sweep();
|
||||
|
||||
Entry entry = entries.remove(searchId);
|
||||
@@ -69,7 +84,10 @@ public final class PendingSearchResults {
|
||||
return null;
|
||||
}
|
||||
|
||||
return entry.candidates();
|
||||
return new Consumed(entry.candidates(), entry.playlist());
|
||||
}
|
||||
|
||||
public record Consumed(List<Track> candidates, PlaylistInfo playlist) {
|
||||
}
|
||||
|
||||
private static void sweep() {
|
||||
@@ -77,6 +95,6 @@ public final class PendingSearchResults {
|
||||
entries.entrySet().removeIf(entry -> now.isAfter(entry.getValue().expiresAt()));
|
||||
}
|
||||
|
||||
private record Entry(String guildId, List<Track> candidates, Instant expiresAt) {
|
||||
private record Entry(String guildId, List<Track> candidates, PlaylistInfo playlist, Instant expiresAt) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package me.bobert.netneutrality.music;
|
||||
/*
|
||||
* Copyright (c) anthorob
|
||||
*
|
||||
* https://discord.gg/qUa6zNFZsd
|
||||
*/
|
||||
|
||||
/**
|
||||
* Describes the playlist a queued track originated from.
|
||||
*
|
||||
* @param identifier the playlist identifier, or null when unknown
|
||||
* @param name the playlist display name
|
||||
* @param trackCount number of tracks in the playlist
|
||||
*/
|
||||
public record PlaylistInfo(String identifier, String name, int trackCount) {
|
||||
}
|
||||
@@ -79,10 +79,26 @@ public class QueueManager {
|
||||
item.put("uri", info.getTrack().getInfo().getUri());
|
||||
item.put("artwork_url", info.getTrack().getInfo().getArtworkUrl());
|
||||
item.put("requester_id", info.getRequester() == null ? null : info.getRequester().getId());
|
||||
item.put("playlist", playlistMap(info.getPlaylist()));
|
||||
return item;
|
||||
}
|
||||
|
||||
private static Map<String, Object> playlistMap(PlaylistInfo playlist) {
|
||||
if (playlist == null) {
|
||||
return null;
|
||||
}
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("name", playlist.name());
|
||||
map.put("identifier", playlist.identifier());
|
||||
map.put("track_count", playlist.trackCount());
|
||||
return map;
|
||||
}
|
||||
|
||||
private void notifyQueueUpdated(String change, List<Map<String, Object>> items) {
|
||||
notifyQueueUpdated(change, items, null);
|
||||
}
|
||||
|
||||
private void notifyQueueUpdated(String change, List<Map<String, Object>> items, PlaylistInfo playlist) {
|
||||
MusicManager musicManager = scheduler.getMusicManager();
|
||||
if (musicManager == null || musicManager.getEvents() == null) {
|
||||
return;
|
||||
@@ -91,6 +107,9 @@ public class QueueManager {
|
||||
payload.put("size", queue.size());
|
||||
payload.put("change", change);
|
||||
payload.put("items", items);
|
||||
if (playlist != null) {
|
||||
payload.put("playlist", playlistMap(playlist));
|
||||
}
|
||||
musicManager.getEvents().publish(MusicEvent.of(MusicEvent.Type.QUEUE_UPDATED, guild.getId(), payload));
|
||||
}
|
||||
|
||||
@@ -155,7 +174,47 @@ public class QueueManager {
|
||||
}
|
||||
|
||||
public void queue(List<Track> tracks, Member member) {
|
||||
tracks.forEach(track -> queue(track, member));
|
||||
queue(tracks, member, null);
|
||||
}
|
||||
|
||||
public void queue(List<Track> tracks, Member member, PlaylistInfo playlist) {
|
||||
if (playlist == null) {
|
||||
tracks.forEach(track -> queue(track, member));
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (queue) {
|
||||
boolean idle = scheduler.getCurrentTrack() == null;
|
||||
List<TrackInfo> added = new ArrayList<>();
|
||||
|
||||
for (Track track : tracks) {
|
||||
TrackInfo info = new TrackInfo(track, member, playlist);
|
||||
if (queue.contains(info)) {
|
||||
continue;
|
||||
}
|
||||
queue.add(info);
|
||||
added.add(info);
|
||||
}
|
||||
|
||||
if (added.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlaylistInfo actual = new PlaylistInfo(playlist.identifier(), playlist.name(), added.size());
|
||||
List<TrackInfo> all = new ArrayList<>(queue);
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
for (TrackInfo info : added) {
|
||||
items.add(item(all.indexOf(info), info));
|
||||
}
|
||||
notifyQueueUpdated("playlist_added", items, actual);
|
||||
|
||||
if (idle) {
|
||||
TrackInfo head = queue.peek();
|
||||
if (head != null) {
|
||||
startTrack(head.getTrack(), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void queue(Track track, Member member) {
|
||||
|
||||
@@ -65,7 +65,8 @@ public class SearchResultHandler extends AbstractAudioLoadResultHandler {
|
||||
boolean truncated = tracks.size() > maxPlaylistSize;
|
||||
List<Track> candidates = truncated ? tracks.subList(0, maxPlaylistSize) : tracks;
|
||||
|
||||
String searchId = PendingSearchResults.put(guild.getId(), candidates);
|
||||
String searchId = PendingSearchResults.put(guild.getId(), candidates,
|
||||
new PlaylistInfo(identifier, result.getInfo().getName(), candidates.size()));
|
||||
future.complete(new PlayOutcome.PendingPlaylist(searchId, identifier, result.getInfo().getName(), candidates, truncated));
|
||||
}
|
||||
|
||||
|
||||
@@ -21,12 +21,23 @@ public class TrackInfo {
|
||||
private final Set<String> skips;
|
||||
private static final Logger logger = LogManager.getLogger(TrackInfo.class);
|
||||
|
||||
private final PlaylistInfo playlist;
|
||||
|
||||
public TrackInfo(Track track, Member member) {
|
||||
this(track, member, null);
|
||||
}
|
||||
|
||||
public TrackInfo(Track track, Member member, PlaylistInfo playlist) {
|
||||
this.track = track;
|
||||
this.member = member;
|
||||
this.playlist = playlist;
|
||||
this.skips = new HashSet<>();
|
||||
}
|
||||
|
||||
public PlaylistInfo getPlaylist() {
|
||||
return playlist;
|
||||
}
|
||||
|
||||
public Track getTrack() {
|
||||
return track;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import me.bobert.netneutrality.core.tasks.trait.Deletable;
|
||||
import me.bobert.netneutrality.core.tasks.trait.WithChildren;
|
||||
import me.bobert.netneutrality.db.GSettings;
|
||||
import me.bobert.netneutrality.music.MusicManager;
|
||||
import me.bobert.netneutrality.music.PlaylistInfo;
|
||||
import me.bobert.netneutrality.music.SourceFinder;
|
||||
import me.bobert.netneutrality.core.tasks.generic.AcceptButton;
|
||||
import me.bobert.netneutrality.core.tasks.generic.CancelButton;
|
||||
@@ -93,7 +94,8 @@ public class LoadPlaylistTask extends ButtonTask implements WithChildren, Deleta
|
||||
|
||||
// Load playlist normally
|
||||
if (MusicUtils.connect(context, musicManager)) {
|
||||
musicManager.getQueueManager().queue(tracks, context.getGuildUser());
|
||||
musicManager.getQueueManager().queue(tracks, context.getGuildUser(),
|
||||
new PlaylistInfo(null, playlistName, tracks.size()));
|
||||
|
||||
context.edit(MessageFunction.buildAddingPlaylistMessage(tracks, playlistName, context.getGuildUser(), sourceType));
|
||||
}
|
||||
@@ -104,7 +106,7 @@ public class LoadPlaylistTask extends ButtonTask implements WithChildren, Deleta
|
||||
Member member = context.getGuildUser();
|
||||
|
||||
if (MusicUtils.connect(context, musicManager)) {
|
||||
musicManager.getQueueManager().queue(tracks, member);
|
||||
musicManager.getQueueManager().queue(tracks, member, new PlaylistInfo(null, playlistName, tracks.size()));
|
||||
context.reply(MessageFunction.buildAddingPlaylistMessage(tracks, playlistName, member, SourceFinder.AudioSourceType.Youtube));
|
||||
|
||||
return Lifecycle.REMOVE_SELF_AND_CHILDREN;
|
||||
|
||||
@@ -78,7 +78,7 @@ public class QueueManagerEventsTest {
|
||||
assertEquals("added", payload(event).get("change"));
|
||||
assertEquals(0, items(event).get(0).get("index"));
|
||||
assertEquals("t1", items(event).get(0).get("identifier"));
|
||||
assertEquals("u1", items(event).get(0).get("requesterId"));
|
||||
assertEquals("u1", items(event).get(0).get("requester_id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -134,6 +134,46 @@ public class QueueManagerEventsTest {
|
||||
assertEquals("guild1", events.get(1).guildId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void playlistPublishesSingleEventWithPlaylist() {
|
||||
List<Track> tracks = List.of(QueueManagerTest.mockTrack("t1"), QueueManagerTest.mockTrack("t2"),
|
||||
QueueManagerTest.mockTrack("t3"));
|
||||
|
||||
queueManager.queue(tracks, member, new PlaylistInfo("pl1", "My List", 3));
|
||||
|
||||
assertEquals(1, events.size());
|
||||
Map<String, Object> payload = payload(events.get(0));
|
||||
assertEquals("playlist_added", payload.get("change"));
|
||||
assertEquals(3, payload.get("size"));
|
||||
assertEquals(3, items(events.get(0)).size());
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> playlist = (Map<String, Object>) payload.get("playlist");
|
||||
assertEquals("My List", playlist.get("name"));
|
||||
assertEquals("pl1", playlist.get("identifier"));
|
||||
assertEquals(3, playlist.get("track_count"));
|
||||
assertNotNull(items(events.get(0)).get(0).get("playlist"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void playlistSkipsDuplicates() {
|
||||
fill(1);
|
||||
List<Track> tracks = List.of(QueueManagerTest.mockTrack("t1"), QueueManagerTest.mockTrack("t2"));
|
||||
|
||||
queueManager.queue(tracks, member, new PlaylistInfo(null, "L", 2));
|
||||
|
||||
assertEquals(1, events.size());
|
||||
assertEquals(1, items(events.get(0)).size());
|
||||
assertEquals("t2", items(events.get(0)).get(0).get("identifier"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleTrackHasNullPlaylist() {
|
||||
queueManager.queue(QueueManagerTest.mockTrack("t1"), member);
|
||||
|
||||
assertNull(items(events.get(0)).get(0).get("playlist"));
|
||||
assertFalse(payload(events.get(0)).containsKey("playlist"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeHeadPublishesRemovedAtIndexZero() {
|
||||
fill(2);
|
||||
|
||||
Reference in new issue
Block a user