This commit is contained in:
Minecon724 2025-02-01 13:02:21 +01:00
commit 0c45358fa6
No known key found for this signature in database
GPG key ID: 3CCC4D267742C8E8
12 changed files with 292 additions and 151 deletions

View file

@ -90,6 +90,15 @@
<include>de.themoep:inventorygui</include>
</includes>
</artifactSet>
<filters>
<filter>
<artifact>*</artifact>
<excludes>
<exclude>META-INF/MANIFEST.MF</exclude>
<exclude>META-INF/maven/**</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>

View file

@ -8,6 +8,7 @@ import eu.m724.music_plugin.audio.storage.Downloader;
import eu.m724.music_plugin.item.ItemEvents;
import eu.m724.music_plugin.item.PmpCommand;
import eu.m724.music_plugin.item.speaker.BlockChecker;
import eu.m724.music_plugin.library.LibraryStorage;
import net.bramp.ffmpeg.FFmpeg;
import org.bukkit.NamespacedKey;
import org.bukkit.plugin.java.JavaPlugin;
@ -23,6 +24,7 @@ public final class MusicPlugin extends JavaPlugin {
private static AudioFileStorage AUDIO_FILE_STORAGE;
private static Converter CONVERTER;
private static Downloader DOWNLOADER;
private static LibraryStorage LIBRARY_STORAGE;
@Override
public void onEnable() {
@ -41,14 +43,19 @@ public final class MusicPlugin extends JavaPlugin {
service.registerPlugin(new MyVoicechatPlugin(api -> VOICECHAT_API = api));
var storagePath = getDataFolder().toPath().resolve("storage");
var audioStoragePath = storagePath.resolve("audio");
var libraryStoragePath = storagePath.resolve("library");
try {
Files.createDirectories(storagePath);
Files.createDirectories(audioStoragePath);
Files.createDirectories(libraryStoragePath);
} catch (IOException e) {
throw new RuntimeException(e);
}
AUDIO_FILE_STORAGE = new AudioFileStorage(storagePath);
DOWNLOADER = new Downloader(storagePath);
AUDIO_FILE_STORAGE = new AudioFileStorage(audioStoragePath);
DOWNLOADER = new Downloader(audioStoragePath);
LIBRARY_STORAGE = new LibraryStorage(libraryStoragePath);
try {
CONVERTER = new Converter(new FFmpeg());
@ -91,6 +98,10 @@ public final class MusicPlugin extends JavaPlugin {
return DOWNLOADER;
}
public static LibraryStorage getLibraryStorage() {
return LIBRARY_STORAGE;
}
public static NamespacedKey getNamespacedKey(String key) {
return new NamespacedKey(INSTANCE, key);
}

View file

@ -60,26 +60,21 @@ public class TestCommand implements CommandExecutor {
}
private void testConvert(Player player, String hash, int bitrate) {
try {
player.sendMessage("Converting...");
MusicPlugin.getStorage().convert(MusicPlugin.getConverter(), hash, bitrate).handle((f, ex) -> {
if (ex != null) {
ex.printStackTrace();
player.sendMessage("Error converting. See console for details.");
} else {
player.spigot().sendMessage(
new ComponentBuilder("Converted " + hash.substring(0, 7) + "... to " + bitrate + "bps! Click to play")
.event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Click to play")))
.event(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/test play " + hash + " " + bitrate))
.create()
);
}
return null;
});
} catch (IOException e) {
e.printStackTrace();
player.sendMessage("Error");
}
player.sendMessage("Converting...");
MusicPlugin.getStorage().convert(MusicPlugin.getConverter(), hash, bitrate).handle((f, ex) -> {
if (ex != null) {
ex.printStackTrace();
player.sendMessage("Error converting. See console for details.");
} else {
player.spigot().sendMessage(
new ComponentBuilder("Converted " + hash.substring(0, 7) + "... to " + bitrate + "bps! Click to play")
.event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Click to play")))
.event(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/test play " + hash + " " + bitrate))
.create()
);
}
return null;
});
}
private void testPlay(Player player, String hash, int bitrate) {

View file

@ -41,7 +41,10 @@ public class AudioFileStorage {
}
/**
* Get an audio file of given original hash and bitrate
* Get an audio file of given original hash and bitrate<br>
* <br>
* <strong>WARNING</strong> Whether the file exists is not checked.<br>
* Do it yourself - simply check if the file exists.
*
* @param hash the hex sha256 hash of the original
* @param bitrate the bitrate in bps like 32000
@ -77,13 +80,18 @@ public class AudioFileStorage {
* @param bitrate the target bitrate in bps
* @return the future with the new file
*/
public CompletableFuture<Path> convert(Converter converter, String hash, int bitrate) throws IOException {
public CompletableFuture<Path> convert(Converter converter, String hash, int bitrate) {
var file = get(hash, bitrate);
if (Files.exists(file)) return CompletableFuture.completedFuture(file);
var og = getOriginal(hash);
if (!Files.exists(og)) return null;
return converter.convert(og, file, bitrate).thenApply(v -> file);
// TODO maybe do that in Converter?
try {
return converter.convert(og, file, bitrate);
} catch (IOException e) {
return CompletableFuture.failedFuture(e);
}
}
}

View file

@ -34,7 +34,7 @@ public class Downloader {
public CompletableFuture<String> download(URI uri) {
DebugLogger.fine("About to download from %s", uri);
// TODO fix this. Yes this is the only way. sendAsync doesn't block for some reason. I don't know why.
// TODO sendAsync doesn't block for some reason
return CompletableFuture.supplyAsync(() -> {
try (
var client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build()

View file

@ -20,7 +20,7 @@ public class ItemEvents implements Listener {
if (event.getItem() == null) return;
var player = PortableMediaPlayers.get(event.getItem());
var player = PortableMediaPlayers.fromItemStack(event.getItem());
if (player == null) return;
System.out.println("A player used");
@ -31,7 +31,7 @@ public class ItemEvents implements Listener {
player.next();
} else {
System.out.println("no shift + lmb: rpevous song");
player.prev();
player.previous();
}
} else {
if (right) {
@ -65,7 +65,7 @@ public class ItemEvents implements Listener {
@EventHandler
public void onDrop(PlayerDropItemEvent event) {
var player = PortableMediaPlayers.get(event.getItemDrop().getItemStack());
var player = PortableMediaPlayers.fromItemStack(event.getItemDrop().getItemStack());
if (player == null) return;
System.out.println("A playe rdropped");
@ -75,7 +75,7 @@ public class ItemEvents implements Listener {
@EventHandler
public void onPickup(EntityPickupItemEvent event) {
var player = PortableMediaPlayers.get(event.getItem().getItemStack());
var player = PortableMediaPlayers.fromItemStack(event.getItem().getItemStack());
if (player == null) return;
System.out.println("A player puckuperd");
@ -86,7 +86,7 @@ public class ItemEvents implements Listener {
@EventHandler
public void onMove(InventoryMoveItemEvent event) {
if (event.getDestination().getType() == InventoryType.PLAYER) {
var player = PortableMediaPlayers.get(event.getItem());
var player = PortableMediaPlayers.fromItemStack(event.getItem());
if (player == null) return;
System.out.println("A player storaged :((");

View file

@ -1,20 +1,19 @@
package eu.m724.music_plugin.item;
import eu.m724.music_plugin.MusicPlugin;
import eu.m724.music_plugin.library.Track;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.Arrays;
public class PmpCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
var player = (Player) sender;
var pmp = PortableMediaPlayers.get(player.getInventory().getItemInMainHand());
var pmp = PortableMediaPlayers.fromItemStack(player.getInventory().getItemInMainHand());
var action = args.length > 0 ? args[0] : null;
@ -22,18 +21,7 @@ public class PmpCommand implements CommandExecutor {
if (pmp != null) {
sender.sendMessage("ID: " + pmp.id);
sender.sendMessage("Premium: " + pmp.premium);
var capacityStr = "";
var h = pmp.storageSeconds % 3600;
var m = (pmp.storageSeconds - (h * 3600)) % 60;
var s = pmp.storageSeconds - (h * 3600) - (m * 60);
if (h > 0) {
capacityStr += h + "h ";
}
capacityStr += m + "m " + s + "s ";
sender.sendMessage("Capacity: " + capacityStr);
sender.sendMessage("Capacity: " + secondsToHms(pmp.storageSeconds));
sender.sendMessage("Bitrate: %d Kbps".formatted(pmp.audioBitrate / 1000));
}
}
@ -41,23 +29,57 @@ public class PmpCommand implements CommandExecutor {
if ("create".equals(action)) {
pmp = PortableMediaPlayer.create(args[1].equals("yes"), String.join(" ", Arrays.asList(args).subList(2, args.length)));
player.getInventory().addItem(pmp.getItemStack());
} else if ("play".equals(action)) {
} else if ("add".equals(action)) {
if (pmp != null) {
var storage = MusicPlugin.getStorage();
var library = pmp.getLibrary();
library.addTrack(new Track(args[1]));
player.sendMessage("Added track");
/*var storage = MusicPlugin.getStorage();
var path = storage.get(args[1], Integer.parseInt(args[2]));
try {
pmp.play(path.toFile());
} catch (IOException e) {
throw new RuntimeException(e);
}
}*/
} else {
player.sendMessage("You must hold a Portable Music Player");
}
} else if ("play".equals(action)) {
if (pmp != null) {
pmp.play();
player.sendMessage("Started");
} else {
player.sendMessage("You must hold a Portable Music Player");
}
} else if ("skip".equals(action)) {
if (pmp != null) {
pmp.next();
player.sendMessage("Skipped");
} else {
player.sendMessage("You must hold a Portable Music Player");
}
} else {
player.sendMessage("create | play");
player.sendMessage("create | add | play | skip");
}
return true;
}
private String secondsToHms(int seconds) {
var sb = new StringBuilder();
var hours = seconds / 3600;
if (hours > 0) {
sb.append(hours).append("h ");
seconds = seconds % 3600;
}
sb.append(seconds / 60).append("m ");
sb.append(seconds % 60).append("s");
return sb.toString();
}
}

View file

@ -5,37 +5,28 @@ import eu.m724.music_plugin.MusicPlugin;
import eu.m724.music_plugin.audio.player.OpusFilePlayer;
import eu.m724.music_plugin.audio.player.TrackEvent;
import eu.m724.music_plugin.item.speaker.Speaker;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.ItemFlag;
import eu.m724.music_plugin.library.Library;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataType;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ThreadLocalRandom;
public class PortableMediaPlayer {
static final NamespacedKey idKey = MusicPlugin.getNamespacedKey("player_id");
static final NamespacedKey dataKey = MusicPlugin.getNamespacedKey("player_data");
// TODO getters for all that
public final int id;
// TODO configurable
// TODO make those two configurable
public final int storageSeconds;
public final int audioBitrate;
public final boolean premium;
public final String engraving;
private final Library library;
private OpusFilePlayer player; // TODO rename?
private Speaker<?> speaker;
private Speaker<?> linkedSpeaker;
PortableMediaPlayer(int id, int storageSeconds, int audioBitrate, boolean premium, String engraving) {
this.id = id;
@ -43,55 +34,45 @@ public class PortableMediaPlayer {
this.audioBitrate = audioBitrate;
this.premium = premium;
this.engraving = engraving;
// TODO there should be a better way. Like load if needed?
this.library = MusicPlugin.getLibraryStorage().load(id);
}
public static PortableMediaPlayer create(boolean premium, String engraving) {
return new PortableMediaPlayer(ThreadLocalRandom.current().nextInt(), 600, 32000, premium, engraving);
}
public void setSpeaker(Speaker<?> speaker) {
if (speaker.equals(this.speaker))
return;
if (this.speaker != null) {
// no need to pause if no need to pause
this.speaker.setDestroyCallback(c -> {});
this.speaker.destroy();
}
speaker.setDestroyCallback(v -> {
System.out.println("spekar rip");
if (player != null)
player.pause();
this.speaker = null;
public void play() {
DebugLogger.fine("play() called");
library.getPlaying().getPath(audioBitrate).thenAccept(p -> {
DebugLogger.finer("accepted");
try {
play(p.toAbsolutePath().toFile());
} catch (IOException e) {
e.printStackTrace();
throw new CompletionException(e);
}
});
this.speaker = speaker;
if (player != null)
player.setChannel(speaker.getChannel().getAudioChannel());
}
public void play(File file) throws IOException {
DebugLogger.finer("pmp play");
if (speaker == null) return;
private void play(File file) throws IOException {
DebugLogger.finer("playing a file: %s", file.toString());
if (linkedSpeaker == null) return;
//played = new Track(audioFile);
if (player != null)
player.stop();
this.player = new OpusFilePlayer(file, speaker.getChannel().getAudioChannel());
this.player = new OpusFilePlayer(file, linkedSpeaker.getChannel().getAudioChannel());
player.setOnTrackEvent(event -> {
if (event == TrackEvent.START) {
DebugLogger.finer("I detected track START");
//played.unpause();
} else if (event == TrackEvent.STOP) {
DebugLogger.finer("I detected track STOP");
//played.pause();
//played.hint(played.file.getEncoder().getFrame());
} else if (event == TrackEvent.END) {
DebugLogger.finer("I detected track END");
//next();
next();
}
});
@ -119,58 +100,46 @@ public class PortableMediaPlayer {
player.stop();
}
// TODO
void prev() {
DebugLogger.fine("pmp previous (does nothing)");
void previous() {
// TODO seek to beginning
if (library.previous() != null)
play();
}
// TODO
void next() {
System.out.println("pmp next (does nothing)");
if (library.next() != null)
play();
}
/* Item functions */
/* Getters / Setters */
public ItemStack getItemStack() {
var is = new ItemStack(premium ? Material.GOLD_INGOT : Material.IRON_INGOT);
var meta = is.getItemMeta();
public void setSpeaker(Speaker<?> speaker) {
if (speaker.equals(this.linkedSpeaker))
return;
if (premium) {
var hue = (id & 0xFFFFFF) / (float) 0xFFFFFF;
var saturation = (id >> 24) & 0xF;
var color = Color.getHSBColor(hue, 0.84f + saturation / 15.0f, 1.0f);
meta.setItemName(ChatColor.of(color) + "Portable Music Player");
if (engraving != null)
meta.setLore(List.of(ChatColor.of(color.darker()) + engraving));
} else {
meta.setItemName("Portable Music Player");
if (engraving != null) // custom colors only in premium
meta.setLore(List.of(ChatColor.GRAY + ChatColor.stripColor(engraving)));
if (this.linkedSpeaker != null) {
// no need to pause if no need to pause
this.linkedSpeaker.setDestroyCallback(c -> {});
this.linkedSpeaker.destroy();
}
meta.addEnchant(Enchantment.UNBREAKING, 1, false);
meta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
meta.getPersistentDataContainer().set(idKey, PersistentDataType.INTEGER, id);
meta.getPersistentDataContainer().set(dataKey, PersistentDataType.BYTE_ARRAY, getData());
speaker.setDestroyCallback(v -> {
DebugLogger.finer("linked speaker destroyed");
if (player != null)
player.pause();
this.linkedSpeaker = null;
});
is.setItemMeta(meta);
return is;
this.linkedSpeaker = speaker;
if (player != null)
player.setChannel(speaker.getChannel().getAudioChannel());
}
private byte[] getData() {
var buffer = ByteBuffer.allocate(11 + engraving.length());
buffer.put((byte) 0); // data format version
buffer.put((byte) (premium ? 1 : 0));
public ItemStack getItemStack() {
return PortableMediaPlayers.getItemStack(this);
}
buffer.putShort((short) storageSeconds); // make int if 18 hours is not enough. or store as minutes
buffer.put((byte) (audioBitrate / 1000));
buffer.put((byte) engraving.length());
buffer.put(engraving.getBytes(StandardCharsets.UTF_8));
return buffer.array();
public Library getLibrary() {
return this.library;
}
}

View file

@ -1,26 +1,36 @@
package eu.m724.music_plugin.item;
import eu.m724.music_plugin.MusicPlugin;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataType;
import java.awt.*;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
// This class stores PortableMediaPlayer instances so that there's no hassle of working with a new one every time
public class PortableMediaPlayers {
private static Map<Integer, PortableMediaPlayer> players = new HashMap<>();
static final NamespacedKey idKey = MusicPlugin.getNamespacedKey("player_id");
static final NamespacedKey dataKey = MusicPlugin.getNamespacedKey("player_data");
public static PortableMediaPlayer get(Integer id) {
private static final Map<Integer, PortableMediaPlayer> players = new HashMap<>();
public static PortableMediaPlayer fromId(Integer id) {
return players.get(id);
}
public static PortableMediaPlayer get(ItemStack itemStack) {
public static PortableMediaPlayer fromItemStack(ItemStack itemStack) {
var meta = itemStack.getItemMeta();
if (meta == null) return null;
var id = meta.getPersistentDataContainer().get(PortableMediaPlayer.idKey, PersistentDataType.INTEGER);
var id = meta.getPersistentDataContainer().get(idKey, PersistentDataType.INTEGER);
if (id == null) return null;
// check cache
@ -30,7 +40,7 @@ public class PortableMediaPlayers {
// if not cached
var data = meta.getPersistentDataContainer().get(PortableMediaPlayer.dataKey, PersistentDataType.BYTE_ARRAY);
var data = meta.getPersistentDataContainer().get(dataKey, PersistentDataType.BYTE_ARRAY);
if (data == null) return null;
player = fromData(id, data);
@ -57,4 +67,47 @@ public class PortableMediaPlayers {
return new PortableMediaPlayer(id, storageSeconds, audioBitrate, premium, engraving);
}
private static byte[] getData(PortableMediaPlayer player) {
var buffer = ByteBuffer.allocate(11 + player.engraving.length());
buffer.put((byte) 0); // data format version
buffer.put((byte) (player.premium ? 1 : 0));
buffer.putShort((short) player.storageSeconds); // make int if 18 hours is not enough. or store as minutes
buffer.put((byte) (player.audioBitrate / 1000));
buffer.put((byte) player.engraving.length());
buffer.put(player.engraving.getBytes(StandardCharsets.UTF_8));
return buffer.array();
}
public static ItemStack getItemStack(PortableMediaPlayer player) {
var is = new ItemStack(player.premium ? Material.GOLD_INGOT : Material.IRON_INGOT);
var meta = is.getItemMeta();
if (player.premium) {
var hue = (player.id & 0xFFFFFF) / (float) 0xFFFFFF;
var saturation = (player.id >> 24) & 0xF;
var color = Color.getHSBColor(hue, 0.84f + saturation / 15.0f, 1.0f);
meta.setItemName(ChatColor.of(color) + "Portable Music Player");
if (player.engraving != null)
meta.setLore(java.util.List.of(ChatColor.of(color.darker()) + player.engraving));
} else {
meta.setItemName("Portable Music Player");
if (player.engraving != null) // custom colors only in premium
meta.setLore(List.of(ChatColor.GRAY + ChatColor.stripColor(player.engraving)));
}
meta.addEnchant(Enchantment.UNBREAKING, 1, false);
meta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
meta.getPersistentDataContainer().set(idKey, PersistentDataType.INTEGER, player.id);
meta.getPersistentDataContainer().set(dataKey, PersistentDataType.BYTE_ARRAY, getData(player));
is.setItemMeta(meta);
return is;
}
}

View file

@ -1,13 +1,21 @@
package eu.m724.music_plugin.library;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class Library {
private List<Track> tracks = new ArrayList<>();
private int playingTrack;
private final int id;
private final List<Track> tracks = new ArrayList<>();
private int playingTrack = 0;
public Library(int id) {
this.id = id;
}
public int getId() {
return this.id;
}
public void addTrack(Track track) {
tracks.add(track);
@ -21,14 +29,27 @@ public class Library {
return tracks.get(playingTrack);
}
public static Library load(InputStream inputStream) throws IOException {
var tracks = new ArrayList<Track>();
byte[] bytes = new byte[32];
while (inputStream.read(bytes) == 32) {
tracks.add(new Track(bytes));
/**
* Previous Track
* @return the previous Track or null if this was the first
*/
public Track previous() {
if (playingTrack > 0) {
return tracks.get(--playingTrack);
} else {
return null;
}
}
return new Library()
/**
* Next Track
* @return the next Track or null if this was the last
*/
public Track next() {
if (playingTrack + 1 < tracks.size()) {
return tracks.get(++playingTrack);
} else {
return null;
}
}
}

View file

@ -0,0 +1,21 @@
package eu.m724.music_plugin.library;
import com.google.common.primitives.Ints;
import eu.m724.music_plugin.DebugLogger;
import java.nio.file.Path;
import java.util.HexFormat;
public class LibraryStorage {
private final Path libraryStoragePath;
public LibraryStorage(Path libraryStoragePath) {
this.libraryStoragePath = libraryStoragePath;
}
public Library load(int id) {
DebugLogger.fine("Now \"loading\" library %s", HexFormat.of().formatHex(Ints.toByteArray(id)));
// TODO
return new Library(id);
}
}

View file

@ -1,20 +1,52 @@
package eu.m724.music_plugin.library;
import eu.m724.music_plugin.DebugLogger;
import eu.m724.music_plugin.MusicPlugin;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HexFormat;
import java.util.concurrent.CompletableFuture;
public class Track {
/** SHA-256 hash as 32 bytes */
private final byte[] hash;
/**
* Creates a new Track from an SHA-256 hash bytes
* @param hash the 32 bytes of the hash
*/
public Track(byte[] hash) {
assert hash.length == 32;
this.hash = hash;
}
/**
* Creates a new Track from an SHA-256 hex encoded hash
* @param hash the hex encoded hash
*/
public Track(String hash) {
this(HexFormat.of().parseHex(hash));
}
/**
* @return the SHA-256 hash as 32 bytes
*/
public byte[] getHash() {
return hash;
}
public String getHashHex() {
return HexFormat.of().formatHex(hash);
}
public CompletableFuture<Path> getPath(int bitrate) {
var path = MusicPlugin.getStorage().get(getHashHex(), bitrate);
if (Files.isRegularFile(path))
return CompletableFuture.completedFuture(path);
DebugLogger.fine("Need to convert to %d", bitrate);
return MusicPlugin.getStorage().convert(MusicPlugin.getConverter(), getHashHex(), bitrate);
}
}