Compare commits
64 Commits
crossfadin
...
master
@ -1,2 +1,11 @@ |
||||
# Chords |
||||
# Chords - Yet Another Discord Music Bot |
||||
|
||||
![The Chords logo - a white guitar with the word "Chords" in bubble font, on a blue background.](chords.png) |
||||
|
||||
Basically everyone has written one at this point, but our little community needed one, so I made one! |
||||
|
||||
Here's some stuff about it: |
||||
|
||||
- Can play from any site that youtube-dl supports ([that's over 1800!](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md)) |
||||
- Built-in multi-site searching via my own [MusicSearcher](https://fluff.nekojimi.moe/gitea/Nekojimi/MusicSearcher) project |
||||
- More cool stuff coming soon! |
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
@ -0,0 +1,3 @@ |
||||
ytdl-cmd: /usr/bin/yt-dlp |
||||
discord-token: ODkwNjU5MjI2NDE2OTg0MDY0.YUzBCw.jHZWpIZSYeaYA7Sc7h93W_jV-rk |
||||
local-addr: 192.168.1.84 |
@ -0,0 +1,556 @@ |
||||
/* |
||||
* To change this license header, choose License Headers in Project Properties. |
||||
* To change this template file, choose Tools | Templates |
||||
* and open the template in the editor. |
||||
*/ |
||||
package moe.nekojimi.chords; |
||||
|
||||
import com.amihaiemil.eoyaml.Yaml; |
||||
import com.amihaiemil.eoyaml.YamlMapping; |
||||
import com.beust.jcommander.JCommander; |
||||
import com.neovisionaries.ws.client.WebSocketFactory; |
||||
import java.io.File; |
||||
import java.io.IOException; |
||||
import java.net.*; |
||||
import java.nio.file.Files; |
||||
import java.time.LocalDateTime; |
||||
import java.time.format.DateTimeFormatter; |
||||
import java.util.*; |
||||
import java.util.function.BiConsumer; |
||||
import java.util.function.Consumer; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
import javax.security.auth.login.LoginException; |
||||
import moe.nekojimi.chords.commands.*; |
||||
import moe.nekojimi.musicsearcher.providers.MetaSearcher; |
||||
import moe.nekojimi.musicsearcher.providers.Searcher; |
||||
import net.dv8tion.jda.api.JDA; |
||||
import net.dv8tion.jda.api.JDABuilder; |
||||
import net.dv8tion.jda.api.entities.*; |
||||
import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel; |
||||
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent; |
||||
import net.dv8tion.jda.api.events.message.MessageEmbedEvent; |
||||
import net.dv8tion.jda.api.events.message.MessageReceivedEvent; |
||||
import net.dv8tion.jda.api.hooks.ListenerAdapter; |
||||
import net.dv8tion.jda.api.managers.AudioManager; |
||||
import net.dv8tion.jda.api.requests.GatewayIntent; |
||||
import net.dv8tion.jda.api.utils.Compression; |
||||
import net.dv8tion.jda.api.utils.cache.CacheFlag; |
||||
import net.dv8tion.jda.internal.utils.IOUtil; |
||||
import okhttp3.OkHttpClient; |
||||
|
||||
/** |
||||
* The Chords music Discord bot. |
||||
* |
||||
* @author Nekojimi |
||||
*/ |
||||
public final class Chords extends ListenerAdapter |
||||
{ |
||||
|
||||
private static final CommandOptions options = new CommandOptions(); |
||||
private static Settings settings; |
||||
|
||||
private final File dataDirectory; |
||||
private final File playlistsDirectory; |
||||
|
||||
private MusicHandler musicHandler; |
||||
private final Downloader downloader; |
||||
private final Searcher searcher; |
||||
private final QueueManager queueManager; |
||||
// private Recommender recommender;
|
||||
private JDA jda; |
||||
|
||||
private final Map<String, Command> commands = new HashMap<>(); |
||||
private final Command helpCommand; |
||||
private PlayCommand playCommand; |
||||
|
||||
private AudioChannel currentVoiceChannel = null; |
||||
|
||||
private final Map<String, Playlist> playlists = new HashMap<>(); |
||||
|
||||
/** |
||||
* @param args the command line arguments |
||||
*/ |
||||
public static void main(String[] args) throws LoginException, IOException |
||||
{ |
||||
|
||||
JCommander commander = JCommander.newBuilder() |
||||
.args(args) |
||||
.acceptUnknownOptions(true) |
||||
.addObject(options) |
||||
.build(); |
||||
|
||||
// We only need 2 gateway intents enabled for this example:
|
||||
EnumSet<GatewayIntent> intents = EnumSet.of( |
||||
// We need messages in guilds to accept commands from users
|
||||
GatewayIntent.GUILD_MESSAGES, |
||||
// We need voice states to connect to the voice channel
|
||||
GatewayIntent.GUILD_VOICE_STATES, |
||||
GatewayIntent.GUILD_MEMBERS, |
||||
GatewayIntent.MESSAGE_CONTENT |
||||
); |
||||
|
||||
settings = new Settings(new File(options.getSettingsPath())); |
||||
|
||||
JDABuilder builder = JDABuilder.createDefault(settings.getDiscordToken(), intents); |
||||
|
||||
builder.enableIntents(GatewayIntent.MESSAGE_CONTENT); |
||||
if (settings.getLocalAddr() != null) |
||||
{ |
||||
System.out.println("Using local address: " + settings.getLocalAddr()); |
||||
// make local binding socket factory
|
||||
final LocalBindSocketFactory localBindSocketFactory = new LocalBindSocketFactory(); |
||||
localBindSocketFactory.setLocalAddress(InetAddress.getByName(settings.getLocalAddr())); |
||||
|
||||
// install local socket factory for HTTP
|
||||
OkHttpClient.Builder httpBuilder = IOUtil.newHttpClientBuilder(); |
||||
httpBuilder.socketFactory(localBindSocketFactory); |
||||
builder.setHttpClientBuilder(httpBuilder); |
||||
|
||||
// install local socket factory for websockets
|
||||
final WebSocketFactory webSocketFactory = new WebSocketFactory(); |
||||
webSocketFactory.setSocketFactory(localBindSocketFactory); |
||||
builder.setWebsocketFactory(webSocketFactory); |
||||
} |
||||
|
||||
// Disable parts of the cache
|
||||
builder.disableCache(CacheFlag.MEMBER_OVERRIDES); |
||||
// Enable the bulk delete event
|
||||
builder.setBulkDeleteSplittingEnabled(false); |
||||
// Disable compression (not recommended)
|
||||
builder.setCompression(Compression.NONE); |
||||
// Set activity (like "playing Something")
|
||||
builder.setActivity(Activity.playing("music!")); |
||||
|
||||
final Chords listener = new Chords(); |
||||
|
||||
builder.addEventListeners(listener); |
||||
builder.setAutoReconnect(true); |
||||
|
||||
JDA jda = builder.build(); |
||||
listener.setJda(jda); |
||||
} |
||||
private final Consumer<Track> nowPlayingConsumer = new NowPlayingConsumer(); |
||||
|
||||
private final BiConsumer<TrackRequest, Exception> downloaderMessageHandler = new DownloaderMessageHandler(); |
||||
|
||||
public Chords() throws IOException |
||||
{ |
||||
log("INFO", "Starting up..."); |
||||
|
||||
// init dirs
|
||||
dataDirectory = new File(System.getProperty("user.dir")); |
||||
playlistsDirectory = initDirectory(dataDirectory, "playlists"); |
||||
|
||||
// init downloader
|
||||
downloader = new Downloader(); |
||||
downloader.setMessageHandler(downloaderMessageHandler); |
||||
|
||||
// init searcher
|
||||
searcher = MetaSearcher.loadYAML(new File("searchproviders.yml")); |
||||
|
||||
// init queue manager
|
||||
queueManager = new QueueManager(); |
||||
queueManager.addSource(downloader); |
||||
|
||||
// init commands
|
||||
addCommand(new JoinCommand(this)); |
||||
addCommand(new LeaveCommand(this)); |
||||
playCommand = new PlayCommand(this); |
||||
addCommand(playCommand); |
||||
addCommand(new QueueCommand(this)); |
||||
addCommand(new RemoveCommand(this)); |
||||
addCommand(new RestartCommand(this)); |
||||
addCommand(new SkipCommand(this)); |
||||
helpCommand = new HelpCommand(this); |
||||
addCommand(helpCommand); |
||||
|
||||
// load playlists
|
||||
loadPlaylists(); |
||||
|
||||
log("INFO", "Started OK!"); |
||||
} |
||||
|
||||
private void addCommand(Command command) |
||||
{ |
||||
commands.put(command.getKeyword(), command); |
||||
} |
||||
|
||||
public void setJda(JDA jda) |
||||
{ |
||||
this.jda = jda; |
||||
} |
||||
|
||||
@Override |
||||
public void onGuildVoiceUpdate(GuildVoiceUpdateEvent event) |
||||
{ |
||||
super.onGuildVoiceUpdate(event); |
||||
if (this.currentVoiceChannel == null) |
||||
return; |
||||
|
||||
final AudioChannel channelLeft = event.getChannelLeft(); |
||||
if (channelLeft != null && channelLeft == currentVoiceChannel) |
||||
{ |
||||
if (channelLeft.getMembers().size() <= 1) // just us now
|
||||
disconnect(); |
||||
} |
||||
} |
||||
@Override |
||||
public void onMessageReceived(MessageReceivedEvent event) |
||||
{ |
||||
super.onMessageReceived(event); //To change body of generated methods, choose Tools | Templates.
|
||||
Message message = event.getMessage(); |
||||
User author = message.getAuthor(); |
||||
String content = message.getContentRaw(); |
||||
Guild guild = event.getGuild(); |
||||
|
||||
// Ignore message if it's not in a music channel
|
||||
if (!event.getChannel().getName().contains("music")) |
||||
return; |
||||
|
||||
// Ignore message if bot
|
||||
if (author.isBot()) |
||||
return; |
||||
|
||||
log("MESG", "G:" + guild.getName() + " A:" + author.getName() + " C:" + content); |
||||
|
||||
Invocation invocation = null; |
||||
|
||||
try |
||||
{ |
||||
try |
||||
{ |
||||
URL parseURL = new URL(content.trim()); |
||||
invocation = new Invocation(event, List.of(parseURL.toExternalForm())); |
||||
playCommand.call(invocation); |
||||
return; |
||||
} catch (MalformedURLException ex) |
||||
{ |
||||
// not a URL, then
|
||||
} |
||||
|
||||
String[] split = content.split("\\s+"); |
||||
List<String> args = new ArrayList<>(); |
||||
Collections.addAll(args, split); |
||||
args.remove(0); |
||||
|
||||
invocation = new Invocation(event, args); |
||||
invocation.setRequestMessage(message); |
||||
|
||||
String cmd = split[0].toLowerCase(); |
||||
|
||||
if (cmd.startsWith("!")) |
||||
{ |
||||
cmd = cmd.substring(1); // strip prefix char
|
||||
|
||||
if (commands.containsKey(cmd)) |
||||
{ |
||||
Command command = commands.get(cmd); |
||||
command.call(invocation); |
||||
} else |
||||
{ |
||||
helpCommand.call(invocation); |
||||
} |
||||
} else if (!event.getMessage().getAttachments().isEmpty()) |
||||
{ |
||||
for (Message.Attachment attach : event.getMessage().getAttachments()) |
||||
{ |
||||
if (attach.getContentType().startsWith("audio") || attach.getContentType().startsWith("video")) |
||||
{ |
||||
try |
||||
{ |
||||
invocation = new Invocation(event, List.of(new URL(attach.getUrl()).toExternalForm())); |
||||
invocation.setRequestMessage(message); |
||||
playCommand.call(invocation); |
||||
} catch (MalformedURLException ex) |
||||
{ |
||||
Logger.getLogger(Chords.class.getName()).log(Level.WARNING, null, ex); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} catch (Exception ex) |
||||
{ |
||||
if (invocation != null) |
||||
invocation.respond("Error: " + ex.getMessage()); |
||||
else |
||||
event.getChannel().sendMessage("Error: " + ex.getMessage()).queue(); |
||||
log("UERR", "Command error:" + ex.getMessage()); |
||||
} |
||||
|
||||
// TODO: this will handle uploading files, maybe
|
||||
} |
||||
|
||||
public void queueDownload(TrackRequest request) |
||||
{ |
||||
request.getInvocation().respond("Request pending..."); |
||||
downloader.accept(request); |
||||
} |
||||
|
||||
public void setStatus(Track nowPlaying) |
||||
{ |
||||
jda.getPresence().setActivity(Activity.listening(nowPlaying.toString())); |
||||
} |
||||
|
||||
public void clearStatus() |
||||
{ |
||||
jda.getPresence().setActivity(null); |
||||
} |
||||
|
||||
/** |
||||
* Connect to requested channel and start echo handler |
||||
* |
||||
* @param channel |
||||
* The channel to connect to |
||||
*/ |
||||
public void connectTo(AudioChannel channel) |
||||
{ |
||||
Guild guild = channel.getGuild(); |
||||
// Get an audio manager for this guild, this will be created upon first use for each guild
|
||||
AudioManager audioManager = guild.getAudioManager(); |
||||
musicHandler = new MusicHandler(); |
||||
queueManager.setHandler(musicHandler); |
||||
// Create our Send/Receive handler for the audio connection
|
||||
// EchoHandler handler = new EchoHandler();
|
||||
|
||||
// The order of the following instructions does not matter!
|
||||
// Set the sending handler to our echo system
|
||||
audioManager.setSendingHandler(musicHandler); |
||||
// Set the receiving handler to the same echo system, otherwise we can't echo anything
|
||||
// audioManager.setReceivingHandler(handler);
|
||||
// Connect to the voice channel
|
||||
audioManager.openAudioConnection(channel); |
||||
currentVoiceChannel = channel; |
||||
|
||||
musicHandler.setNowPlayingConsumer(nowPlayingConsumer); |
||||
} |
||||
|
||||
public void disconnect() |
||||
{ |
||||
if (currentVoiceChannel != null) |
||||
{ |
||||
Guild guild = currentVoiceChannel.getGuild(); |
||||
AudioManager audioManager = guild.getAudioManager(); |
||||
audioManager.setSendingHandler(null); |
||||
audioManager.closeAudioConnection(); |
||||
musicHandler = null; |
||||
currentVoiceChannel = null; |
||||
} |
||||
} |
||||
|
||||
public void log(String type, String message) |
||||
{ |
||||
System.out.println(type + " " + LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME) + "\t" + message); |
||||
} |
||||
|
||||
public File initDirectory(File parent, String name) throws IOException |
||||
{ |
||||
File ret = new File(parent, name); |
||||
if (!ret.exists()) |
||||
Files.createDirectories(ret.toPath()); |
||||
if (!ret.canRead()) |
||||
throw new RuntimeException("Cannot read directory " + ret.getAbsolutePath() + "!"); |
||||
if (!ret.canWrite()) |
||||
throw new RuntimeException("Cannot write to directory " + ret.getAbsolutePath() + "!"); |
||||
return ret; |
||||
} |
||||
|
||||
private void loadPlaylists() |
||||
{ |
||||
File[] files = playlistsDirectory.listFiles((File file, String name) -> name.endsWith(".yaml")); |
||||
for (File file : files) |
||||
{ |
||||
try |
||||
{ |
||||
YamlMapping map = Yaml.createYamlInput(file).readYamlMapping(); |
||||
Playlist playlist = Playlist.fromYaml(map); |
||||
playlists.put(playlist.getName(), playlist); |
||||
} catch (IOException ex) |
||||
{ |
||||
Logger.getLogger(Chords.class.getName()).log(Level.SEVERE, null, ex); |
||||
} |
||||
} |
||||
} |
||||
|
||||
public void setRecommenderEnabled(boolean enabled) |
||||
{ |
||||
// if (recommender == null && enabled)
|
||||
// {
|
||||
//// recommender = new Recommender();
|
||||
// downloader.addSource(recommender);
|
||||
// } else if (recommender != null && !enabled)
|
||||
// {
|
||||
//// downloader.removeSource(recommender);
|
||||
// recommender = null;
|
||||
// }
|
||||
} |
||||
|
||||
public MusicHandler getMusicHandler() |
||||
{ |
||||
return musicHandler; |
||||
} |
||||
|
||||
public Downloader getDownloader() |
||||
{ |
||||
return downloader; |
||||
} |
||||
|
||||
public Searcher getSearcher() |
||||
{ |
||||
return searcher; |
||||
} |
||||
|
||||
public JDA getJda() |
||||
{ |
||||
return jda; |
||||
} |
||||
|
||||
public AudioChannel getCurrentVoiceChannel() |
||||
{ |
||||
return currentVoiceChannel; |
||||
} |
||||
|
||||
public QueueManager getQueueManager() |
||||
{ |
||||
return queueManager; |
||||
} |
||||
|
||||
public static Settings getSettings() |
||||
{ |
||||
return settings; |
||||
} |
||||
|
||||
public static CommandOptions getOptions() |
||||
{ |
||||
return options; |
||||
} |
||||
|
||||
public File getDataDirectory() |
||||
{ |
||||
return dataDirectory; |
||||
} |
||||
|
||||
public File getPlaylistsDirectory() |
||||
{ |
||||
return playlistsDirectory; |
||||
} |
||||
|
||||
public Map<String, Command> getCommands() |
||||
{ |
||||
return commands; |
||||
} |
||||
|
||||
public Map<String, Playlist> getPlaylists() |
||||
{ |
||||
return playlists; |
||||
} |
||||
|
||||
// public Recommender getRecommender()
|
||||
// {
|
||||
// return recommender;
|
||||
// }
|
||||
|
||||
private class DownloaderMessageHandler implements BiConsumer<TrackRequest, Exception> |
||||
{ |
||||
|
||||
private static final String PROGRESS_SYMBOLS = " ▏▎▍▌▋▊▉█"; |
||||
|
||||
private double lastProgressUpdate = 0.0; |
||||
private boolean initialUpdateSent = false; |
||||
|
||||
/* |
||||
⠀⡀⣀⣠⣤⣦⣶⣾⣿ |
||||
🌑🌘🌗🌖🌕 |
||||
🌩🌨🌧🌦🌥🌤🌞 |
||||
○◔◑◕● |
||||
□🞎🞏🞐🞑🞒🞓■ |
||||
□▤▥▦▧▨▩■ |
||||
▁▂▃▄▅▆▇█ |
||||
*/ |
||||
// private static final String PROGRESS_SYMBOLS = " ⠀⡀⣀⣠⣤⣦⣶⣾⣿";
|
||||
|
||||
public DownloaderMessageHandler() |
||||
{ |
||||
} |
||||
|
||||
@Override |
||||
public void accept(TrackRequest request, Exception ex) |
||||
{ |
||||
boolean shouldUpdate = false; |
||||
String response = ""; |
||||
if (request.getTracks().size() > 1) |
||||
response += "Downloading " + request.getTracks().size() + " tracks:\n"; |
||||
for (Track track : request.getTracks()) |
||||
{ |
||||
// TextChannel channel = jda.getTextChannelById(track.getRequestedIn());
|
||||
// String bracketNo = "[" + track.getNumber() + "] ";
|
||||
if (ex == null) |
||||
if (track.getLocation() != null && track.getProgress() >= 100) |
||||
{ |
||||
response += ("Finished downloading " + track + "!"); |
||||
log("DOWN", "Downloaded " + track); |
||||
shouldUpdate = true; |
||||
} else |
||||
{ |
||||
Format format = track.getBestFormat(); |
||||
String formatDetails = ""; |
||||
if (format != null) |
||||
{ |
||||
final int bitrate = format.getSampleRate() / 1000; |
||||
final long size = format.getSize(); |
||||
String sizeFmt = (size <= 0 ? "?.??" : String.format("%.2f", size / (1024.0 * 1024.0))) + "MiB"; |
||||
String bitFmt = (bitrate <= 0 ? "??" : bitrate) + "k"; |
||||
formatDetails = " (" + bitFmt + ", " + sizeFmt + ")"; |
||||
} |
||||
|
||||
String progressDetails = ""; |
||||
if (track.getProgress() >= 0) |
||||
{ |
||||
if (track.getProgress() >= lastProgressUpdate + 5.0) |
||||
{ |
||||
shouldUpdate = true; |
||||
lastProgressUpdate = track.getProgress(); |
||||
} |
||||
progressDetails = " [" + String.format("%.1f", track.getProgress()) + "%]"; |
||||
} |
||||
|
||||
response += ("Now downloading " + track + formatDetails + progressDetails + " ..."); |
||||
log("DOWN", "Downloading " + track + "..."); |
||||
} |
||||
else |
||||
{ |
||||
response += ("Failed to download " + track + "! Reason: " + ex.getMessage()); |
||||
log("DOWN", "Failed to download " + track + "! Reason: " + ex.getMessage()); |
||||
shouldUpdate = true; |
||||
} |
||||
response += "\n"; |
||||
} |
||||
if (!response.isEmpty() && (shouldUpdate || !initialUpdateSent)) |
||||
{ |
||||
request.getInvocation().respond(response); |
||||
initialUpdateSent = true; |
||||
} |
||||
} |
||||
} |
||||
|
||||
private class NowPlayingConsumer implements Consumer<Track> |
||||
{ |
||||
|
||||
public NowPlayingConsumer() |
||||
{ |
||||
} |
||||
|
||||
@Override |
||||
public void accept(Track track) |
||||
{ |
||||
if (track != null) |
||||
jda.getPresence().setActivity(Activity.of(Activity.ActivityType.LISTENING, track.toString())); |
||||
else |
||||
jda.getPresence().setActivity(null); |
||||
|
||||
// if (recommender != null)
|
||||
// recommender.addSeed(track);
|
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,52 @@ |
||||
/* |
||||
* Copyright (C) 2022 jimj316 |
||||
* |
||||
* This program is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU General Public License as published by |
||||
* the Free Software Foundation, either version 3 of the License, or |
||||
* (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ |
||||
package moe.nekojimi.chords; |
||||
|
||||
import com.beust.jcommander.Parameter; |
||||
|
||||
/** |
||||
* |
||||
* @author jimj316 |
||||
*/ |
||||
public class CommandOptions |
||||
{ |
||||
|
||||
@Parameter(names = "-settings", description = "A path to the settings.yml file.") |
||||
private String settingsPath = "settings.yml"; |
||||
|
||||
// @Parameter(names = "-token", description = "The API token for Discord.", required = true)
|
||||
// private String token;
|
||||
//
|
||||
// @Parameter(names = "-local-addr", description = "The local address to bind to.")
|
||||
// private String localAddress;
|
||||
|
||||
// public String getToken()
|
||||
// {
|
||||
// return token;
|
||||
// }
|
||||
//
|
||||
// public String getLocalAddress()
|
||||
// {
|
||||
// return localAddress;
|
||||
// }
|
||||
|
||||
public String getSettingsPath() |
||||
{ |
||||
return settingsPath; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,112 @@ |
||||
/* |
||||
* Copyright (C) 2022 jimj316 |
||||
* |
||||
* This program is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU General Public License as published by |
||||
* the Free Software Foundation, either version 3 of the License, or |
||||
* (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ |
||||
package moe.nekojimi.chords; |
||||
|
||||
import java.io.IOException; |
||||
import java.net.*; |
||||
import javax.net.SocketFactory; |
||||
|
||||
/** |
||||
* |
||||
* @author jimj316 |
||||
*/ |
||||
public class LocalBindSocketFactory extends SocketFactory |
||||
{ |
||||
|
||||
private InetAddress localAddress; |
||||
|
||||
public InetAddress getLocalAddress() |
||||
{ |
||||
return localAddress; |
||||
} |
||||
|
||||
public void setLocalAddress(InetAddress localAddress) |
||||
{ |
||||
this.localAddress = localAddress; |
||||
} |
||||
|
||||
@Override |
||||
public Socket createSocket() throws IOException |
||||
{ |
||||
return new LocalBoundSocket(); |
||||
// throw new RuntimeException("Trying to create an empty socket!");
|
||||
} |
||||
|
||||
private InetSocketAddress findFreeSocketAddress() |
||||
{ |
||||
return new InetSocketAddress(localAddress, findFreePort(localAddress)); |
||||
} |
||||
|
||||
@Override |
||||
public Socket createSocket(String remoteAddr, int remotePort) throws IOException, UnknownHostException |
||||
{ |
||||
return createSocket(remoteAddr, remotePort, localAddress, findFreePort(localAddress)); |
||||
} |
||||
|
||||
@Override |
||||
public Socket createSocket(String remoteAddr, int remotePort, InetAddress x, int localPort) throws IOException, UnknownHostException |
||||
{ |
||||
return createSocket(InetAddress.getByName(remoteAddr), remotePort, localAddress, localPort); |
||||
} |
||||
|
||||
@Override |
||||
public Socket createSocket(InetAddress remoteAddr, int remotePort) throws IOException |
||||
{ |
||||
return createSocket(remoteAddr, remotePort, localAddress, findFreePort(localAddress)); |
||||
} |
||||
|
||||
@Override |
||||
public Socket createSocket(InetAddress remoteAddr, int remotePort, InetAddress x, int localPort) throws IOException |
||||
{ |
||||
System.out.println("Requested socket; " + localAddress + ":" + localPort + " -> " + remoteAddr + ":" + remotePort); |
||||
Socket socket = new Socket(remoteAddr, remotePort, localAddress, localPort); |
||||
System.out.println("Returned socket; " + socket.getLocalSocketAddress() + ":" + socket.getLocalPort() + " -> " + socket.getRemoteSocketAddress() + ":" + socket.getPort()); |
||||
return socket; |
||||
} |
||||
|
||||
private static int findFreePort(InetAddress localAddr) |
||||
{ |
||||
int port = 0; |
||||
// For ServerSocket port number 0 means that the port number is automatically allocated.
|
||||
try (ServerSocket socket = new ServerSocket(0, 0, localAddr)) |
||||
{ |
||||
// Disable timeout and reuse address after closing the socket.
|
||||
socket.setReuseAddress(true); |
||||
port = socket.getLocalPort(); |
||||
} catch (IOException ignored) |
||||
{ |
||||
} |
||||
if (port > 0) |
||||
{ |
||||
return port; |
||||
} |
||||
throw new RuntimeException("Could not find a free port"); |
||||
} |
||||
|
||||
private class LocalBoundSocket extends Socket |
||||
{ |
||||
|
||||
@Override |
||||
public void bind(SocketAddress bindpoint) throws IOException |
||||
{ |
||||
InetSocketAddress localAddress = findFreeSocketAddress(); |
||||
System.err.println("LocalBoundSocket NOT binding to " + bindpoint + ", using " + localAddress + " instead!"); |
||||
super.bind(localAddress); |
||||
} |
||||
} |
||||
|
||||
} |
@ -1,290 +0,0 @@ |
||||
/* |
||||
* To change this license header, choose License Headers in Project Properties. |
||||
* To change this template file, choose Tools | Templates |
||||
* and open the template in the editor. |
||||
*/ |
||||
package moe.nekojimi.chords; |
||||
|
||||
import java.io.File; |
||||
import java.net.URL; |
||||
import java.time.LocalDateTime; |
||||
import java.time.format.DateTimeFormatter; |
||||
import java.util.*; |
||||
import javax.security.auth.login.LoginException; |
||||
import moe.nekojimi.chords.commands.*; |
||||
import moe.nekojimi.musicsearcher.Result; |
||||
import moe.nekojimi.musicsearcher.providers.MetaSearcher; |
||||
import moe.nekojimi.musicsearcher.providers.Searcher; |
||||
import net.dv8tion.jda.api.JDA; |
||||
import net.dv8tion.jda.api.JDABuilder; |
||||
import net.dv8tion.jda.api.entities.*; |
||||
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceLeaveEvent; |
||||
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; |
||||
import net.dv8tion.jda.api.hooks.ListenerAdapter; |
||||
import net.dv8tion.jda.api.managers.AudioManager; |
||||
import net.dv8tion.jda.api.requests.GatewayIntent; |
||||
import net.dv8tion.jda.api.utils.Compression; |
||||
import net.dv8tion.jda.api.utils.cache.CacheFlag; |
||||
|
||||
/** |
||||
* |
||||
* @author jimj316 |
||||
*/ |
||||
public class Main extends ListenerAdapter |
||||
{ |
||||
|
||||
|
||||
private MusicHandler musicHandler; |
||||
private final Downloader downloader; |
||||
private final Searcher searcher; |
||||
private JDA jda; |
||||
|
||||
private final Map<String, Command> commands = new HashMap<>(); |
||||
private final Command helpCommand; |
||||
|
||||
private VoiceChannel currentVoiceChannel = null; |
||||
|
||||
private int trackNumber = 1; |
||||
|
||||
/** |
||||
* @param args the command line arguments |
||||
*/ |
||||
public static void main(String[] args) throws LoginException |
||||
{ |
||||
// We only need 2 gateway intents enabled for this example:
|
||||
EnumSet<GatewayIntent> intents = EnumSet.of( |
||||
// We need messages in guilds to accept commands from users
|
||||
GatewayIntent.GUILD_MESSAGES, |
||||
// We need voice states to connect to the voice channel
|
||||
GatewayIntent.GUILD_VOICE_STATES |
||||
); |
||||
|
||||
JDABuilder builder = JDABuilder.createDefault(args[0], intents); |
||||
|
||||
// Disable parts of the cache
|
||||
builder.disableCache(CacheFlag.MEMBER_OVERRIDES, CacheFlag.VOICE_STATE); |
||||
// Enable the bulk delete event
|
||||
builder.setBulkDeleteSplittingEnabled(false); |
||||
// Disable compression (not recommended)
|
||||
builder.setCompression(Compression.NONE); |
||||
// Set activity (like "playing Something")
|
||||
builder.setActivity(Activity.playing("music!")); |
||||
|
||||
final Main listener = new Main(); |
||||
|
||||
builder.addEventListeners(listener); |
||||
|
||||
JDA jda = builder.build(); |
||||
listener.setJda(jda); |
||||
} |
||||
|
||||
public Main() |
||||
{ |
||||
log("INFO", "Starting up..."); |
||||
downloader = new Downloader(); |
||||
downloader.setMessageHandler((Song song, Exception ex) -> |
||||
{ |
||||
TextChannel channel = jda.getTextChannelById(song.getRequestedIn()); |
||||
// String bracketNo = "[" + song.getNumber() + "] ";
|
||||
if (channel != null) |
||||
if (ex == null) |
||||
if (song.getLocation() != null) |
||||
{ |
||||
channel.sendMessage(/*bracketNo + */"Finished downloading " + song + " for " + song.getRequestedBy() + ", added to queue!").queue(); |
||||
log("DOWN", "Downloaded " + song); |
||||
} else |
||||
{ |
||||
Format format = song.getBestFormat(); |
||||
String formatDetails = ""; |
||||
if (format != null) |
||||
{ |
||||
formatDetails = " (" + format.getBitrate() / 1000 + "k, " + String.format("%.2f", format.getSize() / (1024.0 * 1024.0)) + "MiB)"; |
||||
} |
||||
channel.sendMessage(/*bracketNo + */"Now downloading " + song + formatDetails + " for " + song.getRequestedBy() + " ...").queue(); |
||||
log("DOWN", "Downloading " + song + "..."); |
||||
} |
||||
else |
||||
{ |
||||
channel.sendMessage(/*bracketNo + */"Failed to download " + song + " for " + song.getRequestedBy() + "! Reason: " + ex.getMessage()).queue(); |
||||
log("DOWN", "Failed to download " + song + "! Reason: " + ex.getMessage()); |
||||
} |
||||
|
||||
}); |
||||
searcher = MetaSearcher.loadYAML(new File("searchproviders.yml")); |
||||
|
||||
addCommand(new JoinCommand(this)); |
||||
addCommand(new LeaveCommand(this)); |
||||
addCommand(new PlayCommand(this)); |
||||
addCommand(new QueueCommand(this)); |
||||
addCommand(new RemoveCommand(this)); |
||||
addCommand(new RestartCommand(this)); |
||||
addCommand(new SkipCommand(this)); |
||||
helpCommand = new HelpCommand(this); |
||||
addCommand(helpCommand); |
||||
|
||||
log("INFO", "Started OK!"); |
||||
} |
||||
|
||||
private void addCommand(Command command) |
||||
{ |
||||
commands.put(command.getKeyword(), command); |
||||
} |
||||
|
||||
public void setJda(JDA jda) |
||||
{ |
||||
this.jda = jda; |
||||
} |
||||
|
||||
@Override |
||||
public void onGuildVoiceLeave(GuildVoiceLeaveEvent event) |
||||
{ |
||||
if (this.currentVoiceChannel == null) |
||||
return; |
||||
|
||||
final VoiceChannel channelLeft = event.getChannelLeft(); |
||||
|
||||
if (channelLeft.getMembers().isEmpty()) |
||||
if (channelLeft == currentVoiceChannel) |
||||
disconnect(); |
||||
} |
||||
|
||||
@Override |
||||
public void onGuildMessageReceived(GuildMessageReceivedEvent event) |
||||
{ |
||||
Message message = event.getMessage(); |
||||
User author = message.getAuthor(); |
||||
String content = message.getContentRaw(); |
||||
Guild guild = event.getGuild(); |
||||
|
||||
// Ignore message if it's not in a music channel
|
||||
if (!event.getChannel().getName().contains("music")) |
||||
return; |
||||
|
||||
// Ignore message if bot
|
||||
if (author.isBot()) |
||||
return; |
||||
|
||||
log("MESG", "G:" + guild.getName() + " A:" + author.getName() + " C:" + content); |
||||
|
||||
try |
||||
{ |
||||
String[] split = content.split("\\s+", 2); |
||||
String cmd = split[0].toLowerCase(); |
||||
|
||||
if (!cmd.startsWith("!")) |
||||
return; // doesn't start with prefix char
|
||||
|
||||
cmd = cmd.substring(1); // strip prefix char
|
||||
|
||||
String arg = ""; |
||||
if (split.length > 1) |
||||
arg = split[1]; |
||||
|
||||
if (commands.containsKey(cmd)) |
||||
{ |
||||
Command command = commands.get(cmd); |
||||
command.call(event, List.of(arg)); |
||||
} else |
||||
helpCommand.call(event, List.of(arg)); |
||||
} catch (Exception ex) |
||||
{ |
||||
event.getChannel().sendMessage("Error in command! " + ex.getMessage()).queue(); |
||||
log("UERR", "Command error:" + ex.getMessage()); |
||||
} |
||||
} |
||||
|
||||
public Song queueDownload(final URL url, GuildMessageReceivedEvent event) |
||||
{ |
||||
Song song = new Song(url); |
||||
song.setRequestedBy(event.getAuthor().getName()); |
||||
song.setRequestedIn(event.getChannel().getId()); |
||||
song.setNumber(trackNumber); |
||||
trackNumber++; |
||||
downloader.accept(new Downloader.DownloadTask(song, musicHandler)); |
||||
return song; |
||||
} |
||||
|
||||
public Song queueDownload(Result res, GuildMessageReceivedEvent event) |
||||
{ |
||||
Song song = queueDownload(res.getLink(), event); |
||||
song.setArtist(res.getArtist()); |
||||
song.setTitle(res.getTitle()); |
||||
song.setNumber(trackNumber); |
||||
return song; |
||||
} |
||||
|
||||
/** |
||||
* Connect to requested channel and start echo handler |
||||
* |
||||
* @param channel |
||||
* The channel to connect to |
||||
*/ |
||||
public void connectTo(VoiceChannel channel) |
||||
{ |
||||
Guild guild = channel.getGuild(); |
||||
// Get an audio manager for this guild, this will be created upon first use for each guild
|
||||
AudioManager audioManager = guild.getAudioManager(); |
||||
musicHandler = new MusicHandler(); |
||||
// Create our Send/Receive handler for the audio connection
|
||||
// EchoHandler handler = new EchoHandler();
|
||||
|
||||
// The order of the following instructions does not matter!
|
||||
// Set the sending handler to our echo system
|
||||
audioManager.setSendingHandler(musicHandler); |
||||
// Set the receiving handler to the same echo system, otherwise we can't echo anything
|
||||
// audioManager.setReceivingHandler(handler);
|
||||
// Connect to the voice channel
|
||||
audioManager.openAudioConnection(channel); |
||||
currentVoiceChannel = channel; |
||||
} |
||||
|
||||
public void disconnect() |
||||
{ |
||||
if (currentVoiceChannel != null) |
||||
{ |
||||
Guild guild = currentVoiceChannel.getGuild(); |
||||
AudioManager audioManager = guild.getAudioManager(); |
||||
audioManager.setSendingHandler(null); |
||||
audioManager.closeAudioConnection(); |
||||
musicHandler = null; |
||||
currentVoiceChannel = null; |
||||
} |
||||
} |
||||
|
||||
public void log(String type, String message) |
||||
{ |
||||
System.out.println(type + " " + LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME) + "\t" + message); |
||||
} |
||||
|
||||
public MusicHandler getMusicHandler() |
||||
{ |
||||
return musicHandler; |
||||
} |
||||
|
||||
public Downloader getDownloader() |
||||
{ |
||||
return downloader; |
||||
} |
||||
|
||||
public Searcher getSearcher() |
||||
{ |
||||
return searcher; |
||||
} |
||||
|
||||
public JDA getJda() |
||||
{ |
||||
return jda; |
||||
} |
||||
|
||||
public VoiceChannel getCurrentVoiceChannel() |
||||
{ |
||||
return currentVoiceChannel; |
||||
} |
||||
|
||||
|
||||
public int getTrackNumber() |
||||
{ |
||||
return trackNumber; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,187 @@ |
||||
/* |
||||
* Copyright (C) 2022 jimj316 |
||||
* |
||||
* This program is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU General Public License as published by |
||||
* the Free Software Foundation, either version 3 of the License, or |
||||
* (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ |
||||
package moe.nekojimi.chords; |
||||
|
||||
import java.util.List; |
||||
import java.util.PriorityQueue; |
||||
import java.util.Queue; |
||||
import java.util.function.Consumer; |
||||
|
||||
/** |
||||
* |
||||
* @author jimj316 |
||||
*/ |
||||
public class QueueManager extends QueueThing<Track, Track> |
||||
{ |
||||
|
||||
private final int QUEUE_TARGET_SIZE = 5; |
||||
|
||||
private Track restartingTrack = null; |
||||
// private final PriorityQueue<Track> jukeboxQueue = new PriorityQueue<>();
|
||||
private QueueThing<?, Track> trackSource; |
||||
private Playlist playlist; |
||||
private MusicHandler handler; |
||||
|
||||
public QueueManager() |
||||
{ |
||||
super(new PriorityQueue<Track>()); |
||||
queueTargetSize = QUEUE_TARGET_SIZE; |
||||
// jukeboxQueue = new LinkedList<>();
|
||||
} |
||||
|
||||
@Override |
||||
protected void notifyNewInput() |
||||
{ |
||||
super.notifyNewInput(); |
||||
|
||||
// if (inputQueue.size() < QUEUE_TARGET_SIZE)
|
||||
// demandInput(QUEUE_TARGET_SIZE - inputQueue.size());
|
||||
|
||||
if (handler != null && !handler.isPlaying() || handler.getCurrentTrack() == null) |
||||
handler.requestTrack(); |
||||
} |
||||
|
||||
@Override |
||||
protected boolean completePromise(Promise<Track, Track> request) |
||||
{ |
||||
final Track input = request.getInput(); |
||||
if (input != null) |
||||
{ |
||||
request.complete(input); |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
@Override |
||||
public List<Promise<Track, Track>> request(int count, Consumer<Track> destination) |
||||
{ |
||||
if (restartingTrack != null) |
||||
{ |
||||
Promise ret = new Promise<>(restartingTrack, destination); |
||||
restartingTrack = null; |
||||
return List.of(ret); |
||||
} |
||||
|
||||
List<Promise<Track, Track>> ret = super.request(count, destination); |
||||
if (inputQueue.size() < QUEUE_TARGET_SIZE) |
||||
demandInput(QUEUE_TARGET_SIZE - inputQueue.size()); |
||||
return ret; |
||||
} |
||||
|
||||
/** |
||||
* Called by the music handler when the current track has ended, or if |
||||
* playNext is called with nothing playing. |
||||
* |
||||
* @return the next track to play, or null to stop playing. |
||||
*/ |
||||
// public Track nextTrackNeeded()
|
||||
// {
|
||||
// Track ret;
|
||||
// // if we're restarting the current track: store, clear, and return it
|
||||
// if (restartingTrack != null)
|
||||
// {
|
||||
// ret = restartingTrack;
|
||||
// restartingTrack = null;
|
||||
// }
|
||||
// // if there's anything in the queue, play that first
|
||||
// else if (!jukeboxQueue.isEmpty())
|
||||
// {
|
||||
// ret = jukeboxQueue.poll();
|
||||
// }
|
||||
// // otherwise if there's a playlist, shuffle from that
|
||||
// else if (playlist != null)
|
||||
// {
|
||||
// ret = playlist.getNextTrack();
|
||||
// }
|
||||
// // otherwise stop playing
|
||||
// else
|
||||
// ret = null;
|
||||
//
|
||||
// return ret;
|
||||
// }
|
||||
|
||||
public MusicHandler getHandler() |
||||
{ |
||||
return handler; |
||||
} |
||||
|
||||
public void addTrack(Track track) |
||||
{ |
||||
System.out.println("Track added to queue: " + track.getLocation().getAbsolutePath()); |
||||
inputQueue.add(track); |
||||
|
||||
} |
||||
|
||||
public boolean removeTrack(int i) |
||||
{ |
||||
try |
||||
{ |
||||
return inputQueue.remove((Track) inputQueue.toArray()[i]); |
||||
} catch (ArrayIndexOutOfBoundsException ex) |
||||
{ |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
public boolean removeTrack(Track track) |
||||
{ |
||||
return inputQueue.remove(track); |
||||
} |
||||
|
||||
public void setHandler(MusicHandler handler) |
||||
{ |
||||
this.handler = handler; |
||||
handler.setQueueManager(this); |
||||
} |
||||
|
||||
public Queue<Track> getJukeboxQueue() |
||||
{ |
||||
return inputQueue; |
||||
} |
||||
|
||||
public Playlist getPlaylist() |
||||
{ |
||||
return playlist; |
||||
} |
||||
|
||||
public void setPlaylist(Playlist playlist) |
||||
{ |
||||
this.playlist = playlist; |
||||
} |
||||
|
||||
public boolean restartTrack() |
||||
{ |
||||
restartingTrack = handler.getCurrentTrack(); |
||||
if (restartingTrack != null) |
||||
{ |
||||
handler.requestTrack(); |
||||
return true; |
||||
} else |
||||
return false; |
||||
} |
||||
|
||||
public QueueThing<?, Track> getTrackSource() |
||||
{ |
||||
return trackSource; |
||||
} |
||||
|
||||
public void setTrackSource(QueueThing<?, Track> trackSource) |
||||
{ |
||||
this.trackSource = trackSource; |
||||
} |
||||
} |
@ -0,0 +1,211 @@ |
||||
/* |
||||
* Copyright (C) 2023 jimj316 |
||||
* |
||||
* This program is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU General Public License as published by |
||||
* the Free Software Foundation, either version 3 of the License, or |
||||
* (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ |
||||
package moe.nekojimi.chords; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.LinkedList; |
||||
import java.util.List; |
||||
import java.util.Queue; |
||||
import java.util.function.Consumer; |
||||
|
||||
/** |
||||
* |
||||
* @author jimj316 |
||||
*/ |
||||
public abstract class QueueThing<I, O> implements Consumer<I> |
||||
{ |
||||
// TODO: better name
|
||||
protected final Queue<I> inputQueue; |
||||
protected final List<QueueThing<?, I>> sources = new ArrayList<>(); |
||||
protected final List<QueueThing<O, ?>> sinks = new ArrayList<>(); |
||||
protected final Queue<Promise<I, O>> pendingPromises = new LinkedList<>(); |
||||
|
||||
protected int queueTargetSize = 0; |
||||
|
||||
protected QueueThing(Queue<I> inputQueue) |
||||
{ |
||||
this.inputQueue = inputQueue; |
||||
} |
||||
|
||||
@Override |
||||
public void accept(I t) |
||||
{ |
||||
// FIXME: this causes queue-jumping
|
||||
// if we've got pending promises, fullfill them, otherwise just put it in the queue and notify the sinks
|
||||
if (pendingPromises.isEmpty()) |
||||
{ |
||||
inputQueue.add(t); |
||||
notifyNewInput(); |
||||
} else |
||||
{ |
||||
Promise<I, O> promise = pendingPromises.poll(); |
||||
System.out.println(this.getClass().getSimpleName() + " now has " + pendingPromises + " promises."); |
||||
promise.setInput(t); |
||||
handlePromise(promise); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Called to notify downstream modules that there's input upstream - |
||||
* consider requesting it to pull it through |
||||
*/ |
||||
protected void notifyNewInput() |
||||
{ |
||||
if (inputQueue.size() < queueTargetSize) |
||||
demandInput(queueTargetSize - inputQueue.size()); |
||||
|
||||
for (QueueThing<O, ?> sink : sinks) |
||||
{ |
||||
sink.notifyNewInput(); |
||||
} |
||||
} |
||||
|
||||
public List<Promise<I, O>> request(int count, Consumer<O> destination) |
||||
{ |
||||
List<Promise<I, O>> ret = new ArrayList<>(); |
||||
int demandsForClient = 0; |
||||
for (int i = 0; i < count; i++) |
||||
{ |
||||
I input = null; |
||||
if (!inputQueue.isEmpty()) |
||||
{ |
||||
input = inputQueue.poll(); |
||||
} |
||||
|
||||
Promise<I, O> promise = new Promise<>(input, destination); |
||||
|
||||
boolean ok = handlePromise(promise); |
||||
|
||||
if (ok) |
||||
{ |
||||
// we got a promise of output so we can tell the client
|
||||
ret.add(promise); |
||||
} else |
||||
{ |
||||
// we need to get more input from sources
|
||||
demandsForClient++; |
||||
} |
||||
} |
||||
|
||||
int demandsForMe = queueTargetSize - inputQueue.size(); |
||||
int demandsTotal = demandsForMe + demandsForClient; |
||||
|
||||
if (demandsTotal > 0) |
||||
{ |
||||
// try to get more input from sources
|
||||
int sourcePromises = demandInput(demandsForClient); |
||||
// each promise of input we get represents a promise of output we can give
|
||||
for (int i = 0; i < sourcePromises && i < demandsForClient; i++) |
||||
{ |
||||
Promise<I, O> myPromise = new Promise<>(destination); |
||||
pendingPromises.add(myPromise); |
||||
System.out.println(this.getClass().getSimpleName() + " has made " + pendingPromises + " promises."); |
||||
ret.add(myPromise); |
||||
} |
||||
} |
||||
return ret; |
||||
} |
||||
|
||||
private boolean handlePromise(Promise<I, O> promise) |
||||
{ |
||||
boolean ok; |
||||
ok = completePromise(promise); |
||||
return ok; |
||||
} |
||||
|
||||
protected abstract boolean completePromise(Promise<I, O> request); |
||||
|
||||
/** |
||||
* Requests from sources a certain about of input, to be provided later. |
||||
* |
||||
* @param count the number of input items to request. |
||||
* @return a number Promises of input items. May be more or less than count. |
||||
*/ |
||||
protected int demandInput(int count) |
||||
{ |
||||
int ret = 0; |
||||
for (QueueThing<?, I> source : sources) |
||||
{ |
||||
List<Promise<?, I>> promises = (List<Promise<?, I>>) source.request(count - ret, this); |
||||
ret += promises.size(); |
||||
if (ret >= count) |
||||
break; |
||||
} |
||||
return ret; |
||||
} |
||||
|
||||
public void addSource(QueueThing<?, I> source) |
||||
{ |
||||
sources.add(source); |
||||
source.addSink(this); |
||||
} |
||||
|
||||
public void removeSource(QueueThing<?, I> source) |
||||
{ |
||||
sources.remove(source); |
||||
source.removeSink(this); |
||||
} |
||||
|
||||
private void addSink(QueueThing<O, ?> sink) |
||||
{ |
||||
sinks.add(sink); |
||||
} |
||||
|
||||
private void removeSink(QueueThing<O, ?> sink) |
||||
{ |
||||
sinks.remove(sink); |
||||
} |
||||
|
||||
public static class Promise<I, O> |
||||
{ |
||||
|
||||
private I input; |
||||
private final Consumer<O> output; |
||||
|
||||
public Promise(I input, Consumer<O> output) |
||||
{ |
||||
this.input = input; |
||||
this.output = output; |
||||
} |
||||
|
||||
public Promise(Consumer<O> output) |
||||
{ |
||||
this.output = output; |
||||
} |
||||
|
||||
public void setInput(I input) |
||||
{ |
||||
this.input = input; |
||||
} |
||||
|
||||
public void complete(O out) |
||||
{ |
||||
output.accept(out); |
||||
} |
||||
|
||||
public I getInput() |
||||
{ |
||||
return input; |
||||
} |
||||
|
||||
} |
||||
// protected void submit(O output)
|
||||
// {
|
||||
// if (nextStage != null)
|
||||
// nextStage.accept(output);
|
||||
// }
|
||||
} |
@ -0,0 +1,59 @@ |
||||
/* |
||||
* Copyright (C) 2023 jimj316 |
||||
* |
||||
* This program is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU General Public License as published by |
||||
* the Free Software Foundation, either version 3 of the License, or |
||||
* (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ |
||||
package moe.nekojimi.chords; |
||||
|
||||
import com.amihaiemil.eoyaml.Yaml; |
||||
import com.amihaiemil.eoyaml.YamlInput; |
||||
import com.amihaiemil.eoyaml.YamlMapping; |
||||
import java.io.File; |
||||
import java.io.FileNotFoundException; |
||||
import java.io.IOException; |
||||
|
||||
/** |
||||
* |
||||
* @author jimj316 |
||||
*/ |
||||
public class Settings |
||||
{ |
||||
|
||||
private final YamlMapping mapping; |
||||
|
||||
public Settings(File file) throws FileNotFoundException, IOException |
||||
{ |
||||
YamlInput input = Yaml.createYamlInput(file); |
||||
mapping = input.readYamlMapping(); |
||||
|
||||
} |
||||
|
||||
public String getDiscordToken() |
||||
{ |
||||
return mapping.string("discord-token"); |
||||
} |
||||
|
||||
public String getLocalAddr() |
||||
{ |
||||
return mapping.string("local-addr"); |
||||
} |
||||
|
||||
public String getYtdlCommand() |
||||
{ |
||||
String ret = mapping.string("ytdl-cmd"); |
||||
if (ret == null) |
||||
ret = "/usr/bin/youtube-dl"; |
||||
return ret; |
||||
} |
||||
} |
@ -0,0 +1,210 @@ |
||||
/* |
||||
* Copyright (C) 2022 jimj316 |
||||
* |
||||
* This program is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU General Public License as published by |
||||
* the Free Software Foundation, either version 3 of the License, or |
||||
* (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ |
||||
package moe.nekojimi.chords; |
||||
|
||||
import com.amihaiemil.eoyaml.YamlMapping; |
||||
import java.net.URL; |
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
import java.util.Objects; |
||||
import moe.nekojimi.chords.commands.Invocation; |
||||
import moe.nekojimi.musicsearcher.Result; |
||||
|
||||
/** |
||||
* |
||||
* @author jimj316 |
||||
*/ |
||||
public class TrackRequest implements Comparable<TrackRequest> |
||||
{ |
||||
private Invocation invocation; |
||||
|
||||
private String query; |
||||
private URL url; |
||||
|
||||
private Result result; |
||||
|
||||
private final List<Track> tracks = new ArrayList<>(); |
||||
|
||||
private String requestedBy; |
||||
private String requestedIn; |
||||
|
||||
private double priority = 1.0; |
||||
|
||||
public YamlMapping toYAML() |
||||
{ |
||||
throw new UnsupportedOperationException("Not supported yet."); |
||||
} |
||||
|
||||
public static TrackRequest fromYAML(YamlMapping yaml) |
||||
{ |
||||
TrackRequest ret = new TrackRequest(); |
||||
|
||||
return ret; |
||||
} |
||||
|
||||
public Result getResult() |
||||
{ |
||||
return result; |
||||
} |
||||
|
||||
public void setResult(Result result) |
||||
{ |
||||
this.result = result; |
||||
} |
||||
|
||||
public Invocation getInvocation() |
||||
{ |
||||
return invocation; |
||||
} |
||||
|
||||
public void setInvocation(Invocation invocation) |
||||
{ |
||||
this.invocation = invocation; |
||||
requestedBy = invocation.getRequestMessage().getAuthor().getName(); |
||||
requestedIn = invocation.getRequestMessage().getChannel().getId(); |
||||
} |
||||
|
||||
// public Message getRequestMessage()
|
||||
// {
|
||||
// return requestMessage;
|
||||
// }
|
||||
//
|
||||
// public void setRequestMessage(Message requestMessage)
|
||||
// {
|
||||
// this.requestMessage = requestMessage;
|
||||
// }
|
||||
//
|
||||
// public Message getResponseMessage()
|
||||
// {
|
||||
// return responseMessage;
|
||||
// }
|
||||
//
|
||||
// public void setResponseMessage(Message responseMessage)
|
||||
// {
|
||||
// this.responseMessage = responseMessage;
|
||||
// }
|
||||
|
||||
public String getQuery() |
||||
{ |
||||
return query; |
||||
} |
||||
|
||||
public void setQuery(String query) |
||||
{ |
||||
this.query = query; |
||||
} |
||||
|
||||
public URL getUrl() |
||||
{ |
||||
if (url != null) |
||||
return url; |
||||
else if (result != null) |
||||
return (result.getLink()); |
||||
else |
||||
return null; |
||||
} |
||||
|
||||
public void setUrl(URL url) |
||||
{ |
||||
this.url = url; |
||||
} |
||||
|
||||
public List<Track> getTracks() |
||||
{ |
||||
return tracks; |
||||
} |
||||
|
||||
public void addTrack(Track track) |
||||
{ |
||||
tracks.add(track); |
||||
} |
||||
|
||||
public void clearTracks() |
||||
{ |
||||
tracks.clear(); |
||||
} |
||||
|
||||
public String getRequestedBy() |
||||
{ |
||||
return requestedBy; |
||||
} |
||||
|
||||
public String getRequestedIn() |
||||
{ |
||||
return requestedIn; |
||||
} |
||||
|
||||
public String toString() |
||||
{ |
||||
String trackName; |
||||
if (tracks.isEmpty()) |
||||
trackName = "Request"; |
||||
else |
||||
{ |
||||
trackName = tracks.get(0).toString(); |
||||
if (tracks.size() > 1) |
||||
trackName += " & " + (tracks.size() - 1) + " more tracks"; |
||||
} |
||||
|
||||
String requestName = ""; |
||||
if (!requestedBy.isEmpty()) |
||||
requestName = "for " + requestedBy; |
||||
|
||||
return (trackName + " " + requestName).trim(); |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() |
||||
{ |
||||
int hash = 7; |
||||
hash = 83 * hash + Objects.hashCode(this.url); |
||||
return hash; |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(Object obj) |
||||
{ |
||||
if (this == obj) |
||||
return true; |
||||
if (obj == null) |
||||
return false; |
||||
if (getClass() != obj.getClass()) |
||||
return false; |
||||
final TrackRequest other = (TrackRequest) obj; |
||||
if (!Objects.equals(this.url, other.url)) |
||||
return false; |
||||
return true; |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public int compareTo(TrackRequest o) |
||||
{ |
||||
return -Double.compare(priority, o.priority); // backwards so higher priority comes first
|
||||
} |
||||
|
||||
public double getPriority() |
||||
{ |
||||
return priority; |
||||
} |
||||
|
||||
public void setPriority(double priority) |
||||
{ |
||||
this.priority = priority; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,85 @@ |
||||
/* |
||||
* Copyright (C) 2022 jimj316 |
||||
* |
||||
* This program is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU General Public License as published by |
||||
* the Free Software Foundation, either version 3 of the License, or |
||||
* (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ |
||||
package moe.nekojimi.chords.commands; |
||||
|
||||
import java.util.List; |
||||
import net.dv8tion.jda.api.entities.Message; |
||||
import net.dv8tion.jda.api.events.message.MessageReceivedEvent; |
||||
|
||||
/** |
||||
* |
||||
* @author jimj316 |
||||
*/ |
||||
public class Invocation |
||||
{ |
||||
|
||||
private Message requestMessage; |
||||
private Message responseMessage; |
||||
|
||||
private final MessageReceivedEvent event; |
||||
private final List<String> args; |
||||
|
||||
public Invocation(MessageReceivedEvent event, List<String> args) |
||||
{ |
||||
this.event = event; |
||||
this.args = args; |
||||
} |
||||
|
||||
public MessageReceivedEvent getEvent() |
||||
{ |
||||
return event; |
||||
} |
||||
|
||||
public List<String> getArgs() |
||||
{ |
||||
return args; |
||||
} |
||||
|
||||
@SuppressWarnings("null") |
||||
public void respond(String text) |
||||
{ |
||||
|
||||
if (responseMessage == null) |
||||
{ |
||||
responseMessage = requestMessage.reply(text).complete(); |
||||
} else |
||||
{ |
||||
responseMessage = responseMessage.editMessage(text).complete(); |
||||
} |
||||
} |
||||
|
||||
public Message getRequestMessage() |
||||
{ |
||||
return requestMessage; |
||||
} |
||||
|
||||
public void setRequestMessage(Message requestMessage) |
||||
{ |
||||
this.requestMessage = requestMessage; |
||||
} |
||||
|
||||
public Message getResponseMessage() |
||||
{ |
||||
return responseMessage; |
||||
} |
||||
|
||||
public void setResponseMessage(Message responseMessage) |
||||
{ |
||||
this.responseMessage = responseMessage; |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue