Intial commit 2
This commit is contained in:
commit
cffaf80c71
14 changed files with 905 additions and 0 deletions
113
.gitignore
vendored
Normal file
113
.gitignore
vendored
Normal file
|
@ -0,0 +1,113 @@
|
|||
# User-specific stuff
|
||||
.idea/
|
||||
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# Compiled class file
|
||||
*.class
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
|
||||
*~
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
# .nfs files are created when an open file is removed but is still being accessed
|
||||
.nfs*
|
||||
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
target/
|
||||
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
.flattened-pom.xml
|
||||
|
||||
# Common working directory
|
||||
run/
|
75
pom.xml
Normal file
75
pom.xml
Normal file
|
@ -0,0 +1,75 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>eu.m724</groupId>
|
||||
<artifactId>music-plugin</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>music-plugin</name>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<defaultGoal>clean package</defaultGoal>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<source>${java.version}</source>
|
||||
<target>${java.version}</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spigotmc-repo</id>
|
||||
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>sonatype</id>
|
||||
<url>https://oss.sonatype.org/content/groups/public/</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>maxhenkel</id>
|
||||
<url>https://maven.maxhenkel.de/repository/public</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.spigotmc</groupId>
|
||||
<artifactId>spigot-api</artifactId>
|
||||
<version>1.21.1-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>de.maxhenkel.voicechat</groupId>
|
||||
<artifactId>voicechat-api</artifactId>
|
||||
<version>2.5.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.gagravarr/vorbis-java-core -->
|
||||
<dependency>
|
||||
<groupId>org.gagravarr</groupId>
|
||||
<artifactId>vorbis-java-core</artifactId>
|
||||
<version>0.8</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.bramp.ffmpeg</groupId>
|
||||
<artifactId>ffmpeg</artifactId>
|
||||
<version>0.8.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</project>
|
108
src/main/java/eu/m724/musicPlugin/MusicCommands.java
Normal file
108
src/main/java/eu/m724/musicPlugin/MusicCommands.java
Normal file
|
@ -0,0 +1,108 @@
|
|||
package eu.m724.musicPlugin;
|
||||
|
||||
import eu.m724.musicPlugin.file.AudioFileStorage;
|
||||
import eu.m724.musicPlugin.player.MusicPlayer;
|
||||
import eu.m724.musicPlugin.file.AudioFile;
|
||||
import eu.m724.musicPlugin.player.StaticMusicPlayer;
|
||||
import net.md_5.bungee.api.chat.ClickEvent;
|
||||
import net.md_5.bungee.api.chat.ComponentBuilder;
|
||||
import net.md_5.bungee.api.chat.HoverEvent;
|
||||
import net.md_5.bungee.api.chat.hover.content.Text;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
|
||||
public class MusicCommands implements CommandExecutor {
|
||||
private final AudioFileStorage storage;
|
||||
private MusicPlayer player;
|
||||
|
||||
public MusicCommands(AudioFileStorage storage) {
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (command.getName().equals("download")) {
|
||||
try {
|
||||
var cf = storage.download(URI.create(args[0]).toURL());
|
||||
sender.sendMessage("Started download");
|
||||
cf.handle((hash, ex) -> {
|
||||
if (ex != null)
|
||||
sender.sendMessage("ERROR downloading: " + ex.getMessage());
|
||||
else
|
||||
sender.spigot().sendMessage(
|
||||
new ComponentBuilder("Hash: " + hash)
|
||||
.event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Click to copy")))
|
||||
.event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, hash))
|
||||
.append("\nClick here to convert to 48 kbps")
|
||||
.event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Click to do that")))
|
||||
.event(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/convert " + hash + " 48000"))
|
||||
.create()
|
||||
);
|
||||
return null;
|
||||
});
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} else if (command.getName().equals("convert")) {
|
||||
try {
|
||||
var cf = storage.convert(args[0], Integer.parseInt(args[1]));
|
||||
sender.sendMessage("Converting " + args[0] + " to " + args[1] + "bps...");
|
||||
cf.thenAccept(f -> {
|
||||
sender.spigot().sendMessage(
|
||||
new ComponentBuilder("Converted " + args[0] + " to " + args[1] + "bps! Click to play")
|
||||
.event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Click to play")))
|
||||
.event(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/play " + args[0] + " " + args[1]))
|
||||
.create()
|
||||
);
|
||||
}).exceptionally(ex -> {
|
||||
sender.sendMessage("ERROR converting: " + ex.getMessage());
|
||||
return null;
|
||||
});
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} else if (command.getName().equals("play")) {
|
||||
if (args.length == 1) {
|
||||
StringBuilder msg = new StringBuilder("Available bitrates:");
|
||||
for (Map.Entry<Integer, File> entry : storage.getVersions(args[0]).entrySet()) {
|
||||
msg.append(" ").append(entry.getKey());
|
||||
}
|
||||
sender.sendMessage(msg.toString());
|
||||
return true;
|
||||
}
|
||||
var file = storage.get(args[0], Integer.parseInt(args[1]));
|
||||
sender.sendMessage("Initializeng");
|
||||
player = new StaticMusicPlayer(((Player)sender).getLocation());
|
||||
player.init();
|
||||
|
||||
try (FileInputStream fis = new FileInputStream(file)) {
|
||||
var song = new AudioFile(fis);
|
||||
song.load();
|
||||
player.play(song);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
sender.sendMessage("done si r now playing it");
|
||||
} else if (command.getName().equals("stop")) {
|
||||
player.stop();
|
||||
sender.sendMessage("stopedd");
|
||||
} else if (command.getName().equals("pause")) {
|
||||
player.pause();
|
||||
sender.sendMessage("puased");
|
||||
} else if (command.getName().equals("resume")) {
|
||||
player.unpause();
|
||||
sender.sendMessage("unapuised");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
52
src/main/java/eu/m724/musicPlugin/MusicPlugin.java
Normal file
52
src/main/java/eu/m724/musicPlugin/MusicPlugin.java
Normal file
|
@ -0,0 +1,52 @@
|
|||
package eu.m724.musicPlugin;
|
||||
|
||||
import de.maxhenkel.voicechat.api.BukkitVoicechatService;
|
||||
import de.maxhenkel.voicechat.api.VoicechatConnection;
|
||||
import de.maxhenkel.voicechat.api.VoicechatServerApi;
|
||||
import eu.m724.musicPlugin.file.AudioFileStorage;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
public final class MusicPlugin extends JavaPlugin {
|
||||
@Override
|
||||
public void onEnable() {
|
||||
BukkitVoicechatService service = getServer().getServicesManager().load(BukkitVoicechatService.class);
|
||||
|
||||
if (service != null) {
|
||||
service.registerPlugin(
|
||||
new MyVoicechatPlugin(
|
||||
this::onApiStarted,
|
||||
this::onPlayerConnected
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getDataFolder().mkdir();
|
||||
var mcmd = new MusicCommands(new AudioFileStorage(getDataFolder()));
|
||||
getCommand("download").setExecutor(mcmd);
|
||||
getCommand("convert").setExecutor(mcmd);
|
||||
getCommand("play").setExecutor(mcmd);
|
||||
getCommand("stop").setExecutor(mcmd);
|
||||
getCommand("pause").setExecutor(mcmd);
|
||||
getCommand("resume").setExecutor(mcmd);
|
||||
|
||||
}
|
||||
|
||||
private void onApiStarted(VoicechatServerApi api) {
|
||||
getLogger().info("registerating...");
|
||||
|
||||
var category = api.volumeCategoryBuilder()
|
||||
.setId("music724")
|
||||
.setName("Music players")
|
||||
.build();
|
||||
|
||||
api.registerVolumeCategory(category);
|
||||
|
||||
Statics.api = api;
|
||||
|
||||
getLogger().info("Sucess");
|
||||
}
|
||||
|
||||
private void onPlayerConnected(VoicechatConnection connection) {
|
||||
getLogger().info("Player connected: " + connection);
|
||||
}
|
||||
}
|
45
src/main/java/eu/m724/musicPlugin/MyVoicechatPlugin.java
Normal file
45
src/main/java/eu/m724/musicPlugin/MyVoicechatPlugin.java
Normal file
|
@ -0,0 +1,45 @@
|
|||
package eu.m724.musicPlugin;
|
||||
|
||||
import de.maxhenkel.voicechat.api.VoicechatApi;
|
||||
import de.maxhenkel.voicechat.api.VoicechatConnection;
|
||||
import de.maxhenkel.voicechat.api.VoicechatPlugin;
|
||||
import de.maxhenkel.voicechat.api.VoicechatServerApi;
|
||||
import de.maxhenkel.voicechat.api.events.EventRegistration;
|
||||
import de.maxhenkel.voicechat.api.events.PlayerConnectedEvent;
|
||||
import de.maxhenkel.voicechat.api.events.VoicechatServerStartedEvent;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class MyVoicechatPlugin implements VoicechatPlugin {
|
||||
private final Consumer<VoicechatServerApi> apiConsumer;
|
||||
private final Consumer<VoicechatConnection> playerConnected;
|
||||
|
||||
public MyVoicechatPlugin(Consumer<VoicechatServerApi> apiConsumer, Consumer<VoicechatConnection> playerConnected) {
|
||||
this.apiConsumer = apiConsumer;
|
||||
this.playerConnected = playerConnected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPluginId() {
|
||||
return "myplgugonbo";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(VoicechatApi api) {
|
||||
VoicechatPlugin.super.initialize(api);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerEvents(EventRegistration registration) {
|
||||
registration.registerEvent(VoicechatServerStartedEvent.class, this::onServerStarted);
|
||||
registration.registerEvent(PlayerConnectedEvent.class, this::onPlayerConnected);
|
||||
}
|
||||
|
||||
public void onServerStarted(VoicechatServerStartedEvent event) {
|
||||
apiConsumer.accept(event.getVoicechat());
|
||||
}
|
||||
|
||||
public void onPlayerConnected(PlayerConnectedEvent event) {
|
||||
playerConnected.accept(event.getConnection());
|
||||
}
|
||||
}
|
8
src/main/java/eu/m724/musicPlugin/Statics.java
Normal file
8
src/main/java/eu/m724/musicPlugin/Statics.java
Normal file
|
@ -0,0 +1,8 @@
|
|||
package eu.m724.musicPlugin;
|
||||
|
||||
import de.maxhenkel.voicechat.api.VoicechatServerApi;
|
||||
|
||||
// TODO find a better way
|
||||
public class Statics {
|
||||
public static VoicechatServerApi api;
|
||||
}
|
71
src/main/java/eu/m724/musicPlugin/file/AudioFile.java
Normal file
71
src/main/java/eu/m724/musicPlugin/file/AudioFile.java
Normal file
|
@ -0,0 +1,71 @@
|
|||
package eu.m724.musicPlugin.file;
|
||||
|
||||
import org.gagravarr.ogg.OggFile;
|
||||
import org.gagravarr.opus.OpusAudioData;
|
||||
import org.gagravarr.opus.OpusFile;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class AudioFile {
|
||||
private final InputStream inputStream;
|
||||
|
||||
private NotEncoder encoder;
|
||||
private String title, artist;
|
||||
private int duration;
|
||||
|
||||
public AudioFile(File file) {
|
||||
try {
|
||||
this.inputStream = new FileInputStream(file);
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public AudioFile(InputStream inputStream) {
|
||||
this.inputStream = inputStream;
|
||||
}
|
||||
|
||||
public void load() throws IOException {
|
||||
if (encoder != null) return;
|
||||
|
||||
var list = new ArrayList<byte[]>();
|
||||
|
||||
var opus = new OpusFile(new OggFile(inputStream));
|
||||
var frames = 0;
|
||||
|
||||
OpusAudioData packet;
|
||||
while ((packet = opus.getNextAudioPacket()) != null) {
|
||||
list.add(packet.getData());
|
||||
frames += packet.getNumberOfFrames();
|
||||
}
|
||||
|
||||
this.duration = frames / 50;
|
||||
|
||||
this.title = opus.getTags().getTitle();
|
||||
this.artist = opus.getTags().getArtist();
|
||||
|
||||
this.encoder = new NotEncoder(list.toArray(byte[][]::new));
|
||||
}
|
||||
|
||||
|
||||
public NotEncoder getEncoder() {
|
||||
return encoder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if data was loaded i.e. if load was called
|
||||
*/
|
||||
public boolean isLoaded() {
|
||||
return encoder != null;
|
||||
}
|
||||
|
||||
/** @return Track duration in seconds */
|
||||
public int getTrackDuration() { return duration; }
|
||||
|
||||
/** @return Track title if available */
|
||||
public String getTrackTitle() { return title; }
|
||||
|
||||
/** @return Track artist if available */
|
||||
public String getTrackArtist() { return artist; }
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package eu.m724.musicPlugin.file;
|
||||
|
||||
import net.bramp.ffmpeg.FFmpeg;
|
||||
import net.bramp.ffmpeg.FFmpegExecutor;
|
||||
import net.bramp.ffmpeg.builder.FFmpegBuilder;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class AudioFileConverter {
|
||||
private final FFmpeg ffmpeg;
|
||||
|
||||
public AudioFileConverter(FFmpeg ffmpeg) {
|
||||
this.ffmpeg = ffmpeg;
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> convert(File input, File output, int bitrate) throws IOException {
|
||||
return convert(input.getAbsolutePath(), output, bitrate);
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> convert(URL source, File output, int bitrate) throws IOException {
|
||||
return convert(source.toString(), output, bitrate);
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> convert(String input, File output, int bitrate) throws IOException {
|
||||
var builder = new FFmpegBuilder()
|
||||
.setInput(input)
|
||||
.addOutput(output.getAbsolutePath())
|
||||
.setAudioChannels(1)
|
||||
.setAudioSampleRate(48_000)
|
||||
.setAudioBitRate(bitrate)
|
||||
.done();
|
||||
|
||||
var executor = new FFmpegExecutor(ffmpeg);
|
||||
return CompletableFuture.runAsync(executor.createJob(builder));
|
||||
}
|
||||
}
|
118
src/main/java/eu/m724/musicPlugin/file/AudioFileStorage.java
Normal file
118
src/main/java/eu/m724/musicPlugin/file/AudioFileStorage.java
Normal file
|
@ -0,0 +1,118 @@
|
|||
package eu.m724.musicPlugin.file;
|
||||
|
||||
import net.bramp.ffmpeg.FFmpeg;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class AudioFileStorage {
|
||||
private final File basePath;
|
||||
private final AudioFileConverter converter;
|
||||
|
||||
public AudioFileStorage(File basePath) {
|
||||
this.basePath = basePath;
|
||||
|
||||
try {
|
||||
this.converter = new AudioFileConverter(new FFmpeg()); // TODO better error handling and not here
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get versions (bitrates) of an audio file
|
||||
*
|
||||
* @param hash TODO NOT SANITIZED
|
||||
* @return Map of Bps to file, or null if no such hash
|
||||
*/
|
||||
public Map<Integer, File> getVersions(String hash) {
|
||||
var dir = new File(basePath, hash);
|
||||
|
||||
var files = dir.listFiles();
|
||||
if (files == null) return null;
|
||||
|
||||
return Arrays.stream(files).collect(Collectors.toMap(
|
||||
f -> Integer.parseInt(f.getName().split("\\.")[0]),
|
||||
f -> f
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an audio file of given original hash and bitrate
|
||||
*
|
||||
* @param hash TODO NOT SANITIZED
|
||||
* @param bitrate the bitrate in bps like 32000
|
||||
*/
|
||||
public File get(String hash, int bitrate) {
|
||||
return new File(basePath, hash + "/" + bitrate + ".opus");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the original file with a hash
|
||||
*/
|
||||
public File getOriginal(String hash) {
|
||||
return new File(basePath, hash + "/0.original");
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a (original) file to some bitrate
|
||||
*
|
||||
* @param hash the hash
|
||||
* @param bitrate the target bitrate in bps
|
||||
* @return the future with the new file
|
||||
*/
|
||||
public CompletableFuture<File> convert(String hash, int bitrate) throws IOException {
|
||||
var file = get(hash, bitrate);
|
||||
if (file.exists()) return CompletableFuture.completedFuture(file);
|
||||
|
||||
var og = getOriginal(hash);
|
||||
if (!og.exists()) return null;
|
||||
|
||||
return converter.convert(og, file, bitrate).thenApply(v -> file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads an audio file and saves it as original
|
||||
*
|
||||
* @param url the url to download from
|
||||
* @return the hash
|
||||
*/
|
||||
public CompletableFuture<String> download(URL url) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
var temp = new File(basePath, "temp_" + RandomStringUtils.randomAlphabetic(8));
|
||||
var digest = MessageDigest.getInstance("SHA-256");
|
||||
|
||||
try (
|
||||
var input = new BufferedInputStream(url.openStream());
|
||||
var output = new FileOutputStream(temp)
|
||||
) {
|
||||
var dataBuffer = new byte[1024];
|
||||
int bytesRead;
|
||||
while ((bytesRead = input.read(dataBuffer, 0, 1024)) != -1) {
|
||||
output.write(dataBuffer, 0, bytesRead);
|
||||
digest.update(dataBuffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
|
||||
String hash = HexFormat.of().formatHex(digest.digest());
|
||||
var og = getOriginal(hash);
|
||||
og.getParentFile().mkdir();
|
||||
if (!og.exists())
|
||||
Files.move(temp.toPath(), og.toPath());
|
||||
|
||||
return hash;
|
||||
} catch (IOException | NoSuchAlgorithmException e) {
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
41
src/main/java/eu/m724/musicPlugin/file/NotEncoder.java
Normal file
41
src/main/java/eu/m724/musicPlugin/file/NotEncoder.java
Normal file
|
@ -0,0 +1,41 @@
|
|||
package eu.m724.musicPlugin.file;
|
||||
|
||||
import de.maxhenkel.voicechat.api.opus.OpusEncoder;
|
||||
|
||||
public class NotEncoder implements OpusEncoder {
|
||||
private final byte[][] opusFrames;
|
||||
private int i = 0;
|
||||
private final int size;
|
||||
|
||||
private boolean closed = false;
|
||||
|
||||
public NotEncoder(byte[][] opusFrames) {
|
||||
this.opusFrames = opusFrames;
|
||||
this.size = opusFrames.length;
|
||||
}
|
||||
|
||||
public boolean hasRemaining() {
|
||||
return i < size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encode(short[] rawAudio) {
|
||||
if (i < size)
|
||||
return opusFrames[i++];
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetState() {
|
||||
closed = false;
|
||||
i = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClosed() {
|
||||
return closed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() { closed = true; }
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package eu.m724.musicPlugin.player;
|
||||
|
||||
import de.maxhenkel.voicechat.api.audiochannel.AudioChannel;
|
||||
import org.bukkit.entity.Entity;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class EntityMusicPlayer extends MusicPlayer {
|
||||
private final Entity entity;
|
||||
|
||||
public EntityMusicPlayer(Entity entity) {
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
AudioChannel createChannel() {
|
||||
var channel = api.createEntityAudioChannel(
|
||||
UUID.randomUUID(),
|
||||
api.fromEntity(entity)
|
||||
);
|
||||
|
||||
channel.setCategory("music724");
|
||||
channel.setDistance(32);
|
||||
|
||||
return channel;
|
||||
}
|
||||
}
|
150
src/main/java/eu/m724/musicPlugin/player/MusicPlayer.java
Normal file
150
src/main/java/eu/m724/musicPlugin/player/MusicPlayer.java
Normal file
|
@ -0,0 +1,150 @@
|
|||
package eu.m724.musicPlugin.player;
|
||||
|
||||
import de.maxhenkel.voicechat.api.VoicechatServerApi;
|
||||
import de.maxhenkel.voicechat.api.audiochannel.AudioChannel;
|
||||
import de.maxhenkel.voicechat.api.audiochannel.AudioPlayer;
|
||||
import eu.m724.musicPlugin.Statics;
|
||||
import eu.m724.musicPlugin.file.AudioFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public abstract class MusicPlayer {
|
||||
// TODO find a better way
|
||||
final VoicechatServerApi api = Statics.api;
|
||||
|
||||
private AudioChannel channel;
|
||||
|
||||
private boolean playing = false, paused = false;
|
||||
private AudioPlayer player;
|
||||
private AudioFile audioFile;
|
||||
|
||||
private Consumer<TrackAction> onAction = (r) -> {};
|
||||
|
||||
abstract AudioChannel createChannel();
|
||||
|
||||
/**
|
||||
* Initializes this music player
|
||||
*/
|
||||
public void init() {
|
||||
this.channel = createChannel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the consumer that will be called after playback is paused, stopped, resumed etc<br>
|
||||
* There can be only one for one music player
|
||||
*
|
||||
* @see TrackAction
|
||||
*/
|
||||
public void setOnAction(Consumer<TrackAction> onAction) {
|
||||
this.onAction = onAction;
|
||||
}
|
||||
|
||||
|
||||
/* Playback control */
|
||||
|
||||
/**
|
||||
* Starts playback of an audio file<br>
|
||||
* If it's not loaded, it will be loaded synchronously.
|
||||
*
|
||||
* @see MusicPlayer#pause()
|
||||
* @see MusicPlayer#stop()
|
||||
*/
|
||||
public void play(AudioFile audioFile) {
|
||||
if (audioFile != null)
|
||||
this.audioFile = audioFile;
|
||||
|
||||
if (playing)
|
||||
stop();
|
||||
|
||||
if (!audioFile.isLoaded()) {
|
||||
try {
|
||||
audioFile.load();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
unpause();
|
||||
}
|
||||
|
||||
public void unpause() {
|
||||
var sa = new short[960];
|
||||
var enc = audioFile.getEncoder();
|
||||
|
||||
player = api.createAudioPlayer(channel, enc, () -> enc.hasRemaining() ? sa : null);
|
||||
player.setOnStopped(this::onStop);
|
||||
player.startPlaying();
|
||||
|
||||
// TODO this may take a long time and it might glitch if it does
|
||||
onAction.accept(playing ? TrackAction.UNPAUSE : TrackAction.START);
|
||||
|
||||
playing = true;
|
||||
paused = false;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops playback and rewinds
|
||||
*/
|
||||
public void stop() {
|
||||
stopPlayback(false);
|
||||
audioFile.getEncoder().resetState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses playback
|
||||
*/
|
||||
public void pause() {
|
||||
stopPlayback(true);
|
||||
}
|
||||
|
||||
|
||||
/* Internal methods */
|
||||
|
||||
|
||||
private void stopPlayback(boolean pause) {
|
||||
if (player == null) return;
|
||||
|
||||
paused = pause;
|
||||
playing = false;
|
||||
|
||||
player.stopPlaying();
|
||||
player = null;
|
||||
|
||||
onStop();
|
||||
}
|
||||
|
||||
private void onStop() {
|
||||
if (paused) // paused
|
||||
onAction.accept(TrackAction.PAUSE);
|
||||
else if (playing) { // not paused and still playing
|
||||
onAction.accept(TrackAction.DURATION);
|
||||
playing = false;
|
||||
} else // not playing
|
||||
onAction.accept(TrackAction.STOP);
|
||||
}
|
||||
|
||||
public enum TrackAction {
|
||||
/**
|
||||
* A new track started playing
|
||||
*/
|
||||
START,
|
||||
/**
|
||||
* Song stopped playing because it played
|
||||
*/
|
||||
DURATION,
|
||||
/**
|
||||
* {@link MusicPlayer#stop()} was called, or started playing a new track
|
||||
*/
|
||||
STOP,
|
||||
/**
|
||||
* {@link MusicPlayer#pause()} was called
|
||||
*/
|
||||
PAUSE,
|
||||
/**
|
||||
* {@link MusicPlayer#unpause()} was called
|
||||
*/
|
||||
UNPAUSE
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package eu.m724.musicPlugin.player;
|
||||
|
||||
import de.maxhenkel.voicechat.api.audiochannel.AudioChannel;
|
||||
import org.bukkit.Location;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class StaticMusicPlayer extends MusicPlayer {
|
||||
private final Location location;
|
||||
|
||||
public StaticMusicPlayer(Location location) {
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
@Override
|
||||
AudioChannel createChannel() {
|
||||
var channel = api.createLocationalAudioChannel(
|
||||
UUID.randomUUID(),
|
||||
api.fromServerLevel(location.getWorld()),
|
||||
api.createPosition(location.getX(), location.getY(), location.getZ())
|
||||
);
|
||||
|
||||
channel.setCategory("music724");
|
||||
channel.setDistance(32);
|
||||
|
||||
return channel;
|
||||
}
|
||||
}
|
30
src/main/resources/plugin.yml
Normal file
30
src/main/resources/plugin.yml
Normal file
|
@ -0,0 +1,30 @@
|
|||
name: music-plugin
|
||||
version: 1
|
||||
main: eu.m724.musicPlugin.MusicPlugin
|
||||
api-version: '1.21'
|
||||
|
||||
depend: [voicechat]
|
||||
|
||||
libraries:
|
||||
- net.bramp.ffmpeg:ffmpeg:0.8.0
|
||||
- org.gagravarr:vorbis-java-core:0.8
|
||||
|
||||
commands:
|
||||
download:
|
||||
description: Downloads a file from URL. Run /convert next
|
||||
usage: /<command> <url>
|
||||
convert:
|
||||
description: Converts original downloaded file to some bitrate (in bps)
|
||||
usage: /<command> <hash> <bitrate>
|
||||
play:
|
||||
description: Plays the audio file of specified bitrate. Run /convert first
|
||||
usage: /<command> <hash> <bitrate>
|
||||
stop:
|
||||
description: Stops playback
|
||||
usage: /<command>
|
||||
pause:
|
||||
description: Pauses playback
|
||||
usage: /<command>
|
||||
resume:
|
||||
description: Resumes playback
|
||||
usage: /<command>
|
Loading…
Reference in a new issue