diff --git a/README.md b/README.md index a6f2f1255..56e63fa1a 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,8 @@ _With cmorley191/MusicBot, you'll find that:_ { "emoji": "❓", "weight": 7 } ]}}} ``` +- Quality of life fixes: + - The counter showing progress on loading additional tracks in a playlist now accurately shows the speed at which they are loaded. ## Compiling from source The main JMusicBot repo makes this sound way harder than it actually is, and doesn't provide instructions. Here they are: diff --git a/src/main/java/com/jagrosh/jmusicbot/SpotifyAPI.java b/src/main/java/com/jagrosh/jmusicbot/SpotifyAPI.java index 291d8c584..8a51ff306 100644 --- a/src/main/java/com/jagrosh/jmusicbot/SpotifyAPI.java +++ b/src/main/java/com/jagrosh/jmusicbot/SpotifyAPI.java @@ -227,6 +227,8 @@ public SpotifyTrack getTrack(String trackId) throws IOException { } public SpotifyPlaylist getPlaylist(String playlistId) throws IOException { + ArrayList tracks = new ArrayList(); + String response = executeQuery( "GET", "api.spotify.com", @@ -239,16 +241,38 @@ public SpotifyPlaylist getPlaylist(String playlistId) throws IOException { if (!jo.has("id") || !jo.getString("id").equals(playlistId) || !jo.has("name") || !jo.has("owner") || !jo.getJSONObject("owner").has("display_name") - || !jo.has("tracks") || !jo.getJSONObject("tracks").has("items")) { + || !jo.has("tracks") || !jo.getJSONObject("tracks").has("items") || !jo.getJSONObject("tracks").has("total")) { throw new RuntimeException(String.format("Unexpected response from spotify /v1/playlists endpoint: %s", response)); } String playlistName = String.format("%s by %s", jo.getString("name"), jo.getJSONObject("owner").getString("display_name")); - + int totalTracks = jo.getJSONObject("tracks").getInt("total"); JSONArray tracksData = jo.getJSONObject("tracks").getJSONArray("items"); - ArrayList tracks = new ArrayList(); - for (int iTrack = 0; iTrack < tracksData.length(); iTrack++) { - tracks.add(parseTrack(tracksData.getJSONObject(iTrack).getJSONObject("track"))); + + int iQuery = 0; + while(iQuery < 20) { + for (int iTrack = 0; iTrack < tracksData.length(); iTrack++) { + tracks.add(parseTrack(tracksData.getJSONObject(iTrack).getJSONObject("track"))); + } + + iQuery += 1; + + int offset = 100 * iQuery; + if (offset >= totalTracks) break; + + response = executeQuery( + "GET", + "api.spotify.com", + "/v1/playlists", + playlistId.replaceAll("[^a-zA-Z0-9_-]", "") + "/tracks?offset=" + offset + "&limit=100", + null, + null); + + jo = new JSONObject(response); + if (!jo.has("items")) { + throw new RuntimeException(String.format("Unexpected response from spotify /v1/playlists/.../tracks endpoint: %s", response)); + } + tracksData = jo.getJSONArray("items"); } return new SpotifyPlaylist(playlistName, Arrays.copyOf(tracks.toArray(), tracks.size(), SpotifyTrack[].class)); diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/music/PlayCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/music/PlayCmd.java index fccc5730c..34266b743 100644 --- a/src/main/java/com/jagrosh/jmusicbot/commands/music/PlayCmd.java +++ b/src/main/java/com/jagrosh/jmusicbot/commands/music/PlayCmd.java @@ -136,25 +136,76 @@ public void doCommand(CommandEvent event) } } + /** + * A proxy for Message.editMessage requests to help with discord rate limits. + * + * Only queues one editMessage request at a time. Drops redundant + * requests if mulitple are made while a request is active, which + * can happen during editMessage rate limiting. + */ + private class FrequentMessageEditAgent { + private final Message m; + private boolean editInProgress; + private String queuedNewText; + + private FrequentMessageEditAgent(Message m) { + this.m = m; + this.editInProgress = false; + this.queuedNewText = null; + } + + private synchronized void handleEditComplete() { + String queuedNewText = this.queuedNewText; + if (queuedNewText == null) { + editInProgress = false; + } else { + this.m.editMessage(queuedNewText).queue((_m) -> { + this.handleEditComplete(); + }); + this.queuedNewText = null; + } + } + + private synchronized void queueEditMessage(String newText) { + if (newText == null) return; + + if (!editInProgress) { + this.m.editMessage(newText).queue((_m) -> { + this.handleEditComplete(); + }); + this.editInProgress = true; + } else { + this.queuedNewText = newText; + } + } + } + private class ResultHandler implements AudioLoadResultHandler { protected final Message m; protected final CommandEvent event; protected final boolean ytsearch; + protected final FrequentMessageEditAgent frequentEditAgent; private ResultHandler(Message m, CommandEvent event, boolean ytsearch) + { + this(m, event, ytsearch, new FrequentMessageEditAgent(m)); + } + + private ResultHandler(Message m, CommandEvent event, boolean ytsearch, FrequentMessageEditAgent frequentEditAgent) { this.m = m; this.event = event; this.ytsearch = ytsearch; + this.frequentEditAgent = frequentEditAgent; } protected void loadSingle(AudioTrack track, AudioPlaylist playlist) { if(bot.getConfig().isTooLong(track)) { - m.editMessage(FormatUtil.filter(bot.getWarning(event)+" This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `" - +FormatUtil.formatTime(track.getDuration())+"` > `"+FormatUtil.formatTime(bot.getConfig().getMaxSeconds()*1000)+"`")).queue(); + frequentEditAgent.queueEditMessage(FormatUtil.filter(bot.getWarning(event)+" This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `" + +FormatUtil.formatTime(track.getDuration())+"` > `"+FormatUtil.formatTime(bot.getConfig().getMaxSeconds()*1000)+"`")); return; } AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler(); @@ -162,7 +213,7 @@ protected void loadSingle(AudioTrack track, AudioPlaylist playlist) String addMsg = FormatUtil.filter(bot.getSuccess(event)+" Added **"+track.getInfo().title +"** (`"+FormatUtil.formatTime(track.getDuration())+"`) "+(pos==0?"to begin playing":" to the queue at position "+pos)); if(playlist==null || !event.getSelfMember().hasPermission(event.getTextChannel(), Permission.MESSAGE_ADD_REACTION)) - m.editMessage(addMsg).queue(); + frequentEditAgent.queueEditMessage(addMsg); else { new ButtonMenu.Builder() @@ -173,9 +224,9 @@ protected void loadSingle(AudioTrack track, AudioPlaylist playlist) .setAction(re -> { if(re.getName().equals(LOAD)) - m.editMessage(addMsg+"\n"+bot.getSuccess(event)+" Loaded **"+loadPlaylist(playlist, track)+"** additional tracks!").queue(); + frequentEditAgent.queueEditMessage(addMsg+"\n"+bot.getSuccess(event)+" Loaded **"+loadPlaylist(playlist, track)+"** additional tracks!"); else - m.editMessage(addMsg).queue(); + frequentEditAgent.queueEditMessage(addMsg); }).setFinalAction(m -> { try{ m.clearReactions().queue(); }catch(PermissionException ignore) {} @@ -221,16 +272,16 @@ else if (playlist.getSelectedTrack()!=null) int count = loadPlaylist(playlist, null); if(count==0) { - m.editMessage(FormatUtil.filter(bot.getWarning(event)+" All entries in this playlist "+(playlist.getName()==null ? "" : "(**"+playlist.getName() - +"**) ")+"were longer than the allowed maximum (`"+bot.getConfig().getMaxTime()+"`)")).queue(); + frequentEditAgent.queueEditMessage(FormatUtil.filter(bot.getWarning(event)+" All entries in this playlist "+(playlist.getName()==null ? "" : "(**"+playlist.getName() + +"**) ")+"were longer than the allowed maximum (`"+bot.getConfig().getMaxTime()+"`)")); } else { - m.editMessage(FormatUtil.filter(bot.getSuccess(event)+" Found " + frequentEditAgent.queueEditMessage(FormatUtil.filter(bot.getSuccess(event)+" Found " +(playlist.getName()==null?"a playlist":"playlist **"+playlist.getName()+"**")+" with `" + playlist.getTracks().size()+"` entries; added to the queue!" + (count `"+FormatUtil.formatTime(bot.getConfig().getMaxSeconds()*1000)+"`")).queue(); + frequentEditAgent.queueEditMessage(FormatUtil.filter(bot.getWarning(event)+" This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `" + +FormatUtil.formatTime(track.getDuration())+"` > `"+FormatUtil.formatTime(bot.getConfig().getMaxSeconds()*1000)+"`")); return; } AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler(); @@ -300,7 +353,7 @@ protected void loadSingle(AudioTrack track, AudioPlaylist UNUSED) String addMsg = FormatUtil.filter(bot.getSuccess(event)+" Added **"+track.getInfo().title +"** (`"+FormatUtil.formatTime(track.getDuration())+"`) "+(pos==0?"to begin playing":" to the queue at position "+pos)); if(playlist.tracks.length == 1 || !event.getSelfMember().hasPermission(event.getTextChannel(), Permission.MESSAGE_ADD_REACTION)) - m.editMessage(addMsg).queue(); + frequentEditAgent.queueEditMessage(addMsg); else { new ButtonMenu.Builder() @@ -312,11 +365,11 @@ protected void loadSingle(AudioTrack track, AudioPlaylist UNUSED) { if(re.getName().equals(LOAD)) { String newMessageText = addMsg+"\n"+bot.getSuccess(event)+" Loading **1/"+(playlist.tracks.length - 1)+"** additional tracks..."; - m.editMessage(newMessageText).queue(); - bot.getPlayerManager().loadItemOrdered(event.getGuild(), "ytsearch:"+getYoutubeQueryOfTrack(playlist.tracks[1]), new SpotifyResultHandler(m,event,playlist,1,newMessageText,prevErrors)); + frequentEditAgent.queueEditMessage(newMessageText); + bot.getPlayerManager().loadItemOrdered(event.getGuild(), "ytsearch:"+getYoutubeQueryOfTrack(playlist.tracks[1]), new SpotifyResultHandler(m,event,playlist,1,newMessageText,prevErrors, frequentEditAgent)); } else - m.editMessage(addMsg).queue(); + frequentEditAgent.queueEditMessage(addMsg); }).setFinalAction(m -> { try{ m.clearReactions().queue(); }catch(PermissionException ignore) {} @@ -331,14 +384,14 @@ protected void loadSingle(AudioTrack track, AudioPlaylist UNUSED) { newMessageText = prevMessageText+"\n"+FormatUtil.filter(bot.getError(event)+" Couldn't find youtube link for track " + (iTrack + 1) + " of " + playlist.tracks.length + ": "+playlist.tracks[iTrack].name); newErrors++; - m.editMessage(newMessageText).queue(); + frequentEditAgent.queueEditMessage(newMessageText); } else if(bot.getConfig().isTooLong(track)) { newMessageText = prevMessageText+"\n"+FormatUtil.filter(bot.getWarning(event)+" This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `" +FormatUtil.formatTime(track.getDuration())+"` > `"+FormatUtil.formatTime(bot.getConfig().getMaxSeconds()*1000)+"`"); newErrors++; - m.editMessage(newMessageText).queue(); + frequentEditAgent.queueEditMessage(newMessageText); } else { AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler(); handler.addTrack(new QueuedTrack(track, event.getAuthor())); @@ -354,8 +407,8 @@ else if(bot.getConfig().isTooLong(track)) if (iTrack < playlist.tracks.length - 1) { // update second line ("loading tracks...") newMessageText = firstLine+"\n"+(bot.getSuccess(event)+" Loading **" + (iTrack + 1) + "/"+(playlist.tracks.length - 1)+"** additional tracks...")+(remainingParts.length == 0 ? "" : "\n"+String.join("\n", remainingParts)); - m.editMessage(newMessageText).queue(); - bot.getPlayerManager().loadItemOrdered(event.getGuild(), "ytsearch:"+getYoutubeQueryOfTrack(playlist.tracks[iTrack + 1]), new SpotifyResultHandler(m,event,playlist,iTrack + 1,newMessageText,newErrors)); + frequentEditAgent.queueEditMessage(newMessageText); + bot.getPlayerManager().loadItemOrdered(event.getGuild(), "ytsearch:"+getYoutubeQueryOfTrack(playlist.tracks[iTrack + 1]), new SpotifyResultHandler(m,event,playlist,iTrack + 1,newMessageText,newErrors,frequentEditAgent)); } else { // all done! finalize message // remove the second line ("loading tracks...") but keep everything else @@ -363,7 +416,7 @@ else if(bot.getConfig().isTooLong(track)) firstLine + (remainingParts.length == 0 ? "" : "\n"+String.join("\n", remainingParts)), bot.getSuccess(event)+" Loaded **"+(playlist.tracks.length - newErrors - 1)+"** additional tracks!" ); - m.editMessage(newMessageText).queue(); + frequentEditAgent.queueEditMessage(newMessageText); } } } @@ -397,7 +450,7 @@ public void noMatches() if(ytsearch) loadSingle(null, null); else - bot.getPlayerManager().loadItemOrdered(event.getGuild(), "ytsearch:"+getYoutubeQueryOfTrack(playlist.tracks[iTrack]), new SpotifyResultHandler(m,event,playlist,iTrack + 1,prevMessageText,prevErrors)); + bot.getPlayerManager().loadItemOrdered(event.getGuild(), "ytsearch:"+getYoutubeQueryOfTrack(playlist.tracks[iTrack]), new SpotifyResultHandler(m,event,playlist,iTrack + 1,prevMessageText,prevErrors,frequentEditAgent)); } @Override