diff --git a/src/main/java/com/jagrosh/jmusicbot/BotConfig.java b/src/main/java/com/jagrosh/jmusicbot/BotConfig.java index 9a57beff9..d8b6932e7 100644 --- a/src/main/java/com/jagrosh/jmusicbot/BotConfig.java +++ b/src/main/java/com/jagrosh/jmusicbot/BotConfig.java @@ -16,8 +16,8 @@ package com.jagrosh.jmusicbot; import com.jagrosh.jmusicbot.entities.Prompt; -import com.jagrosh.jmusicbot.utils.FormatUtil; import com.jagrosh.jmusicbot.utils.OtherUtil; +import com.jagrosh.jmusicbot.utils.TimeUtil; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.typesafe.config.*; import java.io.IOException; @@ -336,7 +336,7 @@ public long getMaxSeconds() public String getMaxTime() { - return FormatUtil.formatTime(maxSeconds * 1000); + return TimeUtil.formatTime(maxSeconds * 1000); } public long getAloneTimeUntilStop() diff --git a/src/main/java/com/jagrosh/jmusicbot/JMusicBot.java b/src/main/java/com/jagrosh/jmusicbot/JMusicBot.java index 66071a1fa..30409a6bf 100644 --- a/src/main/java/com/jagrosh/jmusicbot/JMusicBot.java +++ b/src/main/java/com/jagrosh/jmusicbot/JMusicBot.java @@ -199,6 +199,7 @@ private static CommandClient createCommandClient(BotConfig config, SettingsManag new RemoveCmd(bot), new SearchCmd(bot), new SCSearchCmd(bot), + new SeekCmd(bot), new ShuffleCmd(bot), new SkipCmd(bot), diff --git a/src/main/java/com/jagrosh/jmusicbot/audio/AudioHandler.java b/src/main/java/com/jagrosh/jmusicbot/audio/AudioHandler.java index 310389dbe..3875ab3cc 100644 --- a/src/main/java/com/jagrosh/jmusicbot/audio/AudioHandler.java +++ b/src/main/java/com/jagrosh/jmusicbot/audio/AudioHandler.java @@ -18,6 +18,7 @@ import com.jagrosh.jmusicbot.playlist.PlaylistLoader.Playlist; import com.jagrosh.jmusicbot.queue.AbstractQueue; import com.jagrosh.jmusicbot.settings.QueueType; +import com.jagrosh.jmusicbot.utils.TimeUtil; import com.jagrosh.jmusicbot.settings.RepeatMode; import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter; @@ -50,6 +51,7 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler public final static String PAUSE_EMOJI = "\u23F8"; // ⏸ public final static String STOP_EMOJI = "\u23F9"; // ⏹ + private final List defaultQueue = new LinkedList<>(); private final Set votes = new HashSet<>(); @@ -246,7 +248,7 @@ public Message getNowPlaying(JDA jda) double progress = (double)audioPlayer.getPlayingTrack().getPosition()/track.getDuration(); eb.setDescription(getStatusEmoji() + " "+FormatUtil.progressBar(progress) - + " `[" + FormatUtil.formatTime(track.getPosition()) + "/" + FormatUtil.formatTime(track.getDuration()) + "]` " + + " `[" + TimeUtil.formatTime(track.getPosition()) + "/" + TimeUtil.formatTime(track.getDuration()) + "]` " + FormatUtil.volumeIcon(audioPlayer.getVolume())); return mb.setEmbeds(eb.build()).build(); diff --git a/src/main/java/com/jagrosh/jmusicbot/audio/QueuedTrack.java b/src/main/java/com/jagrosh/jmusicbot/audio/QueuedTrack.java index 546d62ef4..81e4f75c2 100644 --- a/src/main/java/com/jagrosh/jmusicbot/audio/QueuedTrack.java +++ b/src/main/java/com/jagrosh/jmusicbot/audio/QueuedTrack.java @@ -15,10 +15,10 @@ */ package com.jagrosh.jmusicbot.audio; +import com.jagrosh.jmusicbot.utils.TimeUtil; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; import com.jagrosh.jmusicbot.queue.Queueable; -import com.jagrosh.jmusicbot.utils.FormatUtil; import net.dv8tion.jda.api.entities.User; /** @@ -28,22 +28,22 @@ public class QueuedTrack implements Queueable { private final AudioTrack track; - - public QueuedTrack(AudioTrack track, User owner) - { - this(track, new RequestMetadata(owner)); - } - + private final RequestMetadata requestMetadata; + public QueuedTrack(AudioTrack track, RequestMetadata rm) { this.track = track; this.track.setUserData(rm == null ? RequestMetadata.EMPTY : rm); + + this.requestMetadata = rm; + if (this.track.isSeekable() && rm != null) + track.setPosition(rm.requestInfo.startTimestamp); } @Override public long getIdentifier() { - return track.getUserData() == null ? 0L : track.getUserData(RequestMetadata.class).getOwner(); + return requestMetadata.getOwner(); } public AudioTrack getTrack() @@ -51,10 +51,15 @@ public AudioTrack getTrack() return track; } + public RequestMetadata getRequestMetadata() + { + return requestMetadata; + } + @Override public String toString() { - String entry = "`[" + FormatUtil.formatTime(track.getDuration()) + "]` "; + String entry = "`[" + TimeUtil.formatTime(track.getDuration()) + "]` "; AudioTrackInfo trackInfo = track.getInfo(); entry = entry + (trackInfo.uri.startsWith("http") ? "[**" + trackInfo.title + "**]("+trackInfo.uri+")" : "**" + trackInfo.title + "**"); return entry + " - <@" + track.getUserData(RequestMetadata.class).getOwner() + ">"; diff --git a/src/main/java/com/jagrosh/jmusicbot/audio/RequestMetadata.java b/src/main/java/com/jagrosh/jmusicbot/audio/RequestMetadata.java index f54ea6b26..3768cb48c 100644 --- a/src/main/java/com/jagrosh/jmusicbot/audio/RequestMetadata.java +++ b/src/main/java/com/jagrosh/jmusicbot/audio/RequestMetadata.java @@ -15,40 +15,67 @@ */ package com.jagrosh.jmusicbot.audio; +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.utils.TimeUtil; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import net.dv8tion.jda.api.entities.User; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * * @author John Grosh (john.a.grosh@gmail.com) */ public class RequestMetadata { - public static final RequestMetadata EMPTY = new RequestMetadata(null); + public static final RequestMetadata EMPTY = new RequestMetadata(null, null); public final UserInfo user; + public final RequestInfo requestInfo; - public RequestMetadata(User user) + public RequestMetadata(User user, RequestInfo requestInfo) { this.user = user == null ? null : new UserInfo(user.getIdLong(), user.getName(), user.getDiscriminator(), user.getEffectiveAvatarUrl()); + this.requestInfo = requestInfo; } public long getOwner() { return user == null ? 0L : user.id; } + + public static RequestMetadata fromResultHandler(AudioTrack track, CommandEvent event) + { + return new RequestMetadata(event.getAuthor(), new RequestInfo(event.getArgs(), track.getInfo().uri)); + } - public class RequestInfo + public static class RequestInfo { public final String query, url; - - private RequestInfo(String query, String url) + public final long startTimestamp; + + public RequestInfo(String query, String url) + { + this(query, url, tryGetTimestamp(query)); + } + + private RequestInfo(String query, String url, long startTimestamp) { - this.query = query; this.url = url; + this.query = query; + this.startTimestamp = startTimestamp; + } + + private static final Pattern youtubeTimestampPattern = Pattern.compile("youtu(?:\\.be|be\\..+)/.*\\?.*(?!.*list=)t=([\\dhms]+)"); + private static long tryGetTimestamp(String url) + { + Matcher matcher = youtubeTimestampPattern.matcher(url); + return matcher.find() ? TimeUtil.parseUnitTime(matcher.group(1)) : 0; } } - public class UserInfo + public static class UserInfo { public final long id; public final String username, discrim, avatar; diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/dj/PlaynextCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/dj/PlaynextCmd.java index 91d79be14..0d5924edc 100644 --- a/src/main/java/com/jagrosh/jmusicbot/commands/dj/PlaynextCmd.java +++ b/src/main/java/com/jagrosh/jmusicbot/commands/dj/PlaynextCmd.java @@ -19,8 +19,10 @@ import com.jagrosh.jmusicbot.Bot; import com.jagrosh.jmusicbot.audio.AudioHandler; import com.jagrosh.jmusicbot.audio.QueuedTrack; +import com.jagrosh.jmusicbot.audio.RequestMetadata; import com.jagrosh.jmusicbot.commands.DJCommand; import com.jagrosh.jmusicbot.utils.FormatUtil; +import com.jagrosh.jmusicbot.utils.TimeUtil; import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; @@ -79,13 +81,13 @@ private void loadSingle(AudioTrack track) if(bot.getConfig().isTooLong(track)) { m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `" - +FormatUtil.formatTime(track.getDuration())+"` > `"+FormatUtil.formatTime(bot.getConfig().getMaxSeconds()*1000)+"`")).queue(); + + TimeUtil.formatTime(track.getDuration())+"` > `"+ TimeUtil.formatTime(bot.getConfig().getMaxSeconds()*1000)+"`")).queue(); return; } AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler(); - int pos = handler.addTrackToFront(new QueuedTrack(track, event.getAuthor()))+1; + int pos = handler.addTrackToFront(new QueuedTrack(track, RequestMetadata.fromResultHandler(track, event)))+1; String addMsg = FormatUtil.filter(event.getClient().getSuccess()+" Added **"+track.getInfo().title - +"** (`"+FormatUtil.formatTime(track.getDuration())+"`) "+(pos==0?"to begin playing":" to the queue at position "+pos)); + +"** (`"+ TimeUtil.formatTime(track.getDuration())+"`) "+(pos==0?"to begin playing":" to the queue at position "+pos)); m.editMessage(addMsg).queue(); } 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 6f9a3d297..67c8c8a1b 100644 --- a/src/main/java/com/jagrosh/jmusicbot/commands/music/PlayCmd.java +++ b/src/main/java/com/jagrosh/jmusicbot/commands/music/PlayCmd.java @@ -15,6 +15,8 @@ */ package com.jagrosh.jmusicbot.commands.music; +import com.jagrosh.jmusicbot.audio.RequestMetadata; +import com.jagrosh.jmusicbot.utils.TimeUtil; import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity; @@ -108,13 +110,13 @@ private void loadSingle(AudioTrack track, AudioPlaylist playlist) if(bot.getConfig().isTooLong(track)) { m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `" - +FormatUtil.formatTime(track.getDuration())+"` > `"+FormatUtil.formatTime(bot.getConfig().getMaxSeconds()*1000)+"`")).queue(); + + TimeUtil.formatTime(track.getDuration())+"` > `"+ TimeUtil.formatTime(bot.getConfig().getMaxSeconds()*1000)+"`")).queue(); return; } AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler(); - int pos = handler.addTrack(new QueuedTrack(track, event.getAuthor()))+1; + int pos = handler.addTrack(new QueuedTrack(track, RequestMetadata.fromResultHandler(track, event)))+1; String addMsg = FormatUtil.filter(event.getClient().getSuccess()+" Added **"+track.getInfo().title - +"** (`"+FormatUtil.formatTime(track.getDuration())+"`) "+(pos==0?"to begin playing":" to the queue at position "+pos)); + +"** (`"+ TimeUtil.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(); else @@ -144,7 +146,7 @@ private int loadPlaylist(AudioPlaylist playlist, AudioTrack exclude) if(!bot.getConfig().isTooLong(track) && !track.equals(exclude)) { AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler(); - handler.addTrack(new QueuedTrack(track, event.getAuthor())); + handler.addTrack(new QueuedTrack(track, RequestMetadata.fromResultHandler(track, event))); count[0]++; } }); @@ -243,7 +245,7 @@ public void doCommand(CommandEvent event) event.getChannel().sendMessage(loadingEmoji+" Loading playlist **"+event.getArgs()+"**... ("+playlist.getItems().size()+" items)").queue(m -> { AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler(); - playlist.loadTracks(bot.getPlayerManager(), (at)->handler.addTrack(new QueuedTrack(at, event.getAuthor())), () -> { + playlist.loadTracks(bot.getPlayerManager(), (at)->handler.addTrack(new QueuedTrack(at, RequestMetadata.fromResultHandler(at, event))), () -> { StringBuilder builder = new StringBuilder(playlist.getTracks().isEmpty() ? event.getClient().getWarning()+" No tracks were loaded!" : event.getClient().getSuccess()+" Loaded **"+playlist.getTracks().size()+"** tracks!"); diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/music/QueueCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/music/QueueCmd.java index 2b18aaf4f..351681294 100644 --- a/src/main/java/com/jagrosh/jmusicbot/commands/music/QueueCmd.java +++ b/src/main/java/com/jagrosh/jmusicbot/commands/music/QueueCmd.java @@ -27,6 +27,7 @@ import com.jagrosh.jmusicbot.settings.RepeatMode; import com.jagrosh.jmusicbot.settings.Settings; import com.jagrosh.jmusicbot.utils.FormatUtil; +import com.jagrosh.jmusicbot.utils.TimeUtil; import net.dv8tion.jda.api.MessageBuilder; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.Message; @@ -112,7 +113,7 @@ private String getQueueTitle(AudioHandler ah, String success, int songslength, l .append(ah.getPlayer().getPlayingTrack().getInfo().title).append("**\n"); } return FormatUtil.filter(sb.append(success).append(" Current Queue | ").append(songslength) - .append(" entries | `").append(FormatUtil.formatTime(total)).append("` ") + .append(" entries | `").append(TimeUtil.formatTime(total)).append("` ") .append("| ").append(queueType.getEmoji()).append(" `").append(queueType.getUserFriendlyName()).append('`') .append(repeatmode.getEmoji() != null ? " | "+repeatmode.getEmoji() : "").toString()); } diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/music/SearchCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/music/SearchCmd.java index 9358698aa..265be6045 100644 --- a/src/main/java/com/jagrosh/jmusicbot/commands/music/SearchCmd.java +++ b/src/main/java/com/jagrosh/jmusicbot/commands/music/SearchCmd.java @@ -15,6 +15,8 @@ */ package com.jagrosh.jmusicbot.commands.music; +import com.jagrosh.jmusicbot.audio.RequestMetadata; +import com.jagrosh.jmusicbot.utils.TimeUtil; import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity; @@ -88,13 +90,13 @@ public void trackLoaded(AudioTrack track) if(bot.getConfig().isTooLong(track)) { m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `" - +FormatUtil.formatTime(track.getDuration())+"` > `"+bot.getConfig().getMaxTime()+"`")).queue(); + + TimeUtil.formatTime(track.getDuration())+"` > `"+bot.getConfig().getMaxTime()+"`")).queue(); return; } AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler(); - int pos = handler.addTrack(new QueuedTrack(track, event.getAuthor()))+1; + int pos = handler.addTrack(new QueuedTrack(track, RequestMetadata.fromResultHandler(track, event)))+1; m.editMessage(FormatUtil.filter(event.getClient().getSuccess()+" Added **"+track.getInfo().title - +"** (`"+FormatUtil.formatTime(track.getDuration())+"`) "+(pos==0 ? "to begin playing" + +"** (`"+ TimeUtil.formatTime(track.getDuration())+"`) "+(pos==0 ? "to begin playing" : " to the queue at position "+pos))).queue(); } @@ -110,13 +112,13 @@ public void playlistLoaded(AudioPlaylist playlist) if(bot.getConfig().isTooLong(track)) { event.replyWarning("This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `" - +FormatUtil.formatTime(track.getDuration())+"` > `"+bot.getConfig().getMaxTime()+"`"); + + TimeUtil.formatTime(track.getDuration())+"` > `"+bot.getConfig().getMaxTime()+"`"); return; } AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler(); - int pos = handler.addTrack(new QueuedTrack(track, event.getAuthor()))+1; + int pos = handler.addTrack(new QueuedTrack(track, RequestMetadata.fromResultHandler(track, event)))+1; event.replySuccess("Added **" + FormatUtil.filter(track.getInfo().title) - + "** (`" + FormatUtil.formatTime(track.getDuration()) + "`) " + (pos==0 ? "to begin playing" + + "** (`" + TimeUtil.formatTime(track.getDuration()) + "`) " + (pos==0 ? "to begin playing" : " to the queue at position "+pos)); }) .setCancel((msg) -> {}) @@ -125,7 +127,7 @@ public void playlistLoaded(AudioPlaylist playlist) for(int i=0; i<4 && i. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jagrosh.jmusicbot.commands.music; + +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.audio.AudioHandler; +import com.jagrosh.jmusicbot.commands.DJCommand; +import com.jagrosh.jmusicbot.commands.MusicCommand; +import com.jagrosh.jmusicbot.utils.TimeUtil; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; + + +/** + * @author Whew., Inc. + */ +public class SeekCmd extends MusicCommand +{ + public SeekCmd(Bot bot) + { + super(bot); + this.name = "seek"; + this.help = "seeks the current song"; + this.arguments = "[+ | -] |<0h0m0s | 0m0s | 0s>"; + this.aliases = bot.getConfig().getAliases(this.name); + this.beListening = true; + this.bePlaying = true; + } + + @Override + public void doCommand(CommandEvent event) + { + AudioHandler handler = (AudioHandler) event.getGuild().getAudioManager().getSendingHandler(); + AudioTrack playingTrack = handler.getPlayer().getPlayingTrack(); + if (!playingTrack.isSeekable()) + { + event.replyError("This track is not seekable."); + return; + } + + + if (!DJCommand.checkDJPermission(event) && playingTrack.getUserData(Long.class) != event.getAuthor().getIdLong()) + { + event.replyError("You cannot seek **" + playingTrack.getInfo().title + "** because you didn't add it!"); + return; + } + + String args = event.getArgs(); + TimeUtil.SeekTime seekTime = TimeUtil.parseTime(args); + if (seekTime == null) + { + event.replyError("Invalid seek! Expected format: " + arguments + "\nExamples: `1:02:23` `+1:10` `-90`, `1h10m`, `+90s`"); + return; + } + + long currentPosition = playingTrack.getPosition(); + long trackDuration = playingTrack.getDuration(); + + long seekMilliseconds = seekTime.relative ? currentPosition + seekTime.milliseconds : seekTime.milliseconds; + if (seekMilliseconds > trackDuration) + { + event.replyError("Cannot seek to `" + TimeUtil.formatTime(seekMilliseconds) + "` because the current track is `" + TimeUtil.formatTime(trackDuration) + "` long!"); + } + else + { + try + { + playingTrack.setPosition(seekMilliseconds); + } + catch (Exception e) + { + event.replyError("An error occurred while trying to seek!"); + e.printStackTrace(); + return; + } + } + event.replySuccess("Successfully seeked to `" + TimeUtil.formatTime(playingTrack.getPosition()) + "/" + TimeUtil.formatTime(playingTrack.getDuration()) + "`!"); + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/utils/FormatUtil.java b/src/main/java/com/jagrosh/jmusicbot/utils/FormatUtil.java index 709be7437..e840a9f21 100644 --- a/src/main/java/com/jagrosh/jmusicbot/utils/FormatUtil.java +++ b/src/main/java/com/jagrosh/jmusicbot/utils/FormatUtil.java @@ -27,41 +27,29 @@ * @author John Grosh */ public class FormatUtil { - - public static String formatTime(long duration) - { - if(duration == Long.MAX_VALUE) - return "LIVE"; - long seconds = Math.round(duration/1000.0); - long hours = seconds/(60*60); - seconds %= 60*60; - long minutes = seconds/60; - seconds %= 60; - return (hours>0 ? hours+":" : "") + (minutes<10 ? "0"+minutes : minutes) + ":" + (seconds<10 ? "0"+seconds : seconds); - } - public static String formatUsername(String username, String discrim) + public static String formatUsername(String username, String discrim) { - if(discrim == null || discrim.equals("0000")) + if(discrim == null || discrim.equals("0000")) { return username; } - else + else { return username + "#" + discrim; } } - + public static String formatUsername(UserInfo userinfo) { return formatUsername(userinfo.username, userinfo.discrim); } - + public static String formatUsername(User user) { return formatUsername(user.getName(), user.getDiscriminator()); } - + public static String progressBar(double percent) { String str = ""; diff --git a/src/main/java/com/jagrosh/jmusicbot/utils/TimeUtil.java b/src/main/java/com/jagrosh/jmusicbot/utils/TimeUtil.java new file mode 100644 index 000000000..081118713 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/utils/TimeUtil.java @@ -0,0 +1,138 @@ +/* + * Copyright 2020 John Grosh . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jagrosh.jmusicbot.utils; + +public class TimeUtil +{ + + public static String formatTime(long duration) + { + if(duration == Long.MAX_VALUE) + return "LIVE"; + long seconds = Math.round(duration/1000.0); + long hours = seconds/(60*60); + seconds %= 60*60; + long minutes = seconds/60; + seconds %= 60; + return (hours>0 ? hours+":" : "") + (minutes<10 ? "0"+minutes : minutes) + ":" + (seconds<10 ? "0"+seconds : seconds); + } + + /** + * Parses a seek time string into milliseconds and determines if it's relative. + * Supports "colon time" (HH:MM:SS) or "unit time" (1h20m) + * @param args time string + * @return SeekTime object, or null if the string could not be parsed + */ + public static SeekTime parseTime(String args) + { + if (args.length() == 0) return null; + String timestamp = args; + boolean relative = false; // seek forward or backward + boolean isSeekingBackwards = false; + char first = timestamp.charAt(0); + if (first == '+' || first == '-') + { + relative = true; + isSeekingBackwards = first == '-'; + timestamp = timestamp.substring(1); + } + + long milliseconds = parseColonTime(timestamp); + if(milliseconds == -1) milliseconds = parseUnitTime(timestamp); + if(milliseconds == -1) return null; + + milliseconds *= isSeekingBackwards ? -1 : 1; + + return new SeekTime(milliseconds, relative); + } + + /** + * @param timestamp timestamp formatted as: [+ | -] <HH:MM:SS | MM:SS | SS> + * @return Time in milliseconds + */ + public static long parseColonTime(String timestamp) + { + String[] timestampSplitArray = timestamp.split(":+"); + if(timestampSplitArray.length > 3 ) + return -1; + double[] timeUnitArray = new double[3]; // hours, minutes, seconds + for(int index = 0; index < timestampSplitArray.length; index++) + { + String unit = timestampSplitArray[index]; + if (unit.startsWith("+") || unit.startsWith("-")) return -1; + unit = unit.replace(",", "."); + try + { + timeUnitArray[index + 3 - timestampSplitArray.length] = Double.parseDouble(unit); + } + catch (NumberFormatException e) + { + return -1; + } + } + return Math.round(timeUnitArray[0] * 3600000 + timeUnitArray[1] * 60000 + timeUnitArray[2] * 1000); + } + + /** + * + * @param timestr time string formatted as a unit time, e.g. 20m10, 1d5h20m14s or 1h and 20m + * @return Time in milliseconds + */ + public static long parseUnitTime(String timestr) + { + timestr = timestr.replaceAll("(?i)(\\s|,|and)","") + .replaceAll("(?is)(-?\\d+|[a-z]+)", "$1 ") + .trim(); + String[] vals = timestr.split("\\s+"); + int time = 0; + try + { + for(int j=0; j j+1) + { + if(vals[j+1].toLowerCase().startsWith("m")) + num*=60; + else if(vals[j+1].toLowerCase().startsWith("h")) + num*=60*60; + else if(vals[j+1].toLowerCase().startsWith("d")) + num*=60*60*24; + } + + time+=num*1000; + } + } + catch(Exception ex) + { + return -1; + } + return time; + } + + public static class SeekTime + { + public final long milliseconds; + public final boolean relative; + + private SeekTime(long milliseconds, boolean relative) + { + this.milliseconds = milliseconds; + this.relative = relative; + } + } +} diff --git a/src/test/java/com/jagrosh/jmusicbot/TimeUtilTest.java b/src/test/java/com/jagrosh/jmusicbot/TimeUtilTest.java new file mode 100644 index 000000000..781b9046d --- /dev/null +++ b/src/test/java/com/jagrosh/jmusicbot/TimeUtilTest.java @@ -0,0 +1,119 @@ +/* + * Copyright 2020 John Grosh . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jagrosh.jmusicbot; + + +import com.jagrosh.jmusicbot.utils.TimeUtil; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * @author Whew., Inc. + */ +public class TimeUtilTest +{ + @Test + public void singleDigit() + { + TimeUtil.SeekTime seek = TimeUtil.parseTime("5"); + assertNotNull(seek); + assertEquals(5000, seek.milliseconds); + } + + @Test + public void multipleDigits() + { + TimeUtil.SeekTime seek = TimeUtil.parseTime("99:9:999"); + assertNotNull(seek); + assertEquals(357939000, seek.milliseconds); + + seek = TimeUtil.parseTime("99h9m999s"); + assertNotNull(seek); + assertEquals(357939000, seek.milliseconds); + } + + @Test + public void decimalDigits() + { + TimeUtil.SeekTime seek = TimeUtil.parseTime("99.5:9.0:999.777"); + assertNotNull(seek); + assertEquals(359739777, seek.milliseconds); + } + + @Test + public void seeking() + { + TimeUtil.SeekTime seek = TimeUtil.parseTime("5"); + assertNotNull(seek); + assertFalse(seek.relative); + assertEquals(5000, seek.milliseconds); + } + + @Test + public void relativeSeekingForward() + { + TimeUtil.SeekTime seek = TimeUtil.parseTime("+5"); + assertNotNull(seek); + assertTrue(seek.relative); + assertEquals(5000, seek.milliseconds); + } + + @Test + public void relativeSeekingBackward() + { + TimeUtil.SeekTime seek = TimeUtil.parseTime("-5"); + assertNotNull(seek); + assertTrue(seek.relative); + assertEquals(-5000, seek.milliseconds); + } + + @Test + public void parseTimeArgumentLength() + { + TimeUtil.SeekTime seek = TimeUtil.parseTime(""); + assertNull(seek); + } + + @Test + public void timestampTotalUnits() + { + TimeUtil.SeekTime seek = TimeUtil.parseTime("1:1:1:1"); + assertNull(seek); + + seek = TimeUtil.parseTime("1h2m3m4s5s"); + assertNotNull(seek); + assertEquals(3909000, seek.milliseconds); + } + + @Test + public void relativeSymbol() + { + TimeUtil.SeekTime seek = TimeUtil.parseTime("+-1:-+1:+-1"); + assertNull(seek); + } + + @Test + public void timestampNumberFormat() + { + TimeUtil.SeekTime seek = TimeUtil.parseTime("1:1:a"); + assertNull(seek); + + seek = TimeUtil.parseTime("1a2s"); + assertNotNull(seek); + assertEquals(3000, seek.milliseconds); + } +}