From d873d2f79666ae7a14a3eea06bb56d603e97565b Mon Sep 17 00:00:00 2001 From: DaniDipp Date: Sat, 13 Jul 2024 15:25:41 +0000 Subject: [PATCH] Squashed commit of the following: commit 8733899aa0121ea986853740bf34ab9ee8e0f593 Author: John Grosh Date: Wed Jul 10 17:35:50 2024 -0400 Update bug-report.yml commit e5fd05e6011d43fb8220b75e6e5005758c7b7228 Author: Moritz Bender <35152647+Morilli@users.noreply.github.com> Date: Wed Jul 10 13:19:13 2024 +0200 Update dependencies to fix youtube issues (#1609) commit 6a5a9c7a754e2c1632fd034c584bd97d74f3a711 Author: John Grosh Date: Fri May 10 17:43:49 2024 -0400 Change playlist page count (#1542) * change playlist page count * make the value configurable * small refactor commit 8557f7ab8ef6ec6c59e37b9c6ce88ad6fb55a308 Author: Michaili K Date: Fri May 10 21:36:43 2024 +0200 Log track exceptions in the audio handler (#1558) commit 48e62f1a50080270ffdd04dedd45674a8120b8a6 Author: Michaili K Date: Fri May 10 21:32:30 2024 +0200 Revert "Always self-deafen (#1491)" (#1551) This reverts commit a7807b96e2684bc3fc77d84b602dfac6c7573842. commit 6f12c33781aed8c1e7229c87c8b5ac2165e7255b Author: John Grosh Date: Fri May 10 15:23:30 2024 -0400 custom eval engine (#1530) commit 0afb3db26938b12858ee3e8dcf15433a830d0cc9 Author: Michaili K Date: Fri May 10 20:19:11 2024 +0200 Switch to lavalink's new YouTube audio source manager (#1552) * Switch to new lavalink's new YouTube audio source manager * Add back setPlaylistPageCount call for the youtube audio source manager * Manually add all the individual audio sources EXCEPT for old youtube * Remove unused import * Upgrade lavaplayer-youtube-source to 1.0.3 * Upgrade lavaplayer-youtube-source to 1.0.4 commit 81322ef993e2d6c23e5731bbe80abf518aa6b274 Author: Michaili K Date: Fri May 10 20:14:28 2024 +0200 Add seek command (supersedes #344) (#674) * Add seek command * Combine nested if statements * Put the seek command in correct place to keep alphabetical order * Add license header * Brackets on next line * Restructure if-statements * Check for permissions with DJCommand#checkDJPermission * Make regex slightly smaller * Optimize imports * Output length of current track if requested seek time is invalid * Restate seeked point when seeked successfully * Add empty newline at end of file to keep consistency * Create TimeUtil class for parsing and formatting time * Move FormatUtil#formatTime to TimeUtil, and refactor * Apply requested changes (Pass 2) * Seek based on current position in track * Apply requested changes (Pass 3) * Add javadoc param * Apply requested changes (Pass 4) * Fix merge * Avoid reassigning parameter (Codacy) * Rework timestamp parsing * Refactor timestamp parsing * Apply requested changes (Pass 5) * Add examples in help * Apply requested changes (Pass 6) * Fix missing import * Keep track of start timestamp & add "unit" times * Fix my abdominal merge with QueuedTrack * Use RequestMetadata to store start timestamp * Store request info in request metadata * Add regex to try getting a timestamp from the url * Require RequestMetadata for QueuedTracks * Add some unit tests for unit seeking * Add docs & examples --------- Co-authored-by: Whew., Inc <22574706+Whew-Inc@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/bug-report.yml | 9 +- pom.xml | 11 +- .../java/com/jagrosh/jmusicbot/BotConfig.java | 20 ++- .../java/com/jagrosh/jmusicbot/JMusicBot.java | 1 + .../jagrosh/jmusicbot/audio/AudioHandler.java | 12 +- .../jmusicbot/audio/PlayerManager.java | 28 +++- .../jagrosh/jmusicbot/audio/QueuedTrack.java | 23 +-- .../jmusicbot/audio/RequestMetadata.java | 41 +++++- .../TransformativeAudioSourceManager.java | 2 +- .../jmusicbot/commands/dj/PlaynextCmd.java | 8 +- .../jmusicbot/commands/music/PlayCmd.java | 12 +- .../jmusicbot/commands/music/QueueCmd.java | 3 +- .../jmusicbot/commands/music/SearchCmd.java | 16 +- .../jmusicbot/commands/music/SeekCmd.java | 92 ++++++++++++ .../jmusicbot/commands/owner/EvalCmd.java | 10 +- .../jagrosh/jmusicbot/utils/FormatUtil.java | 24 +-- .../jmusicbot/utils/LogBackTurboFilter.java | 44 ------ .../com/jagrosh/jmusicbot/utils/TimeUtil.java | 138 ++++++++++++++++++ src/main/resources/logback.xml | 2 - src/main/resources/reference.conf | 10 ++ .../com/jagrosh/jmusicbot/TimeUtilTest.java | 119 +++++++++++++++ 21 files changed, 516 insertions(+), 109 deletions(-) create mode 100644 src/main/java/com/jagrosh/jmusicbot/commands/music/SeekCmd.java delete mode 100644 src/main/java/com/jagrosh/jmusicbot/utils/LogBackTurboFilter.java create mode 100644 src/main/java/com/jagrosh/jmusicbot/utils/TimeUtil.java create mode 100644 src/test/java/com/jagrosh/jmusicbot/TimeUtilTest.java diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 2d2d62d04..c8ea39510 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -53,5 +53,12 @@ body: required: true - label: I have searched for similar issues [on the issues page](https://github.com/jagrosh/MusicBot/issues?q=is%3Aissue) required: true - - label: "I am running the latest version of the bot: ![Release](https://img.shields.io/github/release/jagrosh/MusicBot.svg)" + - label: I am using an official release from [the releases page](https://github.com/jagrosh/MusicBot/releases) required: true + - type: input + id: version + attributes: + label: JMusicBot Version + description: Which version of JMusicBot are you running? + validations: + required: true diff --git a/pom.xml b/pom.xml index efcbf9282..b6a5f99c1 100644 --- a/pom.xml +++ b/pom.xml @@ -33,9 +33,9 @@ https://m2.duncte123.dev/releases - arbjergDev-snapshots + arbjergDev Lavalink Repository - https://maven.lavalink.dev/snapshots + https://maven.lavalink.dev/releases @@ -57,7 +57,12 @@ dev.arbjerg lavaplayer - 0eaeee195f0315b2617587aa3537fa202df07ddc-SNAPSHOT + 2.2.1 + + + dev.lavalink.youtube + common + 1.4.0 com.github.jagrosh diff --git a/src/main/java/com/jagrosh/jmusicbot/BotConfig.java b/src/main/java/com/jagrosh/jmusicbot/BotConfig.java index 9a57beff9..f6a8757ac 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; @@ -40,9 +40,11 @@ public class BotConfig private Path path = null; private String token, prefix, altprefix, helpWord, playlistsFolder, logLevel, - successEmoji, warningEmoji, errorEmoji, loadingEmoji, searchingEmoji; + successEmoji, warningEmoji, errorEmoji, loadingEmoji, searchingEmoji, + evalEngine; private boolean stayInChannel, songInGame, npImages, updatealerts, useEval, dbots; private long owner, maxSeconds, aloneTimeUntilStop; + private int maxYTPlaylistPages; private double skipratio; private OnlineStatus status; private Activity game; @@ -88,7 +90,9 @@ public void load() updatealerts = config.getBoolean("updatealerts"); logLevel = config.getString("loglevel"); useEval = config.getBoolean("eval"); + evalEngine = config.getString("evalengine"); maxSeconds = config.getLong("maxtime"); + maxYTPlaylistPages = config.getInt("maxytplaylistpages"); aloneTimeUntilStop = config.getLong("alonetimeuntilstop"); playlistsFolder = config.getString("playlistsfolder"); aliases = config.getConfig("aliases"); @@ -324,6 +328,11 @@ public boolean useEval() return useEval; } + public String getEvalEngine() + { + return evalEngine; + } + public boolean useNPImages() { return npImages; @@ -334,9 +343,14 @@ public long getMaxSeconds() return maxSeconds; } + public int getMaxYTPlaylistPages() + { + return maxYTPlaylistPages; + } + 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 f1e866bb9..262ef1ecb 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), new TiliiCmd(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..aa8305261 100644 --- a/src/main/java/com/jagrosh/jmusicbot/audio/AudioHandler.java +++ b/src/main/java/com/jagrosh/jmusicbot/audio/AudioHandler.java @@ -18,9 +18,12 @@ 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.jagrosh.jmusicbot.utils.OtherUtil; import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter; +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason; import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrame; @@ -39,6 +42,7 @@ import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.User; +import org.slf4j.LoggerFactory; /** * @@ -50,6 +54,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<>(); @@ -197,6 +202,11 @@ public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason } } + @Override + public void onTrackException(AudioPlayer player, AudioTrack track, FriendlyException exception) { + LoggerFactory.getLogger("AudioHandler").error("Track " + track.getIdentifier() + " has failed to play", exception); + } + @Override public void onTrackStart(AudioPlayer player, AudioTrack track) { @@ -246,7 +256,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/PlayerManager.java b/src/main/java/com/jagrosh/jmusicbot/audio/PlayerManager.java index f4a5605ce..56999a92b 100644 --- a/src/main/java/com/jagrosh/jmusicbot/audio/PlayerManager.java +++ b/src/main/java/com/jagrosh/jmusicbot/audio/PlayerManager.java @@ -17,10 +17,19 @@ import com.dunctebot.sourcemanagers.DuncteBotSources; import com.jagrosh.jmusicbot.Bot; +import com.sedmelluq.discord.lavaplayer.container.MediaContainerRegistry; import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager; import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers; -import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioSourceManager; +import com.sedmelluq.discord.lavaplayer.source.bandcamp.BandcampAudioSourceManager; +import com.sedmelluq.discord.lavaplayer.source.beam.BeamAudioSourceManager; +import com.sedmelluq.discord.lavaplayer.source.getyarn.GetyarnAudioSourceManager; +import com.sedmelluq.discord.lavaplayer.source.http.HttpAudioSourceManager; +import com.sedmelluq.discord.lavaplayer.source.nico.NicoAudioSourceManager; +import com.sedmelluq.discord.lavaplayer.source.soundcloud.SoundCloudAudioSourceManager; +import com.sedmelluq.discord.lavaplayer.source.twitch.TwitchStreamAudioSourceManager; +import com.sedmelluq.discord.lavaplayer.source.vimeo.VimeoAudioSourceManager; +import dev.lavalink.youtube.YoutubeAudioSourceManager; import net.dv8tion.jda.api.entities.Guild; /** @@ -39,10 +48,23 @@ public PlayerManager(Bot bot) public void init() { TransformativeAudioSourceManager.createTransforms(bot.getConfig().getTransforms()).forEach(t -> registerSourceManager(t)); - AudioSourceManagers.registerRemoteSources(this); + + YoutubeAudioSourceManager yt = new YoutubeAudioSourceManager(true); + yt.setPlaylistPageCount(bot.getConfig().getMaxYTPlaylistPages()); + registerSourceManager(yt); + + registerSourceManager(SoundCloudAudioSourceManager.createDefault()); + registerSourceManager(new BandcampAudioSourceManager()); + registerSourceManager(new VimeoAudioSourceManager()); + registerSourceManager(new TwitchStreamAudioSourceManager()); + registerSourceManager(new BeamAudioSourceManager()); + registerSourceManager(new GetyarnAudioSourceManager()); + registerSourceManager(new NicoAudioSourceManager()); + registerSourceManager(new HttpAudioSourceManager(MediaContainerRegistry.DEFAULT_REGISTRY)); + AudioSourceManagers.registerLocalSource(this); + DuncteBotSources.registerAll(this, "en-US"); - source(YoutubeAudioSourceManager.class).setPlaylistPageCount(10); } public Bot getBot() 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/audio/TransformativeAudioSourceManager.java b/src/main/java/com/jagrosh/jmusicbot/audio/TransformativeAudioSourceManager.java index dc57f6476..7e4734d69 100644 --- a/src/main/java/com/jagrosh/jmusicbot/audio/TransformativeAudioSourceManager.java +++ b/src/main/java/com/jagrosh/jmusicbot/audio/TransformativeAudioSourceManager.java @@ -16,10 +16,10 @@ package com.jagrosh.jmusicbot.audio; import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; -import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioSourceManager; import com.sedmelluq.discord.lavaplayer.track.AudioItem; import com.sedmelluq.discord.lavaplayer.track.AudioReference; import com.typesafe.config.Config; +import dev.lavalink.youtube.YoutubeAudioSourceManager; import java.io.IOException; import java.util.Collections; import java.util.List; 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/commands/owner/EvalCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/owner/EvalCmd.java index 1a7f0294c..8143f078c 100644 --- a/src/main/java/com/jagrosh/jmusicbot/commands/owner/EvalCmd.java +++ b/src/main/java/com/jagrosh/jmusicbot/commands/owner/EvalCmd.java @@ -29,6 +29,7 @@ public class EvalCmd extends OwnerCommand { private final Bot bot; + private final String engine; public EvalCmd(Bot bot) { @@ -36,13 +37,20 @@ public EvalCmd(Bot bot) this.name = "eval"; this.help = "evaluates nashorn code"; this.aliases = bot.getConfig().getAliases(this.name); + this.engine = bot.getConfig().getEvalEngine(); this.guildOnly = false; } @Override protected void execute(CommandEvent event) { - ScriptEngine se = new ScriptEngineManager().getEngineByName("Nashorn"); + ScriptEngine se = new ScriptEngineManager().getEngineByName(engine); + if(se == null) + { + event.replyError("The eval engine provided in the config (`"+engine+"`) doesn't exist. This could be due to an invalid " + + "engine name, or the engine not existing in your version of java (`"+System.getProperty("java.version")+"`)."); + return; + } se.put("bot", bot); se.put("event", event); se.put("jda", event.getJDA()); 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/LogBackTurboFilter.java b/src/main/java/com/jagrosh/jmusicbot/utils/LogBackTurboFilter.java deleted file mode 100644 index c991a07ec..000000000 --- a/src/main/java/com/jagrosh/jmusicbot/utils/LogBackTurboFilter.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2024 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; - -import ch.qos.logback.classic.Level; -import ch.qos.logback.classic.Logger; -import ch.qos.logback.classic.turbo.TurboFilter; -import ch.qos.logback.core.spi.FilterReply; -import org.slf4j.Marker; - -/** - * A TurboFilter, currently only used to suppress specific log messages from libraries. - * - * @author Michaili K. - */ -public class LogBackTurboFilter extends TurboFilter -{ - @Override - public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) - { - // Suppresses the auth token warning from the YoutubeAudioSourceManager - // https://github.com/jagrosh/MusicBot/pull/1490#issuecomment-1974070225 - if (logger.getName().equals("com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAccessTokenTracker") - && format.equals("YouTube auth tokens can't be retrieved because email and password is not set in YoutubeAudioSourceManager, age restricted videos will throw exceptions.") - ) { - return FilterReply.DENY; - } - - return FilterReply.NEUTRAL; - } -} 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/main/resources/logback.xml b/src/main/resources/logback.xml index 1d03fcc22..4a7033f5d 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -14,6 +14,4 @@ - - diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf index 19206d3ab..a4fda1e02 100644 --- a/src/main/resources/reference.conf +++ b/src/main/resources/reference.conf @@ -98,6 +98,15 @@ stayinchannel = false maxtime = 0 +// This sets the maximum number of pages of songs that can be loaded from a YouTube +// playlist. Each page can contain up to 100 tracks. Playing a playlist with more +// pages than the maximum will stop loading after the provided number of pages. +// For example, if the max was set to 15 and a playlist contained 1850 tracks, +// only the first 1500 tracks (15 pages) would be loaded. By default, this is +// set to 10 pages (1000 tracks). + +maxytplaylistpages = 10 + // This sets the ratio of users that must vote to skip the currently playing song. // Guild owners can define their own skip ratios, but this will be used if a guild @@ -215,6 +224,7 @@ transforms = {} // IF SOMEONE ASKS YOU TO ENABLE THIS, THERE IS AN 11/10 CHANCE THEY ARE TRYING TO SCAM YOU eval=false +evalengine="Nashorn" /// END OF JMUSICBOT CONFIG /// 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); + } +}