From e6b70f40a70770be0a0489dc6ffe8bee50578372 Mon Sep 17 00:00:00 2001 From: s17herve Date: Wed, 27 Nov 2019 10:17:35 +0100 Subject: [PATCH 1/2] add a new command (playall) --- .../command/music/control/PlayAllCommand.kt | 157 ++++++++++++++++++ .../commandmeta/CommandInitializer.kt | 7 + .../src/main/resources/lang/en_US.properties | 3 + 3 files changed, 167 insertions(+) create mode 100644 FredBoat/src/main/java/fredboat/command/music/control/PlayAllCommand.kt diff --git a/FredBoat/src/main/java/fredboat/command/music/control/PlayAllCommand.kt b/FredBoat/src/main/java/fredboat/command/music/control/PlayAllCommand.kt new file mode 100644 index 000000000..5e2ee7c46 --- /dev/null +++ b/FredBoat/src/main/java/fredboat/command/music/control/PlayAllCommand.kt @@ -0,0 +1,157 @@ +/* + * MIT License + * + * Copyright (c) 2017 Frederik Ar. Mikkelsen + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package fredboat.command.music.control + +import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist +import com.sedmelluq.discord.lavaplayer.track.AudioTrack +import fredboat.audio.player.PlayerLimiter +import fredboat.audio.player.VideoSelectionCache +import fredboat.audio.queue.AudioTrackContext +import fredboat.commandmeta.abs.Command +import fredboat.commandmeta.abs.CommandContext +import fredboat.commandmeta.abs.ICommandRestricted +import fredboat.commandmeta.abs.IMusicCommand +import fredboat.definitions.PermissionLevel +import fredboat.definitions.SearchProvider +import fredboat.main.Launcher +import fredboat.messaging.internal.Context +import fredboat.shared.constant.BotConstants +import fredboat.util.TextUtils +import fredboat.util.extension.edit +import fredboat.util.rest.TrackSearcher +import org.apache.commons.lang3.StringUtils +import org.slf4j.LoggerFactory + +class PlayAllCommand(private val playerLimiter: PlayerLimiter, private val trackSearcher: TrackSearcher, + private val videoSelectionCache: VideoSelectionCache, private val searchProviders: List, + name: String, vararg aliases: String, private val isPriority: Boolean = false +) : Command(name, *aliases), IMusicCommand, ICommandRestricted { + + override val minimumPerms: PermissionLevel + get() = if (isPriority) PermissionLevel.DJ else PermissionLevel.USER + + override suspend fun invoke(context: CommandContext) { + if (context.member.voiceChannel == null) { + context.reply(context.i18n("playerUserNotInChannel")) + return + } + + if (!playerLimiter.checkLimitResponsive(context, Launcher.botController.playerRegistry)) return + + if (!context.hasArguments()) { + context.reply(context.i18n("playAllSearchNotGiven")) + return + } + + var url = StringUtils.strip(context.args[0], "<>") + //Search youtube for videos and play them directly + if (!url.startsWith("http") && !url.startsWith(FILE_PREFIX)) { + searchAndPlayForVideos(context) + return + } + } + + private fun searchAndPlayForVideos(context: CommandContext) { + //Now remove all punctuation + val query = context.rawArgs.replace(TrackSearcher.PUNCTUATION_REGEX.toRegex(), "") + + context.replyMono(context.i18n("playSearching").replace("{q}", query)) + .subscribe{ outMsg -> + val list: AudioPlaylist? + try { + list = trackSearcher.searchForTracks(query, searchProviders) + } catch (e: TrackSearcher.SearchingException) { + context.reply(context.i18n("playYoutubeSearchError")) + log.error("YouTube search exception", e) + return@subscribe + } + + if (list == null || list.tracks.isEmpty()) { + outMsg.edit( + context.textChannel, + context.i18n("playSearchNoResults").replace("{q}", query) + ).subscribe() + + } else { + //Get at most 5 tracks + val selectable = list.tracks.subList(0, Math.min(TrackSearcher.MAX_RESULTS, list.tracks.size)) + + val oldSelection = videoSelectionCache.remove(context.member) + oldSelection?.deleteMessage() + + videoSelectionCache.put(outMsg.messageId, context, selectable, isPriority) + + //Add musics in the queue + val player = Launcher.botController.playerRegistry.getOrCreate(context.guild) + val invoker = context.member + val selection = videoSelectionCache[invoker] + val selectedTracks = arrayOfNulls(TrackSearcher.MAX_RESULTS) + val outputMsgBuilder = StringBuilder() + + if (selection == null) { + outMsg.edit( + context.textChannel, + context.i18n("playSearchNoResults").replace("{q}", query) + ).subscribe() + } else { + + for (i in 0 until TrackSearcher.MAX_RESULTS) { + selectedTracks[i] = selection.choices[i] + + val msg = context.i18nFormat("selectSuccess", (i + 1), + TextUtils.escapeAndDefuse(selectedTracks[i]!!.info.title), + TextUtils.formatTime(selectedTracks[i]!!.info.length)) + + if (i < TrackSearcher.MAX_RESULTS) { + outputMsgBuilder.append("\n") + } + outputMsgBuilder.append(msg) + + player.queue(AudioTrackContext(selectedTracks[i]!!, invoker, selection.isPriority), selection.isPriority) + } + videoSelectionCache.remove(invoker) + + outMsg.edit(context.textChannel, outputMsgBuilder.toString()).subscribe() + + player.setPause(false) + context.deleteMessage() + } + } + } + } + + override fun help(context: Context): String { + val usage = "{0}{1} \n#" + return usage + context.i18nFormat(if (!isPriority) "helpPlayAllCommand" else "helpPlayAllTopCommand", BotConstants.DOCS_URL) + } + + companion object { + + private val log = LoggerFactory.getLogger(PlayAllCommand::class.java) + private val JOIN_COMMAND = JoinCommand("") + private const val FILE_PREFIX = "file://" + } +} \ No newline at end of file diff --git a/FredBoat/src/main/java/fredboat/commandmeta/CommandInitializer.kt b/FredBoat/src/main/java/fredboat/commandmeta/CommandInitializer.kt index 40c466c60..ac7ac3116 100644 --- a/FredBoat/src/main/java/fredboat/commandmeta/CommandInitializer.kt +++ b/FredBoat/src/main/java/fredboat/commandmeta/CommandInitializer.kt @@ -79,6 +79,7 @@ class CommandInitializer(cacheMetrics: CacheMetricsCollector, weather: Weather, const val SOUNDCLOUD_COMM_NAME = "soundcloud" const val PREFIX_COMM_NAME = "prefix" const val PLAY_COMM_NAME = "play" + const val PLAYALL_COMM_NAME = "playall" const val CONFIG_COMM_NAME = "config" const val LANGUAGE_COMM_NAME = "language" } @@ -240,6 +241,12 @@ class CommandInitializer(cacheMetrics: CacheMetricsCollector, weather: Weather, musicModule.registerCommand(PlayCommand(playerLimiter, trackSearcher, videoSelectionCache, listOf(SearchProvider.YOUTUBE, SearchProvider.SOUNDCLOUD), "playnext", "playtop", "pn", isPriority = true)) + musicModule.registerCommand(PlayAllCommand(playerLimiter, trackSearcher, videoSelectionCache, + Arrays.asList(SearchProvider.YOUTUBE, SearchProvider.SOUNDCLOUD), + PLAYALL_COMM_NAME, "pa")) + musicModule.registerCommand(PlayAllCommand(playerLimiter, trackSearcher, videoSelectionCache, + Arrays.asList(SearchProvider.YOUTUBE, SearchProvider.SOUNDCLOUD), + "playalltop", "patop", isPriority = true)) musicModule.registerCommand(PlaySplitCommand(playerLimiter, "split")) musicModule.registerCommand(RepeatCommand("repeat", "rep", "loop")) musicModule.registerCommand(ReshuffleCommand("reshuffle", "resh")) diff --git a/FredBoat/src/main/resources/lang/en_US.properties b/FredBoat/src/main/resources/lang/en_US.properties index 40871fa6f..4b8494c3e 100644 --- a/FredBoat/src/main/resources/lang/en_US.properties +++ b/FredBoat/src/main/resources/lang/en_US.properties @@ -1,6 +1,7 @@ #X-Generator: crowdin.com playQueueEmpty=The player is not currently playing anything. Use the following syntax to add a song\:\n;;play playAlreadyPlaying=The player is already playing. +playAllSearchNotGiven=Don't forget to give a search track. playVCEmpty=There are no users in the voice chat. playWillNowPlay=The player will now play. playSearching=Searching YouTube for `{q}`... @@ -242,6 +243,8 @@ helpPauseCommand=Pause the player. helpPlayCommand=Play music from the given URL or search for a track. For a full list of sources please visit {0} helpPlayNextCommand=Play music from the given URL or search for a track. Tracks are added to the top of the queue. For a full list of sources please visit {0} helpPlaySplitCommand=Split a YouTube video into a tracklist provided in it\'s description. +helpPlayAllCommand=Play all musics from your tracks search. For a full list of sources please visit {0} +helpPlayAllTopCommand=Play all musics from your tracks search. Tracks are added to the top of the queue. For a full list of sources please visit {0} helpRepeatCommand=Toggle between repeat modes. helpReshuffleCommand=Reshuffle the current queue. helpSelectCommand=Select one of the offered tracks after a search to play. From 2f7d58afa9c03250d3c6b73c4399aab6499b6865 Mon Sep 17 00:00:00 2001 From: Maxime Huguet Date: Wed, 18 Dec 2019 17:07:23 +0100 Subject: [PATCH 2/2] Add number of tracks to playall command --- .../command/music/control/PlayAllCommand.kt | 31 +++++++++++++------ .../fredboat/util/rest/TrackSearcher.java | 20 ++++++++++-- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/FredBoat/src/main/java/fredboat/command/music/control/PlayAllCommand.kt b/FredBoat/src/main/java/fredboat/command/music/control/PlayAllCommand.kt index 5e2ee7c46..75a381dec 100644 --- a/FredBoat/src/main/java/fredboat/command/music/control/PlayAllCommand.kt +++ b/FredBoat/src/main/java/fredboat/command/music/control/PlayAllCommand.kt @@ -66,7 +66,7 @@ class PlayAllCommand(private val playerLimiter: PlayerLimiter, private val track return } - var url = StringUtils.strip(context.args[0], "<>") + val url = StringUtils.strip(context.args[0], "<>") //Search youtube for videos and play them directly if (!url.startsWith("http") && !url.startsWith(FILE_PREFIX)) { searchAndPlayForVideos(context) @@ -75,14 +75,28 @@ class PlayAllCommand(private val playerLimiter: PlayerLimiter, private val track } private fun searchAndPlayForVideos(context: CommandContext) { + //Find the number of tracks asked + var nbResults= TrackSearcher.DEFAULT_MAX_RESULTS + var rawArgs = context.rawArgs + + if (context.args[0].matches("[0-9]+".toRegex())){ + nbResults = context.args[0].toInt() + rawArgs = rawArgs.dropWhile { it.isDigit() || it.isWhitespace()} + } + + if (rawArgs.isEmpty()){ + context.reply(context.i18n("playAllSearchNotGiven")) + return + } + //Now remove all punctuation - val query = context.rawArgs.replace(TrackSearcher.PUNCTUATION_REGEX.toRegex(), "") + val query = rawArgs.replace(TrackSearcher.PUNCTUATION_REGEX.toRegex(), "") context.replyMono(context.i18n("playSearching").replace("{q}", query)) .subscribe{ outMsg -> val list: AudioPlaylist? try { - list = trackSearcher.searchForTracks(query, searchProviders) + list = trackSearcher.searchForTracks(query, searchProviders, nbResults) } catch (e: TrackSearcher.SearchingException) { context.reply(context.i18n("playYoutubeSearchError")) log.error("YouTube search exception", e) @@ -96,8 +110,7 @@ class PlayAllCommand(private val playerLimiter: PlayerLimiter, private val track ).subscribe() } else { - //Get at most 5 tracks - val selectable = list.tracks.subList(0, Math.min(TrackSearcher.MAX_RESULTS, list.tracks.size)) + val selectable = list.tracks.subList(0, nbResults.coerceAtMost(list.tracks.size)) val oldSelection = videoSelectionCache.remove(context.member) oldSelection?.deleteMessage() @@ -108,7 +121,7 @@ class PlayAllCommand(private val playerLimiter: PlayerLimiter, private val track val player = Launcher.botController.playerRegistry.getOrCreate(context.guild) val invoker = context.member val selection = videoSelectionCache[invoker] - val selectedTracks = arrayOfNulls(TrackSearcher.MAX_RESULTS) + val selectedTracks = arrayOfNulls(nbResults.coerceAtMost(list.tracks.size)) val outputMsgBuilder = StringBuilder() if (selection == null) { @@ -118,14 +131,14 @@ class PlayAllCommand(private val playerLimiter: PlayerLimiter, private val track ).subscribe() } else { - for (i in 0 until TrackSearcher.MAX_RESULTS) { + for (i in 0 until nbResults.coerceAtMost(list.tracks.size)) { selectedTracks[i] = selection.choices[i] val msg = context.i18nFormat("selectSuccess", (i + 1), TextUtils.escapeAndDefuse(selectedTracks[i]!!.info.title), TextUtils.formatTime(selectedTracks[i]!!.info.length)) - if (i < TrackSearcher.MAX_RESULTS) { + if (i < nbResults.coerceAtMost(list.tracks.size)) { outputMsgBuilder.append("\n") } outputMsgBuilder.append(msg) @@ -144,7 +157,7 @@ class PlayAllCommand(private val playerLimiter: PlayerLimiter, private val track } override fun help(context: Context): String { - val usage = "{0}{1} \n#" + val usage = "{0}{1} [number of tracks] \n#" return usage + context.i18nFormat(if (!isPriority) "helpPlayAllCommand" else "helpPlayAllTopCommand", BotConstants.DOCS_URL) } diff --git a/FredBoat/src/main/java/fredboat/util/rest/TrackSearcher.java b/FredBoat/src/main/java/fredboat/util/rest/TrackSearcher.java index c4cc58b37..187b030a0 100644 --- a/FredBoat/src/main/java/fredboat/util/rest/TrackSearcher.java +++ b/FredBoat/src/main/java/fredboat/util/rest/TrackSearcher.java @@ -26,6 +26,7 @@ package fredboat.util.rest; import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler; +import com.sedmelluq.discord.lavaplayer.player.AudioPlayerLifecycleManager; import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioSourceManager; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; @@ -56,7 +57,8 @@ @Component public class TrackSearcher { - public static final int MAX_RESULTS = 5; + public static final int MAX_RESULTS = 50; + public static final int DEFAULT_MAX_RESULTS = 5; public static final long DEFAULT_CACHE_MAX_AGE = TimeUnit.HOURS.toMillis(48); public static final String PUNCTUATION_REGEX = "[.,/#!$%^&*;:{}=\\-_`~()\"\']"; private static final int DEFAULT_TIMEOUT = 3000; @@ -87,17 +89,29 @@ public AudioPlaylist searchForTracks(String query, List provider return searchForTracks(query, DEFAULT_CACHE_MAX_AGE, DEFAULT_TIMEOUT, providers); } + public AudioPlaylist searchForTracks(String query, List providers, int nb_results) throws SearchingException { + return searchForTracks(query, DEFAULT_CACHE_MAX_AGE, DEFAULT_TIMEOUT, providers, nb_results); + } + + public AudioPlaylist searchForTracks(String query, long cacheMaxAge, int timeoutMillis, List providers) + throws SearchingException { + return searchForTracks(query, cacheMaxAge, timeoutMillis, providers, DEFAULT_MAX_RESULTS); + } + + /** * @param query The search term * @param cacheMaxAge Age of acceptable results from cache. * @param timeoutMillis How long to wait for each lavaplayer search to answer * @param providers Providers that shall be used for the search. They will be used in the order they are provided, the * result of the first successful one will be returned + * @param nbResults Number of results asked * @return The result of the search, or an empty list. * @throws SearchingException If none of the search providers could give us a result, and there was at least one SearchingException thrown by them */ - public AudioPlaylist searchForTracks(String query, long cacheMaxAge, int timeoutMillis, List providers) + public AudioPlaylist searchForTracks(String query, long cacheMaxAge, int timeoutMillis, List providers, int nbResults) throws SearchingException { + nbResults = Math.min(nbResults, MAX_RESULTS); Metrics.searchRequests.inc(); List provs = new ArrayList<>(); @@ -148,7 +162,7 @@ public AudioPlaylist searchForTracks(String query, long cacheMaxAge, int timeout if (provider == SearchProvider.YOUTUBE && (appConfig.isPatronDistribution() || appConfig.isDevDistribution())) { try { - AudioPlaylist youtubeApiResult = youtubeAPI.search(query, MAX_RESULTS, audioPlayerManager.source(YoutubeAudioSourceManager.class)); + AudioPlaylist youtubeApiResult = youtubeAPI.search(query, nbResults, audioPlayerManager.source(YoutubeAudioSourceManager.class)); if (!youtubeApiResult.getTracks().isEmpty()) { log.debug("Loaded search result {} {} from Youtube API", provider, query); // got a search result? cache and return it