Intial commit 2

This commit is contained in:
Minecon724 2024-12-15 19:06:44 +01:00
commit cffaf80c71
Signed by: Minecon724
GPG key ID: 3CCC4D267742C8E8
14 changed files with 905 additions and 0 deletions

113
.gitignore vendored Normal file
View 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
View 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>

View 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;
}
}

View 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);
}
}

View 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());
}
}

View 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;
}

View 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; }
}

View file

@ -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));
}
}

View 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);
}
});
}
}

View 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; }
}

View file

@ -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;
}
}

View 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
}
}

View file

@ -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;
}
}

View 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>