diff --git a/build.gradle b/build.gradle index 137de62f..3cfdd7c6 100644 --- a/build.gradle +++ b/build.gradle @@ -65,7 +65,7 @@ dependencies { // DEPENDENCIES shadow "com.github.WaterMediaTeam:videolan-natives:$vlcj_natives_version" - shadow "com.github.WaterMediaTeam:ffmpeg4j:$ffmpeg4j_version" + shadow "com.github.WaterMediaTeam:ytdl-java:$jyd_version" shadow "net.sf.sevenzipjbinding:sevenzipjbinding:16.02-2.01" shadow "net.sf.sevenzipjbinding:sevenzipjbinding-all-platforms:16.02-2.01" shadow project(":lib-vlcj") @@ -104,6 +104,10 @@ shadowJar { // Add relocation rules for each dependency relocate 'com.github', 'me.lib720' relocate 'com.alibaba', 'me.lib720.alibaba' + relocate 'com.fasterxml', 'me.lib720' + relocate 'org.apache.commons', 'me.lib720.apache' + relocate 'org.tukaani', 'me.lib720.tukaani' + relocate 'uk.co.caprica', 'me.lib720.caprica' exclude "META-INF/versions/**" exclude "META-INF/proguard/**" diff --git a/gradle.properties b/gradle.properties index 48c4c6d2..298f0b2e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -24,13 +24,13 @@ curseforgeid=869524 ######################### #### Version ranges #### ####################### -minecraftrange=[1.18.2,) -forgerange=[40.2.21,) -forgefmlrange=[40,) +minecraftrange=[1.16.5,) +forgerange=[36,) +forgefmlrange=[36,) neorange=[21,) neofmlrange=[3,) fabricrange=>=0.14 -javarange=>=17 +javarange=>=8 ############################## #### Project information #### @@ -40,12 +40,8 @@ authors=SrRapero720, Goedix authors_list=["SrRapero720", "Goedix"] contributors=ZenoArrows, zFERDQFREZrzfq, Kekscussino, cyyynthia contributors_list=["ZenoArrows", "zFERDQFREZrzfq", "Kekscussino", "cyyynthia"] -credits=Powered by FFMPEG and VideoLAN, special thanks to Caprica for vlcj -description=Library and API providing multimedia processing, source patching, rendering, and math tools. - -############################ -#### Libraries Version #### -########################## +credits=VideoLAN Team for libVLC, Caprica for made VLCJ, Linus torvalds for make us suffer with Linux +description=Library with an API using VLC for multimedia integration with Minecraft. Works also standalone #################### #### Libraries #### @@ -54,8 +50,7 @@ modloaders_version=1.0.1 fabric_version=0.16.0 forge_version=unknown jyd_version=3.2.3 -ffmpeg4j_version=master-SNAPSHOT -vlcj_natives_version=4.11.0 +vlcj_natives_version=4.9.0 commoncompress_version=1.26.1 commonslang3_version=3.12.0 commonsio_version=2.7 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index df97d72b..09523c0e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/lib-vlcj/src/main/java/uk/co/caprica/vlcj/player/base/State.java b/lib-vlcj/src/main/java/uk/co/caprica/vlcj/player/base/State.java index 32e56512..fbda1f3d 100644 --- a/lib-vlcj/src/main/java/uk/co/caprica/vlcj/player/base/State.java +++ b/lib-vlcj/src/main/java/uk/co/caprica/vlcj/player/base/State.java @@ -36,7 +36,7 @@ public enum State { ENDED (6), ERROR (7); - private static final Map INT_MAP = new HashMap<>(); + private static final Map INT_MAP = new HashMap(); static { for (State event : State.values()) { diff --git a/src/main/java/me/srrapero720/watermedia/Main.java b/src/main/java/me/srrapero720/watermedia/Main.java index 00245423..404ad57e 100644 --- a/src/main/java/me/srrapero720/watermedia/Main.java +++ b/src/main/java/me/srrapero720/watermedia/Main.java @@ -1,8 +1,7 @@ package me.srrapero720.watermedia; -import org.watermedia.api.MathAPI; -import org.watermedia.tools.IOTool; -import org.watermedia.WaterMedia; +import me.srrapero720.watermedia.api.math.MathAPI; +import me.srrapero720.watermedia.tools.IOTool; import javax.swing.*; import javax.swing.border.EmptyBorder; @@ -180,7 +179,7 @@ public void run() { if (crashReportFiles == null || crashReportFiles.length == 0) throw new NullPointerException("No such directory or is empty"); -// Arrays.sort(crashReportFiles, LastModifiedFileComparator.LASTMODIFIED_REVERSE); + Arrays.sort(crashReportFiles, LastModifiedFileComparator.LASTMODIFIED_REVERSE); File crashReport = crashReportFiles[0]; LOGGER.info("Collected files: " + Arrays.toString(crashReportFiles)); diff --git a/src/main/java/me/srrapero720/watermedia/WaterMedia.java b/src/main/java/me/srrapero720/watermedia/WaterMedia.java new file mode 100644 index 00000000..a20f7b91 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/WaterMedia.java @@ -0,0 +1,113 @@ +package me.srrapero720.watermedia; + +import com.sun.jna.Platform; +import me.srrapero720.watermedia.api.WaterMediaAPI; +import me.srrapero720.watermedia.tools.DataTool; +import me.srrapero720.watermedia.tools.JarTool; +import me.srrapero720.watermedia.loader.ILoader; +import me.srrapero720.watermedia.loader.IModule; +import me.srrapero720.watermedia.tools.PairTool; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.MarkerManager; + +import java.util.*; + +public final class WaterMedia { + private static final Marker IT = MarkerManager.getMarker("Bootstrap"); + + public static final String ID = "watermedia"; + public static final String NAME = "WATERMeDIA"; + public static final Logger LOGGER = LogManager.getLogger(ID); + private static final Set MODULES = new HashSet<>(); + + private static final PairTool NO_BOOT = getArgument("watermedia.disableBoot"); + private static final PairTool HARDFAIL = getArgument("watermedia.hardFail"); + private static ILoader bootstrap; + private static WaterMedia instance; + + private WaterMedia() {} + + public static WaterMedia prepare(ILoader boot) { + if (boot == null) throw new NullPointerException("Bootstrap is null"); + if (instance != null) throw new NullPointerException(NAME + " is already prepared"); + LOGGER.info(IT, "Preparing '{}' on '{}'", NAME, boot.name()); + LOGGER.info(IT, "WaterMedia version '{}'", JarTool.readString("/watermedia/version.cfg")); + LOGGER.info(IT, "OS Detected: {} ({})", System.getProperty("os.name"), Platform.ARCH); + + if (NO_BOOT.getRight()) + LOGGER.warn(IT, "No boot was enabled, skipping bootstrap"); + + if (HARDFAIL.getRight()) + LOGGER.warn(IT, "HardFail mode was enable, {} will crash at the minimum exception", NAME); + + if (!boot.client() && !boot.name().contains("Fabric")) + throw new UnsupportedSideException(); + + + WaterMedia.bootstrap = boot; + return instance = new WaterMedia(); + } + + public static void register(IModule module) { + MODULES.add(module); + } + + public static Set modules() { + return MODULES; + } + + public void start() throws Exception { + if (NO_BOOT.getRight()) return; + if (!bootstrap.client()) return; + + List modules = DataTool.toList(ServiceLoader.load(WaterMediaAPI.class)); + modules.sort(Comparator.comparingInt(e -> e.priority().ordinal())); + + for (WaterMediaAPI m: modules) { + LOGGER.info(IT, "Starting {}", m.getClass().getSimpleName()); + if (!m.prepare(bootstrap)) { + LOGGER.warn(IT, "Module {} refuses to be loaded, skipping", m.getClass().getSimpleName()); + continue; + } + m.start(bootstrap); + LOGGER.info(IT, "Module {} loaded successfully", m.getClass().getSimpleName()); + } + LOGGER.info(IT, "Startup finished"); + } + + public static ILoader getLoader() { return bootstrap; } + + + public static String asResource(String path) { + return WaterMedia.ID + ":" + path; + } + + public static PairTool getArgument(String argument) { + return new PairTool<>(argument, Boolean.parseBoolean(System.getProperty(argument))); + } + + public static class UnsupportedSideException extends RuntimeException { + public UnsupportedSideException() { + super(NAME + " CANNOT be installed on SERVER-SIDE. Please remove " + NAME + " from the server, and keep it on client"); + LOGGER.fatal(IT, "############################## ILLEGAL ENVIRONMENT ######################################"); + LOGGER.fatal(IT, "{} is not designed to work on server-side, please remove it from server and keep it on client", NAME); + LOGGER.fatal(IT, "Dependent mods can work without {} ON SERVERS, remember keep the mod ONLY ON CLIENT-SIDE", NAME); + LOGGER.fatal(IT, "if dependent mods throws exceptions ON SERVER asking for WATERMeDIA, report it to the creators"); + LOGGER.fatal(IT, "############################## ILLEGAL ENVIRONMENT ######################################"); + } + } + + public static class UnsupportedTLException extends Exception { + public UnsupportedTLException() { + super("TLauncher is NOT supported by " + NAME + ", please stop using it (and consider safe alternatives like SKLauncher or MultiMC)"); + LOGGER.fatal(IT, "############################## ILLEGAL LAUNCHER DETECTED ######################################"); + LOGGER.fatal(IT, "{} refuses to load sensitive modules in a INFECTED launcher, please stop using TLauncher dammit", NAME); + LOGGER.fatal(IT, "Because TLauncher infects sensitive files (which {} includes) and we prefer avoid any risk", NAME); + LOGGER.fatal(IT, "Consider use safe alternative like SKLauncher or BUY the game and use the CurseForge Launcher"); + LOGGER.fatal(IT, "And please avoid Feather Launcher, TLauncher Legacy or any CRACKED LAUNCHER WITH A BAD REPUTATION"); + LOGGER.fatal(IT, "############################## ILLEGAL LAUNCHER DETECTED ######################################"); + } + } +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/api/MediaContext.java b/src/main/java/me/srrapero720/watermedia/api/MediaContext.java deleted file mode 100644 index 8715f6b4..00000000 --- a/src/main/java/me/srrapero720/watermedia/api/MediaContext.java +++ /dev/null @@ -1,50 +0,0 @@ -package me.srrapero720.watermedia.api; - -public interface MediaContext { - - /** - * Provides the context unique identifier (like a mod id) - * This helps on debugging to find who do the wrong call - * @return a unique identifier in range of [a-z][1-9] - */ - String id(); - - /** - * Elegant name for the logger and crash report handlers, it can be not unique - * @return a fancy string with the name of your project - */ - String name(); - - /** - * Preference is established for lower qualities when the exact wanted quality is missing - * @return true if it should prefer lower qualities, false otherwise - */ - boolean preferLowerQuality(); - - /** - * Very quick and simple implementation of a MediaModContext, no answers, no questions, just a context - */ - final class Simple implements MediaContext { - private final String id; - private final String name; - public Simple(String id, String name) { - this.id = id; - this.name = name; - } - - @Override public String id() { return id; } - @Override public String name() { return name; } - @Override public boolean preferLowerQuality() { return false; } - } - - final class Static { - private final String id; - private final String name; - private final boolean prefferLowerQuality; - public Static(String id, String name, boolean preferLowerQuality) { - this.id = id; - this.name = name; - this.prefferLowerQuality = preferLowerQuality; - } - } -} diff --git a/src/main/java/org/watermedia/api/WaterMediaAPI.java b/src/main/java/me/srrapero720/watermedia/api/WaterMediaAPI.java similarity index 57% rename from src/main/java/org/watermedia/api/WaterMediaAPI.java rename to src/main/java/me/srrapero720/watermedia/api/WaterMediaAPI.java index ad4c6af6..751da1ef 100644 --- a/src/main/java/org/watermedia/api/WaterMediaAPI.java +++ b/src/main/java/me/srrapero720/watermedia/api/WaterMediaAPI.java @@ -1,6 +1,8 @@ -package org.watermedia.api; +package me.srrapero720.watermedia.api; -import org.watermedia.WaterMedia; + +import me.srrapero720.watermedia.api.math.MathAPI; +import me.srrapero720.watermedia.loader.ILoader; /** * Boostrap class. @@ -20,14 +22,14 @@ public abstract class WaterMediaAPI { * @return true if can bot, otherwise false * @throws Exception the module fails to prepare itself, indicating a broken state */ - public abstract boolean prepare(WaterMedia.ILoader bootCore) throws Exception; + public abstract boolean prepare(ILoader bootCore) throws Exception; /** * Starts module * @throws Exception on some cases a exception breaks WATERMeDIA's intended behavior and making * dependant mods life harder. */ - public abstract void start(WaterMedia.ILoader bootCore) throws Exception; + public abstract void start(ILoader bootCore) throws Exception; /** * Releases all module resources @@ -40,12 +42,26 @@ public abstract class WaterMediaAPI { * and BENCHMARK what name indicates */ public enum Priority { - OVERRIDE, HIGHEST, HIGH, NORMAL, LOW, LOWEST, - MONITOR + MONITOR, + BENCHMARK + } + + /** + * Returns the color of a specified level in range of 1 ~ 255 + * @param a alpha level + * @param r red color + * @param g green color + * @param b blue color + * @deprecated IMPORANT: this method a bouncer of {@link MathAPI#argb(int, int, int, int)}. Please switch as soon as possible + * @return color int + */ + @Deprecated(forRemoval = true) + public static int math_colorARGB(int a, int r, int g, int b) { + return MathAPI.argb(a, r, g, b); } } \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/api/cache/CacheAPI.java b/src/main/java/me/srrapero720/watermedia/api/cache/CacheAPI.java new file mode 100644 index 00000000..8ed262e4 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/cache/CacheAPI.java @@ -0,0 +1,171 @@ +package me.srrapero720.watermedia.api.cache; + +import me.srrapero720.watermedia.api.WaterMediaAPI; +import me.srrapero720.watermedia.tools.DataTool; +import me.srrapero720.watermedia.core.config.WaterConfig; +import me.srrapero720.watermedia.loader.ILoader; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.MarkerManager; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import static me.srrapero720.watermedia.WaterMedia.LOGGER; + +@SuppressWarnings({"unused"}) +public class CacheAPI extends WaterMediaAPI { + private static final Marker IT = MarkerManager.getMarker(CacheAPI.class.getSimpleName()); + private static final Map ENTRIES = new HashMap<>(); + + private static File dir; + private static File index; + private static boolean init = false; + private static boolean released = false; + + + private static boolean refreshAll() { + try(DataOutputStream out = new DataOutputStream(new GZIPOutputStream(Files.newOutputStream(index.toPath())))) { + out.writeInt(ENTRIES.size()); + + for (Map.Entry mapEntry : ENTRIES.entrySet()) { + Entry entry = mapEntry.getValue(); + out.writeUTF(entry.getUrl()); + out.writeUTF(entry.getTag() == null ? "" : entry.getTag()); + out.writeLong(entry.getTime()); + out.writeLong(entry.getExpireTime()); + } + + return true; + } catch (IOException e) { LOGGER.error(IT, "Failed to refresh cache index", e); } + return false; + } + + private static File entry$getFile(String url) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + return new File(dir, DataTool.encodeHexString(digest.digest(url.getBytes(StandardCharsets.UTF_8)))); + } catch (NoSuchAlgorithmException e) { LOGGER.error(IT, "Failed to initalize digest", e); } + + // Fallback to old naming + return new File(dir, Base64.getEncoder().encodeToString(url.getBytes())); + } + + public static void saveFile(String url, String tag, long time, long expireTime, byte[] data) { + synchronized (ENTRIES) { + Entry entry = new Entry(url, tag, time, expireTime); + boolean saved = false; + File file = entry$getFile(entry.url); + + try (OutputStream out = Files.newOutputStream(file.toPath())) { + out.write(data); + saved = true; + } catch (Exception e) { LOGGER.error(IT, "Failed to save cache file {}", url, e); } + + // SAVE INDEX FIST + if (saved && refreshAll()) { + ENTRIES.put(url, entry); + } else if (file.exists()) { + if (file.delete()) LOGGER.warn(IT, "Cannot delete unsaved entry file of '{}' located in '{}'", url, file.toString()); + } + } + } + + public static Entry getEntry(String url) { + synchronized (ENTRIES) { + return ENTRIES.get(url); + } + } + + public static void updateEntry(Entry fresh) { + synchronized (ENTRIES) { + ENTRIES.put(fresh.url, fresh); + } + } + + public static void deleteEntry(String url) { + synchronized (ENTRIES) { + ENTRIES.remove(url); + File file = entry$getFile(url); + if (file.exists()) { + if (file.delete()) LOGGER.warn(IT, "Cannot delete entry file of '{}' located in '{}'", url, file.toString()); + } + } + } + + @Override + public Priority priority() { + return Priority.HIGHEST; + } + + @Override + public boolean prepare(ILoader bootCore) { + dir = new File(WaterConfig.vlcInstallPath, "cache/pictures"); + index = new File(dir, "index"); + + if (!released) { + LOGGER.error(IT, "Failed due boot API while is not released, boot cancelled"); + return false; + } + + return !init; + } + + @Override + public void start(ILoader bootCore) throws Exception { + if (!dir.exists() && !dir.mkdirs()) throw new IOException("Cannot make necessary dirs for proper storing"); + if (index.exists()) { + try (DataInputStream stream = new DataInputStream(new GZIPInputStream(Files.newInputStream(index.toPath())))) { + int length = stream.readInt(); + + for (int i = 0; i < length; i++) { + String url = stream.readUTF(); + String tag = stream.readUTF(); + long time = stream.readLong(); + long expireTime = stream.readLong(); + Entry entry = new Entry(url, !tag.isEmpty() ? tag : null, time, expireTime); + ENTRIES.put(entry.getUrl(), entry); + } + } catch (Exception e) { + LOGGER.error(IT, "Failed to load indexes", e); + } + } + init = true; + } + + @Override + public void release() { + refreshAll(); + released = true; + } + + public static final class Entry { + private final String url; + private String tag; + private long time; + private long expireTime; + + public Entry(String url, String tag, long time, long expireTime) { + this.url = url; + this.tag = tag; + this.time = time; + this.expireTime = expireTime; + } + + public void setTag(String tag) { this.tag = tag; } + public void setTime(long time) { this.time = time; } + public void setExpireTime(long expireTime) { this.expireTime = expireTime; } + public String getUrl() { return url; } + public String getTag() { return tag; } + public long getTime() { return time; } + public long getExpireTime() { return expireTime; } + public File getFile() { return entry$getFile(url); } + } +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/api/image/ImageAPI.java b/src/main/java/me/srrapero720/watermedia/api/image/ImageAPI.java index e2e9c7bc..4c052fb4 100644 --- a/src/main/java/me/srrapero720/watermedia/api/image/ImageAPI.java +++ b/src/main/java/me/srrapero720/watermedia/api/image/ImageAPI.java @@ -1,11 +1,13 @@ package me.srrapero720.watermedia.api.image; -import org.watermedia.WaterMedia; -import org.watermedia.api.WaterMediaAPI; +import me.srrapero720.watermedia.WaterMedia; +import me.srrapero720.watermedia.api.WaterMediaAPI; import me.srrapero720.watermedia.core.config.WaterConfig; import me.srrapero720.watermedia.api.image.decoders.GifDecoder; -import org.watermedia.api.MathAPI; -import org.watermedia.tools.JarTool; +import me.srrapero720.watermedia.api.math.MathAPI; +import me.srrapero720.watermedia.tools.IOTool; +import me.srrapero720.watermedia.tools.JarTool; +import me.srrapero720.watermedia.loader.ILoader; import org.apache.logging.log4j.Marker; import org.apache.logging.log4j.MarkerManager; @@ -17,7 +19,7 @@ import java.util.Map; import java.util.concurrent.Executor; -import static org.watermedia.WaterMedia.LOGGER; +import static me.srrapero720.watermedia.WaterMedia.LOGGER; public class ImageAPI extends WaterMediaAPI { public static final Marker IT = MarkerManager.getMarker(ImageAPI.class.getSimpleName()); @@ -55,6 +57,7 @@ public static ImageRenderer loadingGif(String modId) { return renderer; } + renderer = renderer(IOTool.readGif(modConfig.toAbsolutePath())); LOADING_CACHE.put(modId, renderer); return renderer; } else { @@ -136,6 +139,26 @@ public static ImageRenderer renderer(BufferedImage image, boolean absolute) { return new ImageRenderer.Absolute(image); } + /** + * Creates an instance of an ImageRenderer only for gifs + * @param image image to use + * @return built instance + */ + public static ImageRenderer renderer(GifDecoder image) { + return renderer(image, false); + } + + /** + * Creates an instance of an ImageRenderer only for gifs + * @param image image to use + * @param absolute disabled flush and release methods, by default false + * @return built instance + */ + public static ImageRenderer renderer(GifDecoder image, boolean absolute) { + if (!absolute) return new ImageRenderer(image); + return new ImageRenderer.Absolute(image); + } + private Path loadingGifPath; @Override public Priority priority() { @@ -143,24 +166,28 @@ public Priority priority() { } @Override - public boolean prepare(WaterMedia.ILoader bootCore) throws Exception { + public boolean prepare(ILoader bootCore) throws Exception { this.loadingGifPath = bootCore.cwd().resolve("config/watermedia/assets/loading.gif"); if (!loadingGifPath.toFile().exists()) { LOGGER.info(IT, "Extracting default loading gif..."); - JarTool.extract("/pictures/loading.gif", loadingGifPath); + JarTool.copyAsset("/pictures/loading.gif", loadingGifPath); LOGGER.info(IT, "Extracted successfully"); } return true; } @Override - public void start(WaterMedia.ILoader bootCore) throws Exception { + public void start(ILoader bootCore) throws Exception { if (IMG_LOADING != null) { // TODO: release images and try again } LOGGER.info(IT, "Loading image resources in a {} instance", ImageRenderer.class.getSimpleName()); + IMG_LOADING = renderer(IOTool.readGif(loadingGifPath), true); + IMG_VLC_FAIL = renderer(JarTool.readGif("/pictures/videolan/failed.gif"), true); + IMG_VLC_FAIL_LAND = renderer(JarTool.readGif("/pictures/videolan/failed-land.gif"), true); + BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); image.setRGB(0, 0, MathAPI.argb(255, 0, 0, 0)); IMG_BLACK = renderer(image); diff --git a/src/main/java/me/srrapero720/watermedia/api/image/ImageCache.java b/src/main/java/me/srrapero720/watermedia/api/image/ImageCache.java index 73dd8425..31e56abd 100644 --- a/src/main/java/me/srrapero720/watermedia/api/image/ImageCache.java +++ b/src/main/java/me/srrapero720/watermedia/api/image/ImageCache.java @@ -8,13 +8,36 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; -import static org.watermedia.WaterMedia.LOGGER; +import static me.srrapero720.watermedia.WaterMedia.LOGGER; import static me.srrapero720.watermedia.api.image.ImageAPI.IT; public class ImageCache { static final Map CACHE = new HashMap<>(); static final ImageCache EMPTY_INSTANCE = new ImageCache(null); + /** + * Gets a cache for a URL + * if no exists then creates an unready one + * @param originalURL url of the picture + * @param renderThreadEx concurrent executor + * @deprecated use instead {@link ImageAPI#getCache(String, Executor)} + * @return cache instance + */ + @Deprecated + public static ImageCache get(String originalURL, Executor renderThreadEx) { + return ImageAPI.getCache(originalURL, renderThreadEx); + } + + /** + * Reloads all ImageCache instanced + * This might cause lag + * @deprecated use instead {@link ImageAPI#reloadCache()} + */ + @Deprecated + public static void reloadAll() { + CACHE.values().forEach(ImageCache::reload); + } + // INFO; public final String url; private final ImageFetch fetch; @@ -30,14 +53,16 @@ public class ImageCache { private final List> releaseListeners = new ArrayList<>(); - ImageCache(String url, Executor runnable) { + @Deprecated + public ImageCache(String url, Executor runnable) { this.url = url; this.renderThreadEx = runnable; this.fetch = new ImageFetch(url); CACHE.put(url, this); } - ImageCache(ImageRenderer renderer) { + @Deprecated + public ImageCache(ImageRenderer renderer) { this.url = ""; this.fetch = null; this.renderThreadEx = null; diff --git a/src/main/java/me/srrapero720/watermedia/api/image/ImageFetch.java b/src/main/java/me/srrapero720/watermedia/api/image/ImageFetch.java index 0ffa363f..a13e8c66 100644 --- a/src/main/java/me/srrapero720/watermedia/api/image/ImageFetch.java +++ b/src/main/java/me/srrapero720/watermedia/api/image/ImageFetch.java @@ -1,8 +1,10 @@ package me.srrapero720.watermedia.api.image; -import me.srrapero720.watermedia.core.cache.CacheCore; -import org.watermedia.tools.DataTool; -import org.watermedia.tools.ThreadTool; +import me.srrapero720.watermedia.api.cache.CacheAPI; +import me.srrapero720.watermedia.api.image.decoders.GifDecoder; +import me.srrapero720.watermedia.api.network.DynamicURL; +import me.srrapero720.watermedia.tools.DataTool; +import me.srrapero720.watermedia.tools.ThreadTool; import org.apache.logging.log4j.Marker; import org.apache.logging.log4j.MarkerManager; @@ -14,7 +16,6 @@ import java.io.*; import java.net.ConnectException; import java.net.HttpURLConnection; -import java.net.URL; import java.net.URLConnection; import java.text.DateFormat; import java.text.ParseException; @@ -24,8 +25,8 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import static org.watermedia.WaterMedia.LOGGER; -import static org.watermedia.tools.NetTool.USER_AGENT; +import static me.srrapero720.watermedia.WaterMedia.LOGGER; +import static me.srrapero720.watermedia.api.network.NetworkAPI.USER_AGENT; /** * Tool to fetch new images from internet @@ -69,25 +70,23 @@ public class ImageFetch { public void start() { EX.execute(this::run); } private void run() { try { -// DynamicURL result = new DynamicURL(url); -// if (result.isVideo()) throw new NoPictureException(); - - Object result = null; + DynamicURL result = new DynamicURL(url); + if (result.isVideo()) throw new NoPictureException(); byte[] data = load(result); String type = readType(data); try (ByteArrayInputStream in = new ByteArrayInputStream(data)) { if (type != null && type.equalsIgnoreCase("gif")) { -// GifDecoder gif = new GifDecoder(); -// int status = gif.read(in); -// -// if (status == GifDecoder.STATUS_OK) { -// if (successful != null) successful.run(ImageAPI.renderer(gif)); -// } else { -// LOGGER.error(IT, "Failed to read gif: {}", status); -// throw new GifDecodingException(); -// } + GifDecoder gif = new GifDecoder(); + int status = gif.read(in); + + if (status == GifDecoder.STATUS_OK) { + if (successful != null) successful.run(ImageAPI.renderer(gif)); + } else { + LOGGER.error(IT, "Failed to read gif: {}", status); + throw new GifDecodingException(); + } } else { try { BufferedImage image = ImageIO.read(in); @@ -105,15 +104,14 @@ private void run() { LOGGER.error(IT, "An exception occurred while loading image", e); } if (failed != null) failed.run(e); - // TODO: still use cache even if connection failed -// CacheAPI.delete(url); + CacheAPI.deleteEntry(url); } } - private static byte[] load(Object url) throws IOException, NoPictureException { - CacheCore.Entry entry = CacheCore.get(""); + private static byte[] load(DynamicURL url) throws IOException, NoPictureException { + CacheAPI.Entry entry = CacheAPI.getEntry(url.getSource()); long requestTime = System.currentTimeMillis(); - URLConnection request = ((URL) url).openConnection(); + URLConnection request = url.asURL().openConnection(); request.setDefaultUseCaches(false); request.setRequestProperty("Accept", "image/*"); int code = -1; @@ -121,9 +119,9 @@ private static byte[] load(Object url) throws IOException, NoPictureException { request.addRequestProperty("User-Agent", USER_AGENT); if (request instanceof HttpURLConnection) { final HttpURLConnection conn = (HttpURLConnection) request; - if (entry != null && entry.file.exists()) { - if (!entry.tag.equals("null")) conn.setRequestProperty("If-None-Match", entry.tag); - else if (entry.requestTime != -1) conn.setRequestProperty("If-Modified-Since", FORMAT.format(new Date(entry.requestTime))); + if (entry != null && entry.getFile().exists()) { + if (entry.getTag() != null) conn.setRequestProperty("If-None-Match", entry.getTag()); + else if (entry.getTime() != -1) conn.setRequestProperty("If-Modified-Since", FORMAT.format(new Date(entry.getTime()))); } code = conn.getResponseCode(); } @@ -140,16 +138,9 @@ private static byte[] load(Object url) throws IOException, NoPictureException { long lastTimestamp, expTimestamp = -1; String maxAge = request.getHeaderField("max-age"); - // CONTENT-TYPE - String mimeType = request.getHeaderField("Content-Type"); - if (mimeType != null && !mimeType.isEmpty()) { - int i = mimeType.indexOf(";"); - if (i != -1) mimeType = mimeType.substring(0, i); - } - // EXPIRATION GETTER FIRST if (maxAge != null && !maxAge.isEmpty()) { - long parsed = DataTool.orElse(maxAge, -1); + long parsed = DataTool.parseLongOr(maxAge, -1); if (parsed != -1) expTimestamp = requestTime + Long.parseLong(maxAge) * 100; } @@ -174,21 +165,23 @@ private static byte[] load(Object url) throws IOException, NoPictureException { } if (entry != null) { - String freshTag = tag != null && !tag.isEmpty() ? tag : entry.tag; + String freshTag = entry.getTag(); + if (tag != null && !tag.isEmpty()) freshTag = tag; if (code == HttpURLConnection.HTTP_NOT_MODIFIED) { - try (InputStream in2 = entry.getInputStream()) { - return DataTool.readAllBytes(in2); + File file = entry.getFile(); + + if (file.exists()) try (FileInputStream fileStream = new FileInputStream(file)) { + return DataTool.readAllBytes(fileStream); } finally { - entry.refresh("URL", freshTag, mimeType, lastTimestamp, expTimestamp); + CacheAPI.updateEntry(new CacheAPI.Entry(url.getSource(), freshTag, lastTimestamp, expTimestamp)); } } - } else { - entry = CacheCore.create("URL SOURCE", tag, mimeType, lastTimestamp, expTimestamp); } + byte[] data = DataTool.readAllBytes(in); - entry.storeFile(data); + CacheAPI.saveFile(url.getSource(), tag, lastTimestamp, expTimestamp, data); return data; } finally { if (request instanceof HttpURLConnection) ((HttpURLConnection) request).disconnect(); diff --git a/src/main/java/me/srrapero720/watermedia/api/image/ImageRenderer.java b/src/main/java/me/srrapero720/watermedia/api/image/ImageRenderer.java index 694075ad..9c0d7e5a 100644 --- a/src/main/java/me/srrapero720/watermedia/api/image/ImageRenderer.java +++ b/src/main/java/me/srrapero720/watermedia/api/image/ImageRenderer.java @@ -1,8 +1,9 @@ package me.srrapero720.watermedia.api.image; -import org.watermedia.api.MathAPI; +import me.srrapero720.watermedia.api.image.decoders.GifDecoder; +import me.srrapero720.watermedia.api.math.MathAPI; import me.srrapero720.watermedia.api.rendering.RenderAPI; -import org.watermedia.tools.DataTool; +import me.srrapero720.watermedia.tools.DataTool; import java.awt.image.BufferedImage; import java.nio.ByteBuffer; @@ -14,7 +15,7 @@ public class ImageRenderer { public final int[] textures; public final long[] delay; public final long duration; - private final ByteBuffer[] images; + private ByteBuffer[] images; public boolean flushed; public int remaining; @@ -35,6 +36,23 @@ public class ImageRenderer { this.remaining = this.images.length; } + /** + * creates a new instance of an ImageRenderer + * @param decoder picture to use + * method is going to begin package-protected + */ + ImageRenderer(GifDecoder decoder) { + if (decoder == null) throw new NullPointerException(); + this.images = RenderAPI.getRawImageBuffer(decoder.getFrames()); + this.width = decoder.getWidth(); + this.height = decoder.getHeight(); + this.textures = new int[decoder.getFrameCount()]; + this.delay = decoder.getDelayFrames(); + this.duration = decoder.getDuration(); + this.remaining = this.images.length; + Arrays.fill(textures, -1); + } + /** * gets texture id based on time in millis * use API to calculate time @@ -122,10 +140,10 @@ public boolean isFlushed() { */ protected void flush() { if (flushed) throw new IllegalStateException("Buffers are already flushed"); - for (int i = 0; i < this.images.length; i++) { - RenderAPI.freeByteBuffer(this.images[i]); - this.images[i] = null; + for (ByteBuffer buffer: this.images) { + RenderAPI.freeByteBuffer(buffer); } + this.images = new ByteBuffer[this.images.length]; this.flushed = true; } @@ -152,7 +170,7 @@ public void release() { Arrays.fill(this.textures, -1); } else { this.flush(); - RenderAPI.deleteTexture(DataTool.filter(this.textures, -1)); + RenderAPI.deleteTexture(DataTool.filterValue(this.textures, -1)); Arrays.fill(this.textures, -1); } } @@ -163,6 +181,10 @@ static class Absolute extends ImageRenderer { super(image); } + Absolute(GifDecoder decoder) { + super(decoder); + } + @Override public void release() {} } } diff --git a/src/main/java/me/srrapero720/watermedia/api/image/decoders/GifDecoder.java b/src/main/java/me/srrapero720/watermedia/api/image/decoders/GifDecoder.java new file mode 100644 index 00000000..4d9e9aa1 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/image/decoders/GifDecoder.java @@ -0,0 +1,740 @@ +package me.srrapero720.watermedia.api.image.decoders; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; + +/** Class GifDecoder - Decodes a GIF file into one or more frames. + * Example: + * + *
+ * {
+ *     @code
+ *     GifDecoder d = new GifDecoder();
+ *     d.read("sample.gif");
+ *     int n = d.getFrameCount();
+ *     for (int i = 0; i < n; i++) {
+ *         BufferedImage frame = d.getFrame(i); // frame i
+ *         int t = d.getDelay(i); // display duration of frame in milliseconds
+ *         // do something with frame
+ *     }
+ * }
+ * 
+ * + * No copyright asserted on the source code of this class. May be used for + * any purpose, however, refer to the Unisys LZW patent for any additional + * restrictions. Please forward any corrections to questions at fmsware.com. + * + * @author Kevin Weiner, FM Software; LZW decoder adapted from John Cristy's ImageMagick. + * @version 1.03 November 2003 */ +public class GifDecoder { + + /** File read status: No errors. */ + public static final int STATUS_OK = 0; + + /** File read status: Error decoding file (may be partially decoded) */ + public static final int STATUS_FORMAT_ERROR = 1; + + /** File read status: Unable to open source. */ + public static final int STATUS_OPEN_ERROR = 2; + + protected BufferedInputStream in; + protected int status; + + protected int width; // full image width + protected int height; // full image height + protected boolean gctFlag; // global color table used + protected int gctSize; // size of global color table + protected int loopCount = 1; // iterations; 0 = repeat forever + + protected int[] gct; // global color table + protected int[] lct; // local color table + protected int[] act; // active color table + + protected int bgIndex; // background color index + protected int bgColor; // background color + protected int lastBgColor; // previous bg color + protected int pixelAspect; // pixel aspect ratio + + protected boolean lctFlag; // local color table flag + protected boolean interlace; // interlace flag + protected int lctSize; // local color table size + + protected int ix, iy, iw, ih; // current image rectangle + protected Rectangle lastRect; // last image rect + protected BufferedImage image; // current frame + protected BufferedImage lastImage; // previous frame + + protected byte[] block = new byte[256]; // current data block + protected int blockSize = 0; // block size + + // last graphic control extension info + protected int dispose = 0; + // 0=no action; 1=leave in place; 2=restore to bg; 3=restore to prev + protected int lastDispose = 0; + protected boolean transparency = false; // use transparent color + protected int delay = 0; // delay in milliseconds + protected int transIndex; // transparent color index + + protected static final int MaxStackSize = 4096; + // max decoder pixel stack size + + // LZW decoder working arrays + protected short[] prefix; + protected byte[] suffix; + protected byte[] pixelStack; + protected byte[] pixels; + + protected ArrayList frames; // frames read from current file + protected int frameCount; + protected long duration = 0; + + static class GifFrame { + public GifFrame(BufferedImage im, int del) { + image = im; + delay = del; + } + + public BufferedImage image; + public int delay; + } + + /** Gets display duration for specified frame. + * + * @param n int index of frame + * @return delay in milliseconds */ + public int getDelay(int n) { + // + delay = -1; + if ((n >= 0) && (n < frameCount)) { + delay = frames.get(n).delay; + } + return delay; + } + + /** Gets the number of frames read from file. + * + * @return frame count */ + public int getFrameCount() { + return frameCount; + } + + /** Gets the first (or only) image read. + * + * @return BufferedImage containing first frame, or null if none. */ + public BufferedImage getImage() { + return getFrame(0); + } + + /** Gets the "Netscape" iteration count, if any. + * A count of 0 means repeat indefinitiely. + * + * @return iteration count if one was specified, else 1. */ + public int getLoopCount() { + return loopCount; + } + + /** Creates new frame image from current data (and previous + * frames as specified by their disposition codes). */ + protected void setPixels() { + // expose destination image's pixels as int array + int[] dest = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + + // fill in starting image contents based on last image's dispose code + if (lastDispose > 0) { + if (lastDispose == 3) { + // use image before last + int n = frameCount - 2; + if (n > 0) { + lastImage = getFrame(n - 1); + } else { + lastImage = null; + } + } + + if (lastImage != null) { + int[] prev = ((DataBufferInt) lastImage.getRaster().getDataBuffer()).getData(); + System.arraycopy(prev, 0, dest, 0, width * height); + // copy pixels + + if (lastDispose == 2) { + // fill last image rect area with background color + Graphics2D g = image.createGraphics(); + Color c = null; + if (transparency) { + c = new Color(0, 0, 0, 0); // assume background is transparent + } else { + c = new Color(lastBgColor); // use given background color + } + g.setColor(c); + g.setComposite(AlphaComposite.Src); // replace area + g.fill(lastRect); + g.dispose(); + } + } + } + + // copy each source line to the appropriate place in the destination + int pass = 1; + int inc = 8; + int iline = 0; + for (int i = 0; i < ih; i++) { + int line = i; + if (interlace) { + if (iline >= ih) { + pass++; + switch (pass) { + case 2: + iline = 4; + break; + case 3: + iline = 2; + inc = 4; + break; + case 4: + iline = 1; + inc = 2; + } + } + line = iline; + iline += inc; + } + line += iy; + if (line < height) { + int k = line * width; + int dx = k + ix; // start of line in dest + int dlim = dx + iw; // end of dest line + if ((k + width) < dlim) { + dlim = k + width; // past dest edge + } + int sx = i * iw; // start of line in source + while (dx < dlim) { + // map color and insert in destination + int index = (pixels[sx++]) & 0xff; + int c = act[index]; + if (c != 0) { + dest[dx] = c; + } + dx++; + } + } + } + } + + /** Gets the image contents of frame n. + * + * @return BufferedImage representation of frame, or null if n is invalid. */ + public BufferedImage getFrame(int n) { + BufferedImage im = null; + if ((n >= 0) && (n < frameCount)) { + im = frames.get(n).image; + } + return im; + } + + public BufferedImage[] getFrames() { + BufferedImage[] result = new BufferedImage[frames.size()]; + for (int i = 0; i < result.length; i++) { + result[i] = frames.get(i).image; + } + return result; + } + + public long[] getDelayFrames() { + long[] result = new long[frames.size()]; + for (int i = 0; i < result.length; i++) { + result[i] = frames.get(i).delay; + } + return result; + } + + /** Gets image size. + * + * @return GIF image dimensions */ + public Dimension getFrameSize() { + return new Dimension(width, height); + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public long getDuration() { + return duration; + } + + /** Reads GIF image from stream + * + * @param is + * BufferedInputStream containing GIF file. + * @return read status code (0 = no errors) */ + public int read(BufferedInputStream is) { + init(); + if (is != null) { + in = is; + readHeader(); + if (!err()) { + readContents(); + if (frameCount < 0) { + status = STATUS_FORMAT_ERROR; + } + } + } else { + status = STATUS_OPEN_ERROR; + } + try { + is.close(); + } catch (IOException e) {} + return status; + } + + /** Reads GIF image from stream + * + * @param is + * InputStream containing GIF file. + * @return read status code (0 = no errors) */ + public int read(InputStream is) { + init(); + if (is != null) { + if (!(is instanceof BufferedInputStream)) + is = new BufferedInputStream(is); + in = (BufferedInputStream) is; + readHeader(); + if (!err()) { + readContents(); + if (frameCount < 0) { + status = STATUS_FORMAT_ERROR; + } + } + } else { + status = STATUS_OPEN_ERROR; + } + try { + is.close(); + } catch (IOException e) {} + return status; + } + + /** Reads GIF file from specified file/URL source + * (URL assumed if name contains ":/" or "file:") + * + * @param name + * String containing source + * @return read status code (0 = no errors) */ + public int read(String name) { + status = STATUS_OK; + try { + name = name.trim().toLowerCase(); + if ((name.contains("file:")) || (name.indexOf(":/") > 0)) { + URL url = new URL(name); + in = new BufferedInputStream(url.openStream()); + } else { + in = new BufferedInputStream(new FileInputStream(name)); + } + status = read(in); + } catch (IOException e) { + status = STATUS_OPEN_ERROR; + } + + return status; + } + + /** Decodes LZW image data into pixel array. + * Adapted from John Cristy's ImageMagick. */ + protected void decodeImageData() { + int NullCode = -1; + int npix = iw * ih; + int available, clear, code_mask, code_size, end_of_information, in_code, old_code, bits, code, count, i, datum, data_size, first, top, bi, pi; + + if ((pixels == null) || (pixels.length < npix)) { + pixels = new byte[npix]; // allocate new pixel array + } + if (prefix == null) + prefix = new short[MaxStackSize]; + if (suffix == null) + suffix = new byte[MaxStackSize]; + if (pixelStack == null) + pixelStack = new byte[MaxStackSize + 1]; + + // Initialize GIF data stream decoder. + + data_size = read(); + clear = 1 << data_size; + end_of_information = clear + 1; + available = clear + 2; + old_code = NullCode; + code_size = data_size + 1; + code_mask = (1 << code_size) - 1; + for (code = 0; code < clear; code++) { + prefix[code] = 0; + suffix[code] = (byte) code; + } + + // Decode GIF pixel stream. + + datum = bits = count = first = top = pi = bi = 0; + + for (i = 0; i < npix;) { + if (top == 0) { + if (bits < code_size) { + // Load bytes until there are enough bits for a code. + if (count == 0) { + // Read a new data block. + count = readBlock(); + if (count <= 0) + break; + bi = 0; + } + datum += ((block[bi]) & 0xff) << bits; + bits += 8; + bi++; + count--; + continue; + } + + // Get the next code. + + code = datum & code_mask; + datum >>= code_size; + bits -= code_size; + + // Interpret the code + + if ((code > available) || (code == end_of_information)) + break; + if (code == clear) { + // Reset decoder. + code_size = data_size + 1; + code_mask = (1 << code_size) - 1; + available = clear + 2; + old_code = NullCode; + continue; + } + if (old_code == NullCode) { + pixelStack[top++] = suffix[code]; + old_code = code; + first = code; + continue; + } + in_code = code; + if (code == available) { + pixelStack[top++] = (byte) first; + code = old_code; + } + while (code > clear) { + pixelStack[top++] = suffix[code]; + code = prefix[code]; + } + first = (suffix[code]) & 0xff; + + // Add a new string to the string table, + + if (available >= MaxStackSize) { + pixelStack[top++] = (byte) first; + continue; + } + pixelStack[top++] = (byte) first; + prefix[available] = (short) old_code; + suffix[available] = (byte) first; + available++; + if (((available & code_mask) == 0) && (available < MaxStackSize)) { + code_size++; + code_mask += available; + } + old_code = in_code; + } + + // Pop a pixel off the pixel stack. + + top--; + pixels[pi++] = pixelStack[top]; + i++; + } + + for (i = pi; i < npix; i++) { + pixels[i] = 0; // clear missing pixels + } + + } + + /** Returns true if an error was encountered during reading/decoding */ + protected boolean err() { + return status != STATUS_OK; + } + + /** Initializes or re-initializes reader */ + protected void init() { + status = STATUS_OK; + frameCount = 0; + frames = new ArrayList<>(); + gct = null; + lct = null; + } + + /** Reads a single byte from the input stream. */ + protected int read() { + int curByte = 0; + try { + curByte = in.read(); + } catch (IOException e) { + status = STATUS_FORMAT_ERROR; + } + return curByte; + } + + /** Reads next variable length block from input. + * + * @return number of bytes stored in "buffer" */ + protected int readBlock() { + blockSize = read(); + int n = 0; + if (blockSize > 0) { + try { + int count = 0; + while (n < blockSize) { + count = in.read(block, n, blockSize - n); + if (count == -1) + break; + n += count; + } + } catch (IOException ignored) {} + + if (n < blockSize) { + status = STATUS_FORMAT_ERROR; + } + } + return n; + } + + /** Reads color table as 256 RGB integer values + * + * @param ncolors + * int number of colors to read + * @return int array containing 256 colors (packed ARGB with full alpha) */ + protected int[] readColorTable(int ncolors) { + int nbytes = 3 * ncolors; + int[] tab = null; + byte[] c = new byte[nbytes]; + int n = 0; + try { + n = in.read(c); + } catch (IOException e) {} + if (n < nbytes) { + status = STATUS_FORMAT_ERROR; + } else { + tab = new int[256]; // max size to avoid bounds checks + int i = 0; + int j = 0; + while (i < ncolors) { + int r = (c[j++]) & 0xff; + int g = (c[j++]) & 0xff; + int b = (c[j++]) & 0xff; + tab[i++] = 0xff000000 | (r << 16) | (g << 8) | b; + } + } + return tab; + } + + /** Main file parser. Reads GIF content blocks. */ + protected void readContents() { + // read GIF file content blocks + boolean done = false; + while (!(done || err())) { + int code = read(); + switch (code) { + + case 0x2C: // image separator + readImage(); + break; + + case 0x21: // extension + code = read(); + switch (code) { + case 0xf9: // graphics control extension + readGraphicControlExt(); + break; + + case 0xff: // application extension + readBlock(); + String app = ""; + for (int i = 0; i < 11; i++) { + app += (char) block[i]; + } + if (app.equals("NETSCAPE2.0")) { + readNetscapeExt(); + } else + skip(); // don't care + break; + + default: // uninteresting extension + skip(); + } + break; + + case 0x3b: // terminator + done = true; + break; + + case 0x00: // bad byte, but keep going and see what happens + break; + + default: + status = STATUS_FORMAT_ERROR; + } + } + } + + /** Reads Graphics Control Extension values */ + protected void readGraphicControlExt() { + read(); // block size + int packed = read(); // packed fields + dispose = (packed & 0x1c) >> 2; // disposal method + if (dispose == 0) { + dispose = 1; // elect to keep old image if discretionary + } + transparency = (packed & 1) != 0; + delay = readShort() * 10; // delay in milliseconds + duration += delay; + transIndex = read(); // transparent color index + read(); // block terminator + } + + /** Reads GIF file header information. */ + protected void readHeader() { + String id = ""; + for (int i = 0; i < 6; i++) { + id += (char) read(); + } + if (!id.startsWith("GIF")) { + status = STATUS_FORMAT_ERROR; + return; + } + + readLSD(); + if (gctFlag && !err()) { + gct = readColorTable(gctSize); + bgColor = gct[bgIndex]; + } + } + + /** Reads next frame image */ + protected void readImage() { + ix = readShort(); // (sub)image position & size + iy = readShort(); + iw = readShort(); + ih = readShort(); + + int packed = read(); + lctFlag = (packed & 0x80) != 0; // 1 - local color table flag + interlace = (packed & 0x40) != 0; // 2 - interlace flag + // 3 - sort flag + // 4-5 - reserved + lctSize = 2 << (packed & 7); // 6-8 - local color table size + + if (lctFlag) { + lct = readColorTable(lctSize); // read table + act = lct; // make local table active + } else { + act = gct; // make global table active + if (bgIndex == transIndex) + bgColor = 0; + } + int save = 0; + if (transparency) { + save = act[transIndex]; + act[transIndex] = 0; // set transparent color if specified + } + + if (act == null) { + status = STATUS_FORMAT_ERROR; // no color table defined + } + + if (err()) + return; + + decodeImageData(); // decode pixel data + skip(); + + if (err()) + return; + + frameCount++; + + // create new image to receive frame data + image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE); + + setPixels(); // transfer pixel data to image + + frames.add(new GifFrame(image, delay)); // add image to frame list + + if (transparency) { + act[transIndex] = save; + } + resetFrame(); + + } + + /** Reads Logical Screen Descriptor */ + protected void readLSD() { + + // logical screen size + width = readShort(); + height = readShort(); + + // packed fields + int packed = read(); + gctFlag = (packed & 0x80) != 0; // 1 : global color table flag + // 2-4 : color resolution + // 5 : gct sort flag + gctSize = 2 << (packed & 7); // 6-8 : gct size + + bgIndex = read(); // background color index + pixelAspect = read(); // pixel aspect ratio + } + + /** Reads Netscape extenstion to obtain iteration count */ + protected void readNetscapeExt() { + do { + readBlock(); + if (block[0] == 1) { + // loop count sub-block + int b1 = (block[1]) & 0xff; + int b2 = (block[2]) & 0xff; + loopCount = (b2 << 8) | b1; + } + } while ((blockSize > 0) && !err()); + } + + /** Reads next 16-bit value, LSB first */ + protected int readShort() { + // read 16-bit value, LSB first + return read() | (read() << 8); + } + + /** Resets frame state for reading next image. */ + protected void resetFrame() { + lastDispose = dispose; + lastRect = new Rectangle(ix, iy, iw, ih); + lastImage = image; + lastBgColor = bgColor; + lct = null; + } + + /** Skips variable length blocks up to and including + * next zero length block. */ + protected void skip() { + do { + readBlock(); + } while ((blockSize > 0) && !err()); + } +} \ No newline at end of file diff --git a/src/main/java/org/watermedia/api/MathAPI.java b/src/main/java/me/srrapero720/watermedia/api/math/MathAPI.java similarity index 96% rename from src/main/java/org/watermedia/api/MathAPI.java rename to src/main/java/me/srrapero720/watermedia/api/math/MathAPI.java index 7f25a8a4..afd89f43 100644 --- a/src/main/java/org/watermedia/api/MathAPI.java +++ b/src/main/java/me/srrapero720/watermedia/api/math/MathAPI.java @@ -1,6 +1,7 @@ -package org.watermedia.api; +package me.srrapero720.watermedia.api.math; -import org.watermedia.WaterMedia; +import me.srrapero720.watermedia.api.WaterMediaAPI; +import me.srrapero720.watermedia.loader.ILoader; public class MathAPI extends WaterMediaAPI { @@ -9,7 +10,7 @@ public class MathAPI extends WaterMediaAPI { static { for (int i = 0; i < SIN_SIZE; i++) { - SIN[i] = (float) Math.sin(i * Math.PI * 2.0 / SIN_SIZE); + SIN[i] = (float) Math.sin(i * Math.PI * 2.0 / SIN_SIZE); } } @@ -19,9 +20,21 @@ public class MathAPI extends WaterMediaAPI { * * @param ticks Minecraft Tick count * @return ticks converted to MS + * @deprecated

Tick type was changed from int to long following the time counting standard.

+ * Use instead {@link MathAPI#tickToMs(long)} */ + @Deprecated public static long tickToMs(int ticks) { return ticks * 50L; } + /** + * 1 seconds in Minecraft equals 20 ticks + * 20x50 equals 1000ms (1 sec) + * + * @param ticks Minecraft Tick count + * @return ticks converted to MS + */ + public static long tickToMs(long ticks) { return ticks * 50L; } + /** * 1 seconds in Minecraft equals 20 ticks * 20x50 equals 1000ms (1 sec) @@ -223,6 +236,19 @@ public static byte min(byte a, byte b) { */ public static int argb(int a, int r, int g, int b) { return (a << 24) | (r << 16) | (g << 8) | b; } + /** + * Creates a hexadecimal color based on gave params + * All values need to be in a range of 0 ~ 255 + * @param a Alpha + * @param r Red + * @param g Green + * @param b Blue + * @return HEX color + * @deprecated renamed to {@link MathAPI#argb(int, int, int, int)} + */ + @Deprecated(forRemoval = true) + public static int getColorARGB(int a, int r, int g, int b) { return (a << 24) | (r << 16) | (g << 8) | b; } + /** * Converts arguments into an ease-in value usable on animations. * @@ -750,34 +776,18 @@ public static float cos(float pValue) { return SIN[(int)(pValue * 10430.378F + 16384.0F) & '\uffff']; } - public static int sumArray(int[] arr) { - int r = 0; - for (int i: arr) { - r += i; - } - return r; - } - - public static long sumArray(long[] arr) { - long r = 0; - for (long i: arr) { - r += i; - } - return r; - } - @Override public Priority priority() { return Priority.LOWEST; } @Override - public boolean prepare(WaterMedia.ILoader bootCore) throws Exception { + public boolean prepare(ILoader bootCore) throws Exception { return true; } @Override - public void start(WaterMedia.ILoader bootCore) throws Exception { + public void start(ILoader bootCore) throws Exception { } diff --git a/src/main/java/me/srrapero720/watermedia/api/network/DynamicRequest.java b/src/main/java/me/srrapero720/watermedia/api/network/DynamicRequest.java new file mode 100644 index 00000000..548044cd --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/network/DynamicRequest.java @@ -0,0 +1,43 @@ +package me.srrapero720.watermedia.api.network; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.net.ConnectException; +import java.net.HttpURLConnection; + +public class DynamicRequest implements Closeable { + private final HttpURLConnection connection; + private final String requestMethod; + public DynamicRequest(DynamicURL dynamicURL) throws IOException { + this(dynamicURL, "GET"); + } + public DynamicRequest(DynamicURL dynamicURL, String requestMethod) throws IOException { + this.connection = (HttpURLConnection) dynamicURL.asURL().openConnection(); + this.requestMethod = requestMethod; + } + + public DynamicRequest setRequestProperty(String key, String value) { + this.connection.setRequestProperty(key, value); + return this; + } + + public InputStream getInputStream() throws IOException { + if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { + String msg = String.format("Server response with status code (%s): %s", this.connection.getResponseCode(), this.connection.getResponseMessage()); + this.connection.setRequestMethod(requestMethod); + this.close(); + throw new ConnectException(msg); + } + return connection.getInputStream(); + } + + public int getResponseCode() throws IOException { + return this.connection.getResponseCode(); + } + + @Override + public void close() throws IOException { + this.connection.disconnect(); + } +} diff --git a/src/main/java/me/srrapero720/watermedia/api/network/DynamicURL.java b/src/main/java/me/srrapero720/watermedia/api/network/DynamicURL.java new file mode 100644 index 00000000..91b25693 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/network/DynamicURL.java @@ -0,0 +1,83 @@ +package me.srrapero720.watermedia.api.network; + +import java.io.File; +import java.net.URL; +import java.nio.file.Path; + +public final class DynamicURL { + private final String source; + private final URL url; + private final File file; + boolean video = false; + boolean stream = false; + boolean cached = false; + boolean exists = false; + + public DynamicURL(String source, URL url, File file) { + this.source = source; + this.url = url; + this.file = file; + } + + public DynamicURL(String urlOrPath) { + if (urlOrPath.startsWith("local://")) { + urlOrPath = urlOrPath.replace("local://", ""); + if (urlOrPath.startsWith(File.pathSeparator)) urlOrPath = urlOrPath.substring(1); + urlOrPath = new File(urlOrPath).getAbsolutePath(); + this.stream = false; + this.cached = true; + } + this.source = urlOrPath; + this.url = NetworkAPI.parseUrl(urlOrPath); + this.file = (this.url == null) ? new File(urlOrPath) : null; + } + + public DynamicURL(final String urlOrPath, final boolean isVideo, final boolean isStream) { + this(urlOrPath); + this.video = isVideo; + this.stream = isStream; + } + + public DynamicURL(final File file) { + this.source = file.getAbsolutePath(); + this.file = file; + this.url = NetworkAPI.parseUrl(file.toURI()); + } + + public DynamicURL(final Path path) { + this(path.toFile()); + } + + public String getSource() { + return source; + } + + public URL asURL() { + return this.url; + } + + public File asFile() { + if (!isLocal()) throw new UnsupportedOperationException("DynamicURL doesn't point to any file"); + return file; + } + + public boolean isLocal() { + return this.file != null; + } + + public boolean isVideo() { + return this.video; + } + + public boolean isStream() { + return this.stream; + } + + public boolean isCached() { + return this.cached; + } + + public boolean exists() { + return cached ? exists : (exists = this.file.exists()); + } +} diff --git a/src/main/java/me/srrapero720/watermedia/api/network/NetworkAPI.java b/src/main/java/me/srrapero720/watermedia/api/network/NetworkAPI.java new file mode 100644 index 00000000..28d505a6 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/network/NetworkAPI.java @@ -0,0 +1,112 @@ +package me.srrapero720.watermedia.api.network; + +import com.google.gson.Gson; +import me.srrapero720.watermedia.api.WaterMediaAPI; +import me.srrapero720.watermedia.api.network.patch.URLPatch; +import me.srrapero720.watermedia.loader.ILoader; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.MarkerManager; + +import java.net.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.ServiceLoader; + +import static me.srrapero720.watermedia.WaterMedia.LOGGER; + +public class NetworkAPI extends WaterMediaAPI { + public static final Marker IT = MarkerManager.getMarker(NetworkAPI.class.getSimpleName()); + public static final String USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 Edg/112.0.1722.68"; + public static final Gson GSON = new Gson(); + private static final ServiceLoader URL_PATCHES = ServiceLoader.load(URLPatch.class); + static { + CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ALL)); + } + + public static DynamicURL patchURL(DynamicURL url) { + if (url.isLocal()) return url; + try { + for (URLPatch patcher: URL_PATCHES) { + if (!patcher.isValid(url)) continue; + return patcher.patch(url, URLPatch.Quality.HIGHEST); + } + } catch (Exception e) { + LOGGER.error(IT, "Failed to patch URL '{}'", url.getSource(), e); + } + return url; + } + + public static String[] getPatchNames() { + ArrayList result = new ArrayList<>(); + for (URLPatch patcher: URL_PATCHES) { + result.add(patcher.name()); + } + return result.toArray(new String[0]); + } + + /** + * Parses a query string from a {@link URL#getQuery()} in a Map + * @param query query string + * @return map with all values as a String + */ + public static Map parseQuery(String query) { + Map queryParams = new HashMap<>(); + String[] params = query.split("&"); + for (String param : params) { + String[] keyValue = param.split("="); + if (keyValue.length == 2) { + String key = keyValue[0]; + String value = keyValue[1]; + queryParams.put(key, value); + } + } + return queryParams; + } + + /** + * Gets a URL instance based on a given string, null if is malformed or invalid + * @param url string url + * @return the instanced URL, null if was not a valid URL + */ + public static URL parseUrl(String url) { + try { + return new URL(url); + } catch (Exception e) { + return null; + } + } + + /** + * Gets a URL instance based on a given string, null if is malformed or invalid + * @param uri string url + * @return if was valid + */ + public static URL parseUrl(URI uri) { + try { + return uri.toURL(); + } catch (Exception e) { + return null; + } + } + + @Override + public Priority priority() { + return Priority.LOW; + } + + @Override + public boolean prepare(ILoader bootCore) throws Exception { + return false; + } + + @Override + public void start(ILoader bootCore) throws Exception { + + } + + @Override + public void release() { + + } +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/api/network/models/imgur/ImgurAlbumTagData.java b/src/main/java/me/srrapero720/watermedia/api/network/models/imgur/ImgurAlbumTagData.java new file mode 100644 index 00000000..b20e1fb5 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/network/models/imgur/ImgurAlbumTagData.java @@ -0,0 +1,31 @@ +package me.srrapero720.watermedia.api.network.models.imgur; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import me.srrapero720.watermedia.api.network.models.imgur.images.ImgurImage; + +import java.util.List; + +public class ImgurAlbumTagData { + @SerializedName("id") + @Expose + public String id; + + @SerializedName("cover") + @Expose + public String coverId; + + @SerializedName("nsfw") + @Expose + public boolean nsfw; + + @SerializedName("images_count") + @Expose + public int imageCount; + + @SerializedName("images") + @Expose + public List images; + + +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/api/network/models/imgur/ImgurData.java b/src/main/java/me/srrapero720/watermedia/api/network/models/imgur/ImgurData.java new file mode 100644 index 00000000..6eeec5f2 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/network/models/imgur/ImgurData.java @@ -0,0 +1,10 @@ +package me.srrapero720.watermedia.api.network.models.imgur; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class ImgurData { + @SerializedName("data") + @Expose + public T data; +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/api/network/models/imgur/images/ImgurImage.java b/src/main/java/me/srrapero720/watermedia/api/network/models/imgur/images/ImgurImage.java new file mode 100644 index 00000000..1a337e69 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/network/models/imgur/images/ImgurImage.java @@ -0,0 +1,34 @@ +package me.srrapero720.watermedia.api.network.models.imgur.images; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class ImgurImage { + @SerializedName("id") + @Expose + public String id; + + @SerializedName("type") + @Expose + public String type; + + @SerializedName("width") + @Expose + public int width; + + @SerializedName("height") + @Expose + public int height; + + @SerializedName("size") + @Expose + public int size; + + @SerializedName("link") + @Expose + public String link; + + @SerializedName("hls") + @Expose + public String hls; +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/api/network/models/kick/KickChannel.java b/src/main/java/me/srrapero720/watermedia/api/network/models/kick/KickChannel.java new file mode 100644 index 00000000..d6e41c76 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/network/models/kick/KickChannel.java @@ -0,0 +1,39 @@ +package me.srrapero720.watermedia.api.network.models.kick; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; + +public class KickChannel implements Serializable { + + @SerializedName("id") + @Expose + public int id; + + @SerializedName("user_id") + @Expose + public int userId; + + @SerializedName("slug") + @Expose + public String username; + + @SerializedName("playback_url") + @Expose + public String url; + + @SerializedName("livestream") + @Expose + public isLive livestream; + + public static class isLive implements Serializable { + @SerializedName("id") + @Expose + public int id; + + @SerializedName("is_live") + @Expose + public boolean isStreaming; + } +} diff --git a/src/main/java/me/srrapero720/watermedia/api/network/models/kick/KickVideo.java b/src/main/java/me/srrapero720/watermedia/api/network/models/kick/KickVideo.java new file mode 100644 index 00000000..3a0f3c37 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/network/models/kick/KickVideo.java @@ -0,0 +1,21 @@ +package me.srrapero720.watermedia.api.network.models.kick; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; + + +public class KickVideo implements Serializable { + @SerializedName("id") + @Expose + public int id; + + @SerializedName("live_stream_id") + @Expose + public int streamId; + + @SerializedName("source") + @Expose + public String url; +} diff --git a/src/main/java/me/srrapero720/watermedia/api/network/models/onedrive/OneDriveItem.java b/src/main/java/me/srrapero720/watermedia/api/network/models/onedrive/OneDriveItem.java new file mode 100644 index 00000000..02ab7d0b --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/network/models/onedrive/OneDriveItem.java @@ -0,0 +1,36 @@ +package me.srrapero720.watermedia.api.network.models.onedrive; + +import com.google.gson.annotations.SerializedName; + +public class OneDriveItem { + private String id; + private String name; + private long size; + @SerializedName("@content.downloadUrl") + private String url; + + public OneDriveItem() {} + + public OneDriveItem(String id, String name, long size, String url) { + this.id = id; + this.name = name; + this.size = size; + this.url = url; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public long getSize() { + return size; + } + + public String getUrl() { + return url; + } + } \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/api/network/models/pornhub/VideoQuality.java b/src/main/java/me/srrapero720/watermedia/api/network/models/pornhub/VideoQuality.java new file mode 100644 index 00000000..77f9f57d --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/network/models/pornhub/VideoQuality.java @@ -0,0 +1,20 @@ +package me.srrapero720.watermedia.api.network.models.pornhub; + +public class VideoQuality { + + private final String resolution; + private final String uri; + + public VideoQuality(String resolution, String uri) { + this.resolution = resolution; + this.uri = uri; + } + + public String getResolution() { + return resolution; + } + + public String getUri() { + return uri; + } +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/api/network/models/twitter/GuestTokenResponse.java b/src/main/java/me/srrapero720/watermedia/api/network/models/twitter/GuestTokenResponse.java new file mode 100644 index 00000000..d0c4ab65 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/network/models/twitter/GuestTokenResponse.java @@ -0,0 +1,5 @@ +package me.srrapero720.watermedia.api.network.models.twitter; + +public class GuestTokenResponse { + public String guest_token; +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/api/network/models/twitter/RequestDetails.java b/src/main/java/me/srrapero720/watermedia/api/network/models/twitter/RequestDetails.java new file mode 100644 index 00000000..94c175f0 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/network/models/twitter/RequestDetails.java @@ -0,0 +1,8 @@ +package me.srrapero720.watermedia.api.network.models.twitter; + +import java.util.Map; + +public class RequestDetails { + public Map variables; + public Map features; +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/api/network/patch/DropboxPatch.java b/src/main/java/me/srrapero720/watermedia/api/network/patch/DropboxPatch.java new file mode 100644 index 00000000..b0348f4f --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/network/patch/DropboxPatch.java @@ -0,0 +1,27 @@ +package me.srrapero720.watermedia.api.network.patch; + +import me.srrapero720.watermedia.api.network.DynamicURL; +import me.srrapero720.watermedia.tools.exceptions.PatchingURLException; + +public class DropboxPatch extends URLPatch { + @Override + public String platform() { + return "Dropbox"; + } + + @Override + public boolean isValid(DynamicURL dynamicURL) { + String q; + return dynamicURL.asURL().getHost().contains("dropbox.com") && ((q = dynamicURL.asURL().getQuery()) != null) && q.contains("dl=0"); + } + + @Override + public DynamicURL patch(DynamicURL dynamicURL, Quality prefQuality) throws PatchingURLException { + super.patch(dynamicURL, prefQuality); + try { + return new DynamicURL(dynamicURL.toString().replace("dl=0", "dl=1"), false, false); + } catch (Exception e) { + throw new PatchingURLException(dynamicURL.getSource(), e); + } + } +} diff --git a/src/main/java/me/srrapero720/watermedia/api/network/patch/GoogleDrivePatch.java b/src/main/java/me/srrapero720/watermedia/api/network/patch/GoogleDrivePatch.java new file mode 100644 index 00000000..262f1ec5 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/network/patch/GoogleDrivePatch.java @@ -0,0 +1,39 @@ +package me.srrapero720.watermedia.api.network.patch; + +import me.srrapero720.watermedia.api.network.DynamicURL; +import me.srrapero720.watermedia.tools.exceptions.PatchingURLException; + +import java.net.URL; + +public class GoogleDrivePatch extends URLPatch { + private static final String API_KEY = "AIzaSyBiFNT6TTo506kCYYwA2NHqs36TlXC1DMo"; + private static final String API_URL = "https://www.googleapis.com/drive/v3/files/%s?alt=media&key=%s"; + + @Override + public String platform() { + return "Google Drive"; + } + + @Override + public boolean isValid(DynamicURL dynamicURL) { + URL normalURL = dynamicURL.asURL(); + return normalURL.getHost().equals("drive.google.com") && normalURL.getPath().startsWith("/file/d/"); + } + + @Override + public DynamicURL patch(DynamicURL dynamicURL, Quality prefQuality) throws PatchingURLException { + super.patch(dynamicURL, prefQuality); + try { + final URL url = dynamicURL.asURL(); + final int start = url.getPath().indexOf("/file/d/") + 4; + + int end = url.getPath().indexOf('/', start); + if (end == -1) end = url.getPath().length(); + String fileID = url.getPath().substring(start, end); + + return new DynamicURL(String.format(API_URL, fileID, API_KEY), false, false); + } catch (Exception e) { + throw new PatchingURLException(dynamicURL.getSource(), e); + } + } +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/api/network/patch/ImgurPatch.java b/src/main/java/me/srrapero720/watermedia/api/network/patch/ImgurPatch.java new file mode 100644 index 00000000..88d46354 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/network/patch/ImgurPatch.java @@ -0,0 +1,73 @@ +package me.srrapero720.watermedia.api.network.patch; + +import me.srrapero720.watermedia.api.network.DynamicURL; +import me.srrapero720.watermedia.tools.exceptions.PatchingURLException; + +public class ImgurPatch extends URLPatch { + @Override + public String platform() { + return "Imgur"; + } + + @Override + public boolean isValid(DynamicURL dynamicURL) { + return dynamicURL.asURL().getHost().equals("imgur.com"); // i.imgur.com is a working url + } + + @Override + public DynamicURL patch(DynamicURL dynamicURL, Quality preferQuality) throws PatchingURLException { + super.patch(dynamicURL, preferQuality); + try { +// String path = url.getPath(); +// if (path.endsWith("/")) path = path.substring(0, path.length() - 1); +// +// // URL DATA +// String[] ps = path.split("/"); +// String id = ps[ps.length - 1]; +// String tag = ps[ps.length - 2]; +// +// if (path.startsWith("/gallery/") || path.startsWith("/a/")) { +// Response> res = ImgurAPIv3.NET.getImageFromAlbum(id).execute(); +// ImgurAlbumTagData data; +// if (res.isSuccessful() && res.body() != null && (data = res.body().data) != null && !data.images.isEmpty()) { +// for (ImgurImage image: data.images) { +// // TODO: add support for multiple entries +// return new Result(image.link, image.type.startsWith("video"), false); +// } +// } else { +// LOGGER.debug(IT, "Imgur responses with status code: {}", res.code()); +// throw new Exception("Cannot load imgur data, " + res.message()); +// } +// } else if (path.startsWith("/t/")) { +// Response> res = ImgurAPIv3.NET.getImageFromTagGallery(tag, id).execute(); +// ImgurAlbumTagData data; +// if (res.isSuccessful() && res.body() != null && (data = res.body().data) != null) { +// if (data.images != null && !data.images.isEmpty()) { +// for (ImgurImage image: data.images) { +// // TODO: add support for multiple entries +// return new Result(image.link, image.type.startsWith("video"), false); +// } +// } else { +// LOGGER.debug(IT, "Imgur responses with status code '{}' but with a empty image list", res.code()); +// throw new Exception("Cannot load imgur data, empty list"); +// } +// } else { +// LOGGER.debug(IT, "Imgur responses with status code: {}", res.code()); +// throw new Exception("Cannot load imgur data, " + res.message()); +// } +// } else { +// Response> res = ImgurAPIv3.NET.getImage(id).execute(); +// ImgurImage data; +// if (res.isSuccessful() && res.body() != null && (data = res.body().data) != null) { +// return new Result(data.link, data.type.startsWith("video"), false); +// } else { +// LOGGER.debug(IT, "Imgur responses with status code: {}", res.code()); +// throw new Exception("Cannot load imgur data, " + res.message()); +// } +// } + throw new Exception("Unreachable code"); + } catch (Exception e) { + throw new PatchingURLException(dynamicURL, e); + } + } +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/api/network/patch/KickPatch.java b/src/main/java/me/srrapero720/watermedia/api/network/patch/KickPatch.java new file mode 100644 index 00000000..45e12781 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/network/patch/KickPatch.java @@ -0,0 +1,58 @@ +package me.srrapero720.watermedia.api.network.patch; + +import me.srrapero720.watermedia.api.network.DynamicRequest; +import me.srrapero720.watermedia.api.network.DynamicURL; +import me.srrapero720.watermedia.api.network.NetworkAPI; +import me.srrapero720.watermedia.api.network.models.kick.KickChannel; +import me.srrapero720.watermedia.api.network.models.kick.KickVideo; +import me.srrapero720.watermedia.tools.exceptions.PatchingURLException; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.ConnectException; + +public class KickPatch extends URLPatch { + private static final String API_URL = "https://kick.com/api/v1/"; + + @Override + public String platform() { + return "Kick.com"; + } + + @Override + public boolean isValid(DynamicURL dynamicURL) { + return dynamicURL.asURL().getHost().contains("kick.com"); + } + + @Override + public DynamicURL patch(DynamicURL dynamicURL, Quality preferQuality) throws PatchingURLException { + super.patch(dynamicURL, preferQuality); + + try { + if (dynamicURL.asURL().getPath().contains("/video/")) { + String videoID = dynamicURL.asURL().getPath().replace("/video/", ""); + KickVideo video = getVideoInfo(videoID); + return new DynamicURL(video.url, true, false); + } else { + String streamerName = dynamicURL.asURL().getPath().replace("/", ""); + KickChannel channel = getChannelInfo(streamerName); + if (channel.livestream == null || !channel.livestream.isStreaming) throw new ConnectException("Streamer is not online"); + return new DynamicURL(channel.url, true, true); + } + } catch (Exception e) { + throw new PatchingURLException(dynamicURL, e); + } + } + + public KickChannel getChannelInfo(String channel) throws IOException { + try (DynamicRequest request = new DynamicRequest(new DynamicURL(API_URL + "channels/" + channel)); InputStreamReader in = new InputStreamReader(request.getInputStream())) { + return NetworkAPI.GSON.fromJson(in, KickChannel.class); + } + } + + public KickVideo getVideoInfo(String videoId) throws IOException { + try (DynamicRequest request = new DynamicRequest(new DynamicURL(API_URL + "video/" + videoId)); InputStreamReader in = new InputStreamReader(request.getInputStream())) { + return NetworkAPI.GSON.fromJson(in, KickVideo.class); + } + } +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/api/network/patch/OneDrivePatch.java b/src/main/java/me/srrapero720/watermedia/api/network/patch/OneDrivePatch.java new file mode 100644 index 00000000..53008275 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/network/patch/OneDrivePatch.java @@ -0,0 +1,42 @@ +package me.srrapero720.watermedia.api.network.patch; + +import me.srrapero720.watermedia.api.network.DynamicRequest; +import me.srrapero720.watermedia.api.network.DynamicURL; +import me.srrapero720.watermedia.api.network.NetworkAPI; +import me.srrapero720.watermedia.api.network.models.onedrive.OneDriveItem; +import me.srrapero720.watermedia.tools.exceptions.PatchingURLException; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.Base64; +import java.util.regex.Pattern; + +public class OneDrivePatch extends URLPatch { + private static final Pattern ONE_DRIVE_URL_PATTERN = Pattern.compile("^https://1drv.ms/[a-z]/[a-zA-Z0-9!_-]+$"); + private static final String API_URL = "https://api.onedrive.com/v1.0/"; + + @Override + public String platform() { + return "OneDrive"; + } + + @Override + public boolean isValid(DynamicURL dynamicURL) { + return ONE_DRIVE_URL_PATTERN.matcher(dynamicURL.toString()).matches(); + } + + @Override + public DynamicURL patch(DynamicURL dynamicURL, Quality prefQuality) throws PatchingURLException { + super.patch(dynamicURL, prefQuality); + final String encodedUrl = "u!" + Base64.getUrlEncoder().withoutPadding().encodeToString(dynamicURL.getSource().getBytes()); + try (DynamicRequest request = new DynamicRequest(new DynamicURL(API_URL + "shares/" + encodedUrl + "/driveItem"))) { + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()))) { + final OneDriveItem item = NetworkAPI.GSON.fromJson(reader, OneDriveItem.class); + return new DynamicURL(item.getUrl(), false, false); + } + } catch (Exception e) { + throw new PatchingURLException(dynamicURL.getSource(), e); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/watermedia/core/network/patchs/PornHubPatch.java b/src/main/java/me/srrapero720/watermedia/api/network/patch/PornhubPatch.java similarity index 57% rename from src/main/java/org/watermedia/core/network/patchs/PornHubPatch.java rename to src/main/java/me/srrapero720/watermedia/api/network/patch/PornhubPatch.java index dd77d3a1..dc5cd4e7 100644 --- a/src/main/java/org/watermedia/core/network/patchs/PornHubPatch.java +++ b/src/main/java/me/srrapero720/watermedia/api/network/patch/PornhubPatch.java @@ -1,47 +1,44 @@ -package org.watermedia.core.network.patchs; +package me.srrapero720.watermedia.api.network.patch; -import me.srrapero720.watermedia.api.MediaContext; -import org.watermedia.api.network.MRL; +import me.srrapero720.watermedia.api.network.DynamicRequest; +import me.srrapero720.watermedia.api.network.DynamicURL; +import me.srrapero720.watermedia.api.network.models.pornhub.VideoQuality; +import me.srrapero720.watermedia.tools.ByteTools; +import me.srrapero720.watermedia.tools.exceptions.PatchingURLException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -public class PornHubPatch extends AbstractPatch { +public class PornhubPatch extends URLPatch { private static final Pattern VIDEO_QUALITY_PATTERN = Pattern.compile("(?<=\\*/)\\w+"); private static final Pattern RESOLUTION_PATTERN = Pattern.compile("(?<=[_/])\\d*P(?=_)"); @Override public String platform() { - return "PornHub"; + return "Pornhub"; } @Override - public boolean active(MediaContext context) { - return false; + public boolean isValid(DynamicURL dynamicURL) { + return (dynamicURL.asURL().getHost().equals("es.pornhub.com") || dynamicURL.asURL().getHost().equals("www.pornhub.com")) && dynamicURL.asURL().getPath().startsWith("/view_video.php"); } @Override - public boolean validate(MRL source) { - var host = source.getUri().getHost(); - var path = source.getUri().getPath(); - return (host.contains(".pornhub.com") || host.equals("pornhub.com")) && path.endsWith("/view_video.php"); - } - - @Override - public void patch(MediaContext context, MRL source) throws PatchException { + public DynamicURL patch(DynamicURL dynamicURL, Quality prefQuality) throws PatchingURLException { + super.patch(dynamicURL, prefQuality); + try (DynamicRequest connection = new DynamicRequest(dynamicURL); InputStream reader = connection.getInputStream()) { + String source = new String(ByteTools.readAllBytes(reader), StandardCharsets.UTF_8); - try (InputStream i = null/*DynamicRequest connection = new DynamicRequest(dynamicURL); InputStream reader = connection.getInputStream()*/) { -// String source = new String(ByteTools.readAllBytes(reader), StandardCharsets.UTF_8); - String apiSource = ""; List urls = new ArrayList<>(); List videoQualities = new ArrayList<>(); List matches = new ArrayList<>(); // GET QUALITIES - Matcher matcher = VIDEO_QUALITY_PATTERN.matcher(apiSource); + Matcher matcher = VIDEO_QUALITY_PATTERN.matcher(source); while (matcher.find()) { matches.add(matcher.group()); } @@ -50,7 +47,7 @@ public void patch(MediaContext context, MRL source) throws PatchException { for (String match: matches) { String regexString = "(?<=" + match + "=\")[^;]+(?=\")"; Pattern regexPattern = Pattern.compile(regexString); - Matcher regexMatcher = regexPattern.matcher(apiSource); + Matcher regexMatcher = regexPattern.matcher(source); if (regexMatcher.find()) { String value = regexMatcher.group().replaceAll("[\" +]", ""); @@ -74,18 +71,9 @@ public void patch(MediaContext context, MRL source) throws PatchException { } } -// return new DynamicURL(videoQualities.get(0).getUri(), true, false); + return new DynamicURL(videoQualities.get(0).getUri(), true, false); } catch (Exception e) { -// throw new PatchingURLException(dynamicURL, e); + throw new PatchingURLException(dynamicURL, e); } } - - @Override - public void test(MediaContext context, String url) { - - } - - private record VideoQuality(String resolution, String uri) { - - } } diff --git a/src/main/java/me/srrapero720/watermedia/api/network/patch/TwitchPatch.java b/src/main/java/me/srrapero720/watermedia/api/network/patch/TwitchPatch.java new file mode 100644 index 00000000..4f4c08a7 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/network/patch/TwitchPatch.java @@ -0,0 +1,32 @@ +package me.srrapero720.watermedia.api.network.patch; + +import me.srrapero720.watermedia.api.network.DynamicURL; +import me.srrapero720.watermedia.api.network.patch.util.Twitch; +import me.srrapero720.watermedia.tools.exceptions.PatchingURLException; + +public class TwitchPatch extends URLPatch { + @Override + public String platform() { + return "Twitch"; + } + + @Override + public boolean isValid(DynamicURL url) { + return (url.asURL().getHost().equals("www.twitch.tv") || url.asURL().getHost().equals("twitch.tv")) && url.asURL().getPath().startsWith("/"); + } + + @Override + public DynamicURL patch(DynamicURL url, Quality preferQuality) throws PatchingURLException { + super.patch(url, preferQuality); + try { + String path = url.asURL().getPath(); + if (path.startsWith("/videos/")) { + return new DynamicURL(Twitch.getVod(path.substring(8)).get(0).getUrl(), true, false); + } + + return new DynamicURL(Twitch.getStream(path.substring(1)).get(0).getUrl(), true, true); + } catch (Exception e) { + throw new PatchingURLException(url, e); + } + } +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/api/network/patch/TwitterPatch.java b/src/main/java/me/srrapero720/watermedia/api/network/patch/TwitterPatch.java new file mode 100644 index 00000000..b64837a5 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/network/patch/TwitterPatch.java @@ -0,0 +1,32 @@ +package me.srrapero720.watermedia.api.network.patch; + +import com.google.gson.Gson; +import me.srrapero720.watermedia.api.network.DynamicURL; +import me.srrapero720.watermedia.api.network.patch.util.twitter.TweetScrapper; +import me.srrapero720.watermedia.tools.exceptions.PatchingURLException; + +public class TwitterPatch extends URLPatch { + private static final Gson gson = new Gson(); + + @Override + public String platform() { + return "Twitter"; + } + + @Override + public boolean isValid(DynamicURL dynamicURL) { + return (dynamicURL.asURL().getHost().equals("www.x.com") || dynamicURL.asURL().getHost().equals("x.com") || + dynamicURL.asURL().getHost().equals("www.twitter.com") || dynamicURL.asURL().getHost().equals("twitter.com")) + && dynamicURL.asURL().getPath().matches("/[a-zA-Z0-9_]+/status/[0-9]+"); + } + + @Override + public DynamicURL patch(DynamicURL dynamicURL, Quality preferQuality) throws PatchingURLException { + super.patch(dynamicURL, preferQuality); + try { + return new DynamicURL(new TweetScrapper(gson).extractVideo(dynamicURL.getSource()).get(0), true, false); + } catch (Exception e) { + throw new PatchingURLException(dynamicURL, e); + } + } +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/api/network/patch/URLPatch.java b/src/main/java/me/srrapero720/watermedia/api/network/patch/URLPatch.java new file mode 100644 index 00000000..5a2004fc --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/network/patch/URLPatch.java @@ -0,0 +1,54 @@ +package me.srrapero720.watermedia.api.network.patch; + +import me.srrapero720.watermedia.api.network.DynamicURL; +import me.srrapero720.watermedia.tools.exceptions.PatchingURLException; + +/** + * Abstract fixers class + * To make your own fixer, you should use services.
+ * Create a file in: + * resources/META-INF/services/
+ * named: + * me.srrapero720.watermedia.api.url.fixers.URLFixer + * and put inside the entire package to your fixer. + */ +public abstract class URLPatch { + /** + * Get the name of the current patcher + * @return class name + */ + public String name() { return this.getClass().getSimpleName(); } + + /** + * Name of the platform used for this patcher + * @return platform name + */ + public abstract String platform(); + + /** + * Validates if URL can be processed by this URLPatch instance + * @param dynamicURL Valid URL to check + * @return Can be built a static dynamicURL + */ + public abstract boolean isValid(DynamicURL dynamicURL); + + /** + * Patch the URL + * @param dynamicURL URL to patch + * @return static URL + * @throws PatchingURLException if URL is null or invalid in this patch + */ + public DynamicURL patch(DynamicURL dynamicURL, Quality prefQuality) throws PatchingURLException { + if (!isValid(dynamicURL)) throw new PatchingURLException(dynamicURL.getSource(), new IllegalArgumentException("Attempt to build a invalid URL in a invalid compat")); + return null; + } + + @Override + public String toString() { + return name(); + } + + public enum Quality { + LOWEST, LOW, MIDDLE, HIGH, HIGHEST, + } +} diff --git a/src/main/java/me/srrapero720/watermedia/api/network/patch/YoutubePatch.java b/src/main/java/me/srrapero720/watermedia/api/network/patch/YoutubePatch.java new file mode 100644 index 00000000..04040c75 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/network/patch/YoutubePatch.java @@ -0,0 +1,116 @@ +package me.srrapero720.watermedia.api.network.patch; + +import com.github.kiulian.downloader.YoutubeDownloader; +import com.github.kiulian.downloader.downloader.request.RequestVideoInfo; +import com.github.kiulian.downloader.downloader.response.Response; +import com.github.kiulian.downloader.model.videos.VideoDetails; +import com.github.kiulian.downloader.model.videos.VideoInfo; +import com.github.kiulian.downloader.model.videos.formats.AudioFormat; +import com.github.kiulian.downloader.model.videos.formats.VideoFormat; +import me.srrapero720.watermedia.api.network.DynamicURL; +import me.srrapero720.watermedia.api.network.streams.StreamQuality; +import me.srrapero720.watermedia.tools.exceptions.PatchingURLException; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class YoutubePatch extends URLPatch { + private static final Pattern PATTERN = Pattern.compile("(?:youtu\\.be/|youtube\\.com/(?:embed/|v/|shorts/|feeds/api/videos/|watch\\?v=|watch\\?.+&v=))([^/?&#]+)"); + + @Override + public String platform() { + return "Youtube"; + } + + @Override + public boolean isValid(DynamicURL dynamicURL) { + return dynamicURL.isLocal() && dynamicURL.asURL().getHost().endsWith("youtube.com") || dynamicURL.asURL().getHost().endsWith("youtu.be"); + } + + @Override + public DynamicURL patch(DynamicURL dynamicURL, Quality preferQuality) throws PatchingURLException { + super.patch(dynamicURL, preferQuality); + + Matcher matcher = PATTERN.matcher(dynamicURL.toString()); + if (matcher.find()) { + try { + String videoId = matcher.group(1); + Response response = new YoutubeDownloader().getVideoInfo(new RequestVideoInfo(videoId)); + + if (response == null) throw new NullPointerException("Response from Youtube is null"); + VideoInfo videoInfo = new YoutubeDownloader().getVideoInfo(new RequestVideoInfo(videoId)).data(); + VideoDetails videoDetails = videoInfo.details(); + + if (videoDetails.isLive()) { + // LIVE STREAM + String ytLivePlaylist = fetchLivePlaylist(videoDetails.liveUrl()); + if (ytLivePlaylist == null) throw new IllegalArgumentException("Live URL playlist is null"); + + // TODO: add quality support + return new DynamicURL(StreamQuality.parse(ytLivePlaylist).get(0).getUrl(), false, true); + } else { + // TODO: add quality support + // BEST WITH ALL + VideoFormat bestAll = videoInfo.bestVideoWithAudioFormat(); + if (bestAll != null) return new DynamicURL(bestAll.url(), true, false); + + // TODO: add quality support + // WITHOUT AUDIO + VideoFormat bestWithoutAudio = videoInfo.bestVideoFormat(); + if (bestWithoutAudio != null) { + return new DynamicURL(bestWithoutAudio.url(), true, false); + } + + // TODO: add quality support + // WITHOUT VIDEO + AudioFormat bestWithoutVideo = videoInfo.bestAudioFormat(); + if (bestWithoutVideo != null) return new DynamicURL(bestWithoutAudio.url()), true, false).setAudioTrack(new URL(bestWithoutVideo.url())); + +// // BEST WITH ALL +// VideoFormat bestAll = videoInfo.bestVideoWithAudioFormat(); +// +// // WITHOUT AUDIO +// VideoFormat bestWithoutAudio = videoInfo.bestVideoFormat(); +// if (bestWithoutAudio != null) { +// // WITHOUT VIDEO +// AudioFormat bestWithoutVideo = videoInfo.bestAudioFormat(); +// if (bestWithoutVideo != null) return new Result(new URL(bestWithoutAudio.url()), true, false).setAudioTrack(new URL(bestWithoutVideo.url())); +// +// if (bestAll != null) return new Result(new URL(bestAll.url()), true, false); +// +// return new Result(new URL(bestWithoutAudio.url()), true, false); +// } + + if (bestAll != null) return new Result(new URL(bestAll.url(), true, false); + } + } catch (Exception e) { + throw new PatchingURLException(dynamicURL.getSource(), e); + } + } + + return dynamicURL; + } + + private String fetchLivePlaylist(String url) throws IOException { + URL apiUrl = new URL(url); + HttpURLConnection conn = (HttpURLConnection) apiUrl.openConnection(); + conn.setRequestMethod("GET"); + + int responseCode = conn.getResponseCode(); + if (responseCode != HttpURLConnection.HTTP_OK) return null; + + InputStream inputStream = conn.getInputStream(); + ByteArrayOutputStream result = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int length; + while ((length = inputStream.read(buffer)) != -1) { + result.write(buffer, 0, length); + } + return result.toString("UTF-8"); + } +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/api/network/patch/util/Twitch.java b/src/main/java/me/srrapero720/watermedia/api/network/patch/util/Twitch.java new file mode 100644 index 00000000..ca0f9898 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/network/patch/util/Twitch.java @@ -0,0 +1,129 @@ +package me.srrapero720.watermedia.api.network.patch.util; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import me.srrapero720.watermedia.api.network.streams.StreamQuality; +import me.srrapero720.watermedia.tools.ByteTools; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static me.srrapero720.watermedia.api.network.NetworkAPI.USER_AGENT; + +public class Twitch { + public static final String GRAPH_QL_URL = "https://gql.twitch.tv/gql"; + public static final String TTV_LIVE_API_URL_TEMPLATE = "https://usher.ttvnw.net/api/channel/hls/%s.m3u8"; + public static final String TTV_PLAYLIST_API_URL_TEMPLATE = "https://usher.ttvnw.net/vod/%s.m3u8"; + public static final String CLIENT_ID = "kimne78kx3ncx6brgo4mv6wki5h1ko"; + + private static final Gson gson = new Gson(); + + public static List getStream(String stream) throws IOException, StreamNotFound { + String apiUrl = buildApiUrl(stream, false); + return StreamQuality.parse(performGetRequest(apiUrl)); + } + + public static List getVod(String video) throws IOException, StreamNotFound { + String apiUrl = buildApiUrl(video, true); + return StreamQuality.parse(performGetRequest(apiUrl)); + } + + // BUSTED + private static String buildApiUrl(String id, boolean isVOD) throws IOException { + JsonElement response = post(id, isVOD); + JsonObject accessTokenData = response + .getAsJsonObject().get("data") + .getAsJsonObject().get(isVOD ? "videoPlaybackAccessToken" : "streamPlaybackAccessToken") + .getAsJsonObject(); + + String signature = accessTokenData.get("signature").getAsString(); + String value = accessTokenData.get("value").getAsString(); + + String url = String.format(isVOD ? TTV_PLAYLIST_API_URL_TEMPLATE : TTV_LIVE_API_URL_TEMPLATE, id); + return url + buildUrlParameters(signature, value); + } + + private static String buildUrlParameters(String signature, String value) throws UnsupportedEncodingException { + value = URLEncoder.encode(value, StandardCharsets.UTF_8.toString()); + return String.format("?acmb=e30%%3D&allow_source=true&fast_bread=true&p=7370379&play_session_id=21efcd962e7b3fbc891bac088214aa63&player_backend=mediaplayer&playlist_include_framerate=true&reassignments_supported=true&sig=%s&supported_codecs=avc1&token=%s&transcode_mode=cbr_v1&cdm=wv&player_version=1.21.0", signature, value); + } + + private static String performGetRequest(String apiUrl) throws IOException, StreamNotFound { + HttpURLConnection conn = initializeConnection(apiUrl, "GET"); + conn.setRequestProperty("x-donate-to", "https://ttv.lol/donate"); + + int responseCode = conn.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) throw new StreamNotFound("Stream not found"); + return (responseCode == HttpURLConnection.HTTP_OK) ? + new String(ByteTools.readAllBytes(conn.getInputStream())) : + new String(ByteTools.readAllBytes(conn.getErrorStream())); + } + + private static JsonElement post(String id, boolean isVOD) throws IOException { + HttpURLConnection conn = initializeConnection(GRAPH_QL_URL, "POST"); + conn.setDoOutput(true); + conn.setRequestProperty("Client-ID", CLIENT_ID); + conn.setRequestProperty("User-Agent", USER_AGENT); + conn.setRequestProperty("Content-Type", "application/json; charset=utf-8"); + + try (OutputStream os = conn.getOutputStream()) { + os.write(buildJsonString(id, isVOD).getBytes(StandardCharsets.UTF_8)); + } + + return new JsonParser().parse(new String(ByteTools.readAllBytes(conn.getInputStream()))); + } + + /** + * Initializes a connection to the given URL with the given request method. + * @param url The URL to connect to. + * @param requestMethod The request method to use. + * @return The initialized connection. + * @throws IOException If an I/O error occurs. + */ + private static HttpURLConnection initializeConnection(String url, String requestMethod) throws IOException { + URL apiUrl = new URL(url); + HttpURLConnection conn = (HttpURLConnection) apiUrl.openConnection(); + conn.setRequestMethod(requestMethod); + return conn; + } + + /** + * Builds the JSON string to send to the Twitch API. + * @param id The id of the video stream to get. + * @param isVOD Whether the video is a VOD or not. + * @return The built JSON string. + */ + private static String buildJsonString(String id, boolean isVOD) { + // Variables mapping + Map variables = new HashMap<>(); + variables.put("isLive", !isVOD); + variables.put("isVod", isVOD); + variables.put("login", !isVOD ? id : ""); + variables.put("vodID", isVOD ? id : ""); + variables.put("playerType", "site"); + + // Main JSON mapping + Map jsonMap = new HashMap<>(); + jsonMap.put("operationName", "PlaybackAccessToken_Template"); + jsonMap.put("query", "query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, $isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, params: {platform: \"web\", playerBackend: \"mediaplayer\", playerType: $playerType}) @include(if: $isLive) { value signature authorization { isForbidden forbiddenReasonCode } __typename } videoPlaybackAccessToken(id: $vodID, params: {platform: \"web\", playerBackend: \"mediaplayer\", playerType: $playerType}) @include(if: $isVod) { value signature __typename }}"); + jsonMap.put("variables", variables); + + return gson.toJson(jsonMap); + } + + public static class StreamNotFound extends Exception { + public StreamNotFound(String message) { + super(message); + } + } +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/api/network/patch/util/twitter/TweetScrapper.java b/src/main/java/me/srrapero720/watermedia/api/network/patch/util/twitter/TweetScrapper.java new file mode 100644 index 00000000..95db9c2d --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/network/patch/util/twitter/TweetScrapper.java @@ -0,0 +1,27 @@ +package me.srrapero720.watermedia.api.network.patch.util.twitter; + +import com.google.gson.Gson; + +import java.util.List; + +public class TweetScrapper { + + private final TwitterAPI twitterAPI; + private final TwitterVideoExtractor videoExtractor; + + public TweetScrapper(Gson gson) { + this.twitterAPI = new TwitterAPI(gson); + this.videoExtractor = new TwitterVideoExtractor(); + } + + public List extractVideo(String url) { + try { + String[] tokens = twitterAPI.getTokens(url); + String tweet_details = twitterAPI.getTweetDetails(url, tokens[1], tokens[0]); + return videoExtractor.extractMp4s(tweet_details, url); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/api/network/patch/util/twitter/TwitterAPI.java b/src/main/java/me/srrapero720/watermedia/api/network/patch/util/twitter/TwitterAPI.java new file mode 100644 index 00000000..cf49e5f8 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/network/patch/util/twitter/TwitterAPI.java @@ -0,0 +1,206 @@ +package me.srrapero720.watermedia.api.network.patch.util.twitter; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import me.srrapero720.watermedia.api.network.models.twitter.GuestTokenResponse; +import me.srrapero720.watermedia.api.network.models.twitter.RequestDetails; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class TwitterAPI { + private final Gson gson; + + public TwitterAPI(Gson gson) { + this.gson = gson; + } + + public String[] getTokens(String tweetUrl) throws Exception { + // Initial request to get HTML + URL url = new URL(tweetUrl); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0"); + String response = readResponse(conn); + + // Find main.js URL + Pattern pattern = Pattern.compile("https://abs.twimg.com/responsive-web/client-web/main.[^\\.]+.js"); + Matcher matcher = pattern.matcher(response); + if (!matcher.find()) { + throw new Exception("Failed to find main.js file. Tweet url: " + tweetUrl); + } + String mainJsUrl = matcher.group(); + + // Request to get main.js + url = new URL(mainJsUrl); + conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + response = readResponse(conn); + + // Find bearer token + pattern = Pattern.compile("AAAAAAAAA[^\"]+"); + matcher = pattern.matcher(response); + if (!matcher.find()) { + throw new Exception("Failed to find bearer token. Tweet url: " + tweetUrl + ", main.js url: " + mainJsUrl); + } + String bearerToken = matcher.group(); + + // Get guest token + url = new URL("https://api.twitter.com/1.1/guest/activate.json"); + conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("authorization", "Bearer " + bearerToken); + response = readResponse(conn); + + // Parse guest token from JSON response + GuestTokenResponse guestTokenResponse = gson.fromJson(response, GuestTokenResponse.class); + if (guestTokenResponse.guest_token == null) { + throw new Exception("Failed to find guest token. Tweet url: " + tweetUrl + ", main.js url: " + mainJsUrl); + } + + return new String[] {bearerToken, guestTokenResponse.guest_token}; + } + + private static String readResponse(HttpURLConnection conn) throws Exception { + if (conn.getResponseCode() != 200) { + throw new Exception("HTTP error code: " + conn.getResponseCode()); + } + + BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); + String inputLine; + StringBuffer content = new StringBuffer(); + while ((inputLine = in.readLine()) != null) { + content.append(inputLine); + } + in.close(); + conn.disconnect(); + + return content.toString(); + } + + public String getTweetDetails(String tweetUrl, String guestToken, String bearerToken) throws Exception { + // Extract tweet ID from URL + Pattern pattern = Pattern.compile("(?<=status/)\\d+"); + Matcher matcher = pattern.matcher(tweetUrl); + if (!matcher.find()) { + throw new Exception("Could not parse tweet id from your url. Tweet url: " + tweetUrl); + } + String tweetId = matcher.group(); + + // Load request details (features and variables) from file + RequestDetails requestDetails; + + String t = + "{\n" + + " \"features\":{\n" + + " \"creator_subscriptions_tweet_preview_api_enabled\":true,\n" + + " \"tweetypie_unmention_optimization_enabled\":true,\n" + + " \"responsive_web_edit_tweet_api_enabled\":true,\n" + + " \"graphql_is_translatable_rweb_tweet_is_translatable_enabled\":true,\n" + + " \"view_counts_everywhere_api_enabled\":true,\n" + + " \"longform_notetweets_consumption_enabled\":true,\n" + + " \"responsive_web_twitter_article_tweet_consumption_enabled\":false,\n" + + " \"tweet_awards_web_tipping_enabled\":false,\n" + + " \"freedom_of_speech_not_reach_fetch_enabled\":true,\n" + + " \"standardized_nudges_misinfo\":true,\n" + + " \"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled\":true,\n" + + " \"longform_notetweets_rich_text_read_enabled\":true,\n" + + " \"longform_notetweets_inline_media_enabled\":true,\n" + + " \"responsive_web_graphql_exclude_directive_enabled\":true,\n" + + " \"verified_phone_label_enabled\":false,\n" + + " \"responsive_web_media_download_video_enabled\":false,\n" + + " \"responsive_web_graphql_skip_user_profile_image_extensions_enabled\":false,\n" + + " \"responsive_web_graphql_timeline_navigation_enabled\":true,\n" + + " \"responsive_web_enhance_cards_enabled\":false\n" + + " },\n" + + " \"variables\": {\n" + + " \"withCommunity\":false,\n" + + " \"includePromotedContent\":false,\n" + + " \"withVoice\":true\n" + + " }\n" + + "}"; + + requestDetails = gson.fromJson(t, RequestDetails.class); + + String url = getDetailsUrl(tweetId, requestDetails.features, requestDetails.variables); + + // Initial request for tweet details + HttpURLConnection conn = makeGetRequest(url, bearerToken, guestToken); + int maxRetries = 10; + int curRetry = 0; + while (conn.getResponseCode() == 400 && curRetry < maxRetries) { + // Parse JSON response + String response = readResponse(conn); + JsonObject jsonResponse = JsonParser.parseString(response).getAsJsonObject(); + + if (!jsonResponse.has("errors")) { + throw new Exception("Failed to find errors in details error json. Tweet url: " + tweetUrl); + } + + pattern = Pattern.compile("Variable '([^']+)'"); + for (JsonElement errorElement : jsonResponse.getAsJsonArray("errors")) { + JsonObject error = errorElement.getAsJsonObject(); + matcher = pattern.matcher(error.get("message").getAsString()); + while (matcher.find()) { + requestDetails.variables.put(matcher.group(1), true); + } + } + + pattern = Pattern.compile("The following features cannot be null: ([^\"]+)"); + for (JsonElement errorElement : jsonResponse.getAsJsonArray("errors")) { + JsonObject error = errorElement.getAsJsonObject(); + matcher = pattern.matcher(error.get("message").getAsString()); + while (matcher.find()) { + for (String feature : matcher.group(1).split(",")) { + requestDetails.features.put(feature.trim(), true); + } + } + } + + url = getDetailsUrl(tweetId, requestDetails.features, requestDetails.variables); + conn = makeGetRequest(url, bearerToken, guestToken); + curRetry++; + } + + if (conn.getResponseCode() != 200) { + throw new Exception("Failed to get tweet details. Tweet url: " + tweetUrl); + } + + return readResponse(conn); + } + + + private String getDetailsUrl(String tweetId, Map features, Map variables) throws UnsupportedEncodingException { + // Create a copy of variables - we don't want to modify the original + Map newVariables = new HashMap<>(variables); + newVariables.put("tweetId", tweetId); + + String variablesJson = gson.toJson(newVariables); + String featuresJson = gson.toJson(features); + + String encodedVariables = URLEncoder.encode(variablesJson, "UTF-8"); + String encodedFeatures = URLEncoder.encode(featuresJson, "UTF-8"); + + return "https://twitter.com/i/api/graphql/0hWvDhmW8YQ-S_ib3azIrw/TweetResultByRestId?variables=" + encodedVariables + "&features=" + encodedFeatures; + } + + private static HttpURLConnection makeGetRequest(String url, String bearerToken, String guestToken) throws IOException { + URL urlObj = new URL(url); + HttpURLConnection conn = (HttpURLConnection) urlObj.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("authorization", "Bearer " + bearerToken); + conn.setRequestProperty("x-guest-token", guestToken); + return conn; + } +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/api/network/patch/util/twitter/TwitterVideoExtractor.java b/src/main/java/me/srrapero720/watermedia/api/network/patch/util/twitter/TwitterVideoExtractor.java new file mode 100644 index 00000000..70af5966 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/network/patch/util/twitter/TwitterVideoExtractor.java @@ -0,0 +1,141 @@ +package me.srrapero720.watermedia.api.network.patch.util.twitter; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class TwitterVideoExtractor { + + public List extractMp4s(String jsonString, String tweetUrl) { + // regex patterns + Pattern amplitudePattern = Pattern.compile("(https://video.twimg.com/amplify_video/(\\d+)/vid/(\\d+x\\d+)/[^.]+.mp4\\?tag=\\d+)"); + Pattern extTwPattern = Pattern.compile("(https://video.twimg.com/ext_tw_video/(\\d+)/pu/vid/(\\d+x\\d+)/[^.]+.mp4\\?tag=\\d+)"); + Pattern tweetVideoPattern = Pattern.compile("https://video.twimg.com/tweet_video/[^\"]+"); + Pattern containerPattern = Pattern.compile("https://video.twimg.com/[^\"].*container=fmp4"); + + String mediaId = getAssociatedMediaId(jsonString, tweetUrl); + + List matches = findMatches(jsonString, amplitudePattern); + matches.addAll(findMatches(jsonString, extTwPattern)); + List containerMatches = findMatches(jsonString, containerPattern); + List tweetVideoMatches = findMatches(jsonString, tweetVideoPattern); + + if (matches.isEmpty() && !tweetVideoMatches.isEmpty()) { + return tweetVideoMatches; + } + + Map> results = new HashMap<>(); + + for (String match : matches) { + Matcher matcher = amplitudePattern.matcher(match); + + boolean find = matcher.find(); + if (!find) { + matcher = extTwPattern.matcher(match); + find = matcher.find(); + } + + if (find) { + String url = matcher.group(1); + String tweetId = matcher.group(2); + String resolution = matcher.group(3); + + if (!results.containsKey(tweetId)) { + results.put(tweetId, new HashMap<>()); + results.get(tweetId).put("resolution", resolution); + results.get(tweetId).put("url", url); + } else { + int[] myDims = Arrays.stream(resolution.split("x")).mapToInt(Integer::parseInt).toArray(); + int[] theirDims = Arrays.stream(results.get(tweetId).get("resolution").split("x")).mapToInt(Integer::parseInt).toArray(); + + if (myDims[0] * myDims[1] > theirDims[0] * theirDims[1]) { + results.get(tweetId).put("resolution", resolution); + results.get(tweetId).put("url", url); + } + } + } + } + + // Fix urls in the containerMatches list splitting ? + containerMatches.replaceAll(s -> s.split("\\?")[0]); + + if (mediaId != null) { + List allUrls = new ArrayList<>(); + for (Map value : results.values()) { + allUrls.add(value.get("url").split("\\?")[0]); + } + allUrls.addAll(containerMatches); + + List urlWithMediaId = new ArrayList<>(); + for (String url : allUrls) { + if (url.contains(mediaId)) { + urlWithMediaId.add(url); + } + } + + if (!urlWithMediaId.isEmpty()) { + return urlWithMediaId; + } + } + + if (!containerMatches.isEmpty()) { + return containerMatches; + } + + List resultUrls = new ArrayList<>(); + for (Map value : results.values()) { + resultUrls.add(value.get("url").split("\\?")[0]); + } + + return resultUrls; + } + + private List findMatches(String inputString, Pattern pattern) { + List matches = new ArrayList<>(); + Matcher matcher = pattern.matcher(inputString); + + while (matcher.find()) { + matches.add(matcher.group()); + } + + return matches; + } + + public String getAssociatedMediaId(String jsonString, String tweetUrl) { + String sid = getTweetStatusId(tweetUrl); + Pattern pattern = Pattern.compile("\"expanded_url\"\\s*:\\s*\"https://twitter\\.com/[^/]+/status/" + sid + "/[^\"]+\",\\s*\"id_str\"\\s*:\\s*\"\\d+\","); + Matcher matcher = pattern.matcher(jsonString); + + if (matcher.find()) { + String target = matcher.group(); + target = target.substring(0, target.length() - 1); //remove the coma at the end + JsonObject jsonObject = JsonParser.parseString("{" + target + "}").getAsJsonObject(); + return jsonObject.get("id_str").getAsString(); + } + + return null; + } + + public String getTweetStatusId(String tweetUrl) { + String sidPattern = "https://twitter\\.com/[^/]+/status/(\\d+)"; + + if (tweetUrl.charAt(tweetUrl.length() - 1) != '/') { + tweetUrl = tweetUrl + "/"; + } + + Pattern pattern = Pattern.compile(sidPattern); + Matcher matcher = pattern.matcher(tweetUrl); + + if (matcher.find()) { + return matcher.group(1); + } else { + System.out.println("error, could not get status id from this tweet url: " + tweetUrl); + System.exit(1); + } + + return null; + } +} \ No newline at end of file diff --git a/src/main/java/org/watermedia/core/network/NetworkStream.java b/src/main/java/me/srrapero720/watermedia/api/network/streams/StreamQuality.java similarity index 62% rename from src/main/java/org/watermedia/core/network/NetworkStream.java rename to src/main/java/me/srrapero720/watermedia/api/network/streams/StreamQuality.java index 28a478d3..17869417 100644 --- a/src/main/java/org/watermedia/core/network/NetworkStream.java +++ b/src/main/java/me/srrapero720/watermedia/api/network/streams/StreamQuality.java @@ -1,4 +1,4 @@ -package org.watermedia.core.network; +package me.srrapero720.watermedia.api.network.streams; import java.util.ArrayList; import java.util.List; @@ -6,7 +6,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -public final class NetworkStream implements Comparable { +public class StreamQuality implements Comparable { private static final Pattern M3U8_STREAM_INF_RE = Pattern.compile("^#EXT-X-STREAM-INF:(.*)"); private static final Pattern M3U8_INF_VALUE_RE = Pattern.compile("([A-Z-]+)=(?:([^,]+)|\"([^\"]+?)\")"); private static final Pattern HTTP_URL_RE = Pattern.compile("https?://.*"); @@ -64,10 +64,6 @@ public int getHeight() { return height; } - public int getPixelDensity() { - return width * height; - } - public int getFramerate() { return framerate; } @@ -76,18 +72,16 @@ public String getUrl() { return url; } - public static List parse(String playlistData) { - List result = new ArrayList<>(); - if (playlistData == null || playlistData.isEmpty()) return result; - + public static List parse(String playlistData) { String[] lines = playlistData.split("\n"); - NetworkStream currentQuality = null; + List streamQualities = new ArrayList<>(); + StreamQuality currentQuality = null; - for (String line: lines) { + for (String line : lines) { Matcher matcher = M3U8_STREAM_INF_RE.matcher(line); if (matcher.matches()) { String streamInformation = matcher.group(1); - currentQuality = new NetworkStream(); + currentQuality = new StreamQuality(); Matcher valueMatcher = M3U8_INF_VALUE_RE.matcher(streamInformation); while (valueMatcher.find()) { @@ -98,37 +92,46 @@ public static List parse(String playlistData) { // Note: using `parseFloat` to have a more lax parser which does not panic on "60.000". // Twitch sends framerate using this notation which causes parseInt to throw. switch (key) { - case "BANDWIDTH" -> currentQuality.setBandwidth((int) Float.parseFloat(value)); - case "RESOLUTION" -> currentQuality.setResolution(value); - case "CODECS" -> currentQuality.setCodecs(value); - case "FRAME-RATE" -> currentQuality.setFramerate((int) Float.parseFloat(value)); + case "BANDWIDTH": + currentQuality.setBandwidth((int) Float.parseFloat(value)); + break; + case "RESOLUTION": + currentQuality.setResolution(value); + break; + case "CODECS": + currentQuality.setCodecs(value); + break; + case "FRAME-RATE": + currentQuality.setFramerate((int) Float.parseFloat(value)); + break; } } } else if (HTTP_URL_RE.matcher(line).matches()) { if (currentQuality != null) { currentQuality.setUrl(line); - result.add(currentQuality); + streamQualities.add(currentQuality); currentQuality = null; } } } - // Sort them (in reverse) based on their pixel density and framerate - result.sort((q1, q2) -> { - int den1 = q1.getPixelDensity(); - int den2 = q2.getPixelDensity(); - - int diff = den1 - den2; - - if (diff == 0) diff = q2.framerate - q1.framerate; - return diff; + // Sort them (in reverse) based on their width and framerate + // Assumptions made here: + // - All videos have the same aspect ratio + // => as height is proportional to width don't need to take it into account + // - Video resolution is more important than framerate + // => in practice higher resolutions always come with higher framerate (we parse from YT and Twitch) + streamQualities.sort((q1, q2) -> { + int res = q2.width - q1.width; + if (res == 0) res = q2.framerate - q1.framerate; + return res; }); - return result; + return streamQualities; } @Override - public int compareTo(NetworkStream streamQuality) { + public int compareTo(StreamQuality streamQuality) { int res = streamQuality.width - width; if (res == 0) res = streamQuality.framerate - framerate; return res; @@ -137,20 +140,15 @@ public int compareTo(NetworkStream streamQuality) { @Override public boolean equals(Object o) { if (this == o) return true; - if (!(o instanceof NetworkStream that)) return false; + if (!(o instanceof StreamQuality)) return false; + StreamQuality that = (StreamQuality) o; return bandwidth == that.bandwidth && width == that.width && height == that.height && framerate == that.framerate && Objects.equals(codecs, that.codecs) && Objects.equals(url, that.url); } @Override public String toString() { - return "StreamQuality{" + - "bandwidth=" + bandwidth + - ", width=" + width + - ", height=" + height + - ", framerate=" + framerate + - ", codecs='" + codecs + '\'' + - ", url='" + url + '\'' + - '}'; + return String.format("Bandwidth: %d, Resolution: %dx%d, Framerate: %d, Codecs: %s, URL: %s", + bandwidth, width, height, framerate, codecs, url); } @Override diff --git a/src/main/java/me/srrapero720/watermedia/api/player/IAudioPlayer.java b/src/main/java/me/srrapero720/watermedia/api/player/IAudioPlayer.java deleted file mode 100644 index 45c4b399..00000000 --- a/src/main/java/me/srrapero720/watermedia/api/player/IAudioPlayer.java +++ /dev/null @@ -1,5 +0,0 @@ -package me.srrapero720.watermedia.api.player; - -public interface IAudioPlayer extends IMediaPlayer { - -} diff --git a/src/main/java/me/srrapero720/watermedia/api/player/IMediaPlayer.java b/src/main/java/me/srrapero720/watermedia/api/player/IMediaPlayer.java deleted file mode 100644 index 72fffb50..00000000 --- a/src/main/java/me/srrapero720/watermedia/api/player/IMediaPlayer.java +++ /dev/null @@ -1,71 +0,0 @@ -package me.srrapero720.watermedia.api.player; - -public interface IMediaPlayer { - boolean start(); - - boolean startPaused(); - - boolean resume(); - - boolean pause(); - - boolean pause(boolean paused); - - boolean stop(); - - boolean togglePlay(); - - boolean seek(long time); - - boolean seekQuick(long time); - - boolean fastFoward(); - - boolean fastRewind(); - - boolean speed(float speed); - - boolean mute(); - - boolean mute(boolean muted); - - boolean unmute(); - - boolean repeaat(); - - boolean repeaat(boolean repeat); - - // status - boolean usable(); - - boolean loading(); - - boolean buffering(); - - boolean ready(); - - boolean paused(); - - boolean playing(); - - boolean stopped(); - - boolean ended(); - - boolean muted(); - - boolean validSource(); - - boolean liveSource(); - - boolean canSeek(); - - long duration(); - - long time(); - - void release(); - - static enum Status { - } -} diff --git a/src/main/java/me/srrapero720/watermedia/api/player/IVideoPlayer.java b/src/main/java/me/srrapero720/watermedia/api/player/IVideoPlayer.java deleted file mode 100644 index ab30f6e7..00000000 --- a/src/main/java/me/srrapero720/watermedia/api/player/IVideoPlayer.java +++ /dev/null @@ -1,16 +0,0 @@ -package me.srrapero720.watermedia.api.player; - -import java.nio.ByteBuffer; - -public interface IVideoPlayer extends IAudioPlayer, IMediaPlayer { - - int width(); - - int height(); - - ByteBuffer textureBuffer(); - - int prepare(); - - int texture(); -} diff --git a/src/main/java/me/srrapero720/watermedia/api/player/PlayerAPI.java b/src/main/java/me/srrapero720/watermedia/api/player/PlayerAPI.java index 51afbfb3..f0eb0ac4 100644 --- a/src/main/java/me/srrapero720/watermedia/api/player/PlayerAPI.java +++ b/src/main/java/me/srrapero720/watermedia/api/player/PlayerAPI.java @@ -1,10 +1,12 @@ package me.srrapero720.watermedia.api.player; -import org.watermedia.WaterMedia; -import org.watermedia.api.WaterMediaAPI; -import org.watermedia.tools.DataTool; -import org.watermedia.tools.IOTool; -import org.watermedia.tools.JarTool; +import me.srrapero720.watermedia.WaterMedia; +import me.srrapero720.watermedia.api.WaterMediaAPI; +import me.srrapero720.watermedia.api.player.vlc.SimplePlayer; +import me.srrapero720.watermedia.tools.DataTool; +import me.srrapero720.watermedia.tools.IOTool; +import me.srrapero720.watermedia.tools.JarTool; +import me.srrapero720.watermedia.loader.ILoader; import org.apache.logging.log4j.Marker; import org.apache.logging.log4j.MarkerManager; import uk.co.caprica.vlcj.factory.MediaPlayerFactory; @@ -14,7 +16,7 @@ import java.nio.file.Path; import java.util.*; -import static org.watermedia.WaterMedia.LOGGER; +import static me.srrapero720.watermedia.WaterMedia.LOGGER; public class PlayerAPI extends WaterMediaAPI { private static final Marker IT = MarkerManager.getMarker(PlayerAPI.class.getSimpleName()); @@ -59,7 +61,7 @@ public static MediaPlayerFactory getFactorySoundOnly() { * check VideoLAN wiki * @param resLoc the identifier (ResourceLocation#toString()) * @param vlcArgs arguments used to create new VLC player instances - * @return MediaPlayerFactory to create custom VLC players. + * @return MediaPlayerFactory to create custom VLC players. {@link SimplePlayer} can accept factory for new instances */ public static MediaPlayerFactory registerFactory(String resLoc, String[] vlcArgs) { if (NativeDiscovery.discovery()) { @@ -74,6 +76,24 @@ public static MediaPlayerFactory registerFactory(String resLoc, String[] vlcArgs return null; } + /** + * Use your own VLCArgs at your own risk + * By default this method makes a ReleaseHook to release factory on process shutdown + * Suggestion: Use the same VLC arguments for logging but with another filename + * Example:
 "--logfile", "logs/vlc/mymod-latest.log",
+ * check VideoLAN wiki + * @param vlcArgs arguments to make another VLC instance + * @return a PlayerFactory to create custom VLC players. {@link SimplePlayer} can accept factory for new instances + * @deprecated use instead {@link PlayerAPI#registerFactory(String, String[])}. Fallback here is not efficient + */ + public static MediaPlayerFactory customFactory(String[] vlcArgs) { + int fakeID = 0; + while (FACTORIES.containsKey(WaterMedia.asResource("unidentified_" + fakeID))) { + fakeID++; + } + return registerFactory(WaterMedia.asResource("unidentified_" + fakeID), vlcArgs); + } + // LOADING private Path dir; private Path logs; @@ -93,7 +113,7 @@ public Priority priority() { } @Override - public boolean prepare(WaterMedia.ILoader bootCore) throws Exception { + public boolean prepare(ILoader bootCore) throws Exception { LOGGER.info(IT, "Binaries are {}", wrapped ? "wrapped" : "not wrapped"); if (wrapped) { String versionInJar = JarTool.readString(configInput); @@ -123,16 +143,16 @@ public boolean prepare(WaterMedia.ILoader bootCore) throws Exception { } @Override - public void start(WaterMedia.ILoader bootCore) throws Exception { + public void start(ILoader bootCore) throws Exception { if (extract) { LOGGER.info(IT, "Extracting VideoLAN binaries..."); - if ((!zipOutput.exists() && JarTool.extract(zipInput, zipOutput.toPath())) || zipOutput.exists()) { - IOTool.un7zip(zipOutput.toPath()); + if ((!zipOutput.exists() && JarTool.copyAsset(zipInput, zipOutput.toPath())) || zipOutput.exists()) { + IOTool.un7zip(IT, zipOutput.toPath()); if (!zipOutput.delete()) { LOGGER.error(IT, "Failed to delete binaries zip file..."); } - JarTool.extract(configInput, configOutput.toPath()); + JarTool.copyAsset(configInput, configOutput.toPath()); LOGGER.info(IT, "VideoLAN binaries extracted successfully"); } else { @@ -143,7 +163,7 @@ public void start(WaterMedia.ILoader bootCore) throws Exception { try { String[] args = JarTool.readArray("videolan/arguments.json"); registerFactory(WaterMedia.asResource("default"), args); - registerFactory(WaterMedia.asResource("sound_only"), DataTool.concat(args, "--vout=none")); + registerFactory(WaterMedia.asResource("sound_only"), DataTool.concatArray(args, "--vout=none")); Runtime.getRuntime().addShutdownHook(new Thread(this::release)); } catch (Exception e) { diff --git a/src/main/java/me/srrapero720/watermedia/api/player/old/VLCPlayer.java b/src/main/java/me/srrapero720/watermedia/api/player/SyncBasePlayer.java similarity index 93% rename from src/main/java/me/srrapero720/watermedia/api/player/old/VLCPlayer.java rename to src/main/java/me/srrapero720/watermedia/api/player/SyncBasePlayer.java index b927b4c6..f209c26c 100644 --- a/src/main/java/me/srrapero720/watermedia/api/player/old/VLCPlayer.java +++ b/src/main/java/me/srrapero720/watermedia/api/player/SyncBasePlayer.java @@ -1,8 +1,7 @@ -package me.srrapero720.watermedia.api.player.old; +package me.srrapero720.watermedia.api.player; import com.sun.jna.Platform; -import me.srrapero720.watermedia.api.player.PlayerAPI; -import org.watermedia.tools.ThreadTool; +import me.srrapero720.watermedia.tools.ThreadTool; import org.apache.logging.log4j.Marker; import org.apache.logging.log4j.MarkerManager; import uk.co.caprica.vlcj.factory.MediaPlayerFactory; @@ -20,9 +19,9 @@ import java.net.URISyntaxException; import java.net.URL; -import static org.watermedia.WaterMedia.LOGGER; +import static me.srrapero720.watermedia.WaterMedia.LOGGER; -public abstract class VLCPlayer { +public abstract class SyncBasePlayer { protected static final Marker IT = MarkerManager.getMarker("SyncMediaPlayer"); protected static final WaterMediaPlayerEventListener LISTENER = new WaterMediaPlayerEventListener(); @@ -42,7 +41,7 @@ public abstract class VLCPlayer { */ public void submit(Runnable r) { if (raw != null) raw.mediaPlayer().submit(r); } - protected VLCPlayer(MediaPlayerFactory factory, RenderCallback renderCallback, SimpleBufferFormatCallback bufferFormatCallback) { + protected SyncBasePlayer(MediaPlayerFactory factory, RenderCallback renderCallback, SimpleBufferFormatCallback bufferFormatCallback) { this.init(factory, renderCallback, bufferFormatCallback); } @@ -50,7 +49,7 @@ protected VLCPlayer(MediaPlayerFactory factory, RenderCallback renderCallback, S * This constructor skips raw player creation, instead waits for {@link #init(MediaPlayerFactory, RenderCallback, SimpleBufferFormatCallback)} to create raw player * Intended to be used just in case you need to do some special implementations of {@link RenderCallback} or {@link SimpleBufferFormatCallback} */ - protected VLCPlayer() {} + protected SyncBasePlayer() {} /** * Creates a raw player and makes this works normally @@ -72,12 +71,12 @@ protected void init(MediaPlayerFactory factory, RenderCallback renderCallback, S private boolean rpa(CharSequence url) { // request player action if (raw == null) return false; try { -// URLFixer.Result result = UrlAPI.fixURL(url.toString()); -// if (result == null) throw new IllegalArgumentException("Invalid URL"); -// -// this.url = result.url; -// this.audioUrl = result.audioUrl; -// this.live = result.assumeStream; + URLFixer.Result result = UrlAPI.fixURL(url.toString()); + if (result == null) throw new IllegalArgumentException("Invalid URL"); + + this.url = result.url; + this.audioUrl = result.audioUrl; + this.live = result.assumeStream; return true; } catch (Exception e) { LOGGER.error(IT, "Failed to load player", e); @@ -141,6 +140,12 @@ public void startPaused(CharSequence url, String[] vlcArgs) { } } + @Deprecated + public State getRawPlayerState() { + if (raw == null) return State.ERROR; + return raw.mediaPlayer().status().state(); + } + public void resume() { this.play(); } @@ -311,7 +316,7 @@ public void setMuteMode(boolean mode) { */ public long getDuration() { if (raw == null) return 0L; - if (!isValid() || (Platform.isLinux() && this.isStopped())) return 0L; + if (!isValid() || (Platform.isLinux() && getRawPlayerState().equals(State.STOPPED))) return 0L; return raw.mediaPlayer().status().length(); } diff --git a/src/main/java/me/srrapero720/watermedia/api/player/SyncMusicPlayer.java b/src/main/java/me/srrapero720/watermedia/api/player/SyncMusicPlayer.java new file mode 100644 index 00000000..00749aeb --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/player/SyncMusicPlayer.java @@ -0,0 +1,18 @@ +package me.srrapero720.watermedia.api.player; + +import uk.co.caprica.vlcj.factory.MediaPlayerFactory; + +/** + * Player variant with NO VIDEO + */ +public class SyncMusicPlayer extends SyncBasePlayer { + + + public SyncMusicPlayer(MediaPlayerFactory factory) { + super(factory, null, null); + } + + public SyncMusicPlayer() { + super(PlayerAPI.getFactorySoundOnly(), null, null); + } +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/api/player/old/VideoPBOPlayer.java b/src/main/java/me/srrapero720/watermedia/api/player/SyncVideoPlayer.java similarity index 72% rename from src/main/java/me/srrapero720/watermedia/api/player/old/VideoPBOPlayer.java rename to src/main/java/me/srrapero720/watermedia/api/player/SyncVideoPlayer.java index 13008d37..55e9fcfd 100644 --- a/src/main/java/me/srrapero720/watermedia/api/player/old/VideoPBOPlayer.java +++ b/src/main/java/me/srrapero720/watermedia/api/player/SyncVideoPlayer.java @@ -1,41 +1,29 @@ -package me.srrapero720.watermedia.api.player.old; +package me.srrapero720.watermedia.api.player; -import me.srrapero720.watermedia.api.player.PlayerAPI; import me.srrapero720.watermedia.api.rendering.RenderAPI; import org.apache.logging.log4j.Marker; import org.apache.logging.log4j.MarkerManager; import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL12; -import org.lwjgl.opengl.GL15; -import org.lwjgl.opengl.GL21; import uk.co.caprica.vlcj.factory.MediaPlayerFactory; import uk.co.caprica.vlcj.player.embedded.videosurface.callback.BufferFormat; import java.awt.*; -import java.nio.Buffer; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantLock; -import static org.watermedia.WaterMedia.LOGGER; +import static me.srrapero720.watermedia.WaterMedia.LOGGER; -/** - * WARNING: Experimental - * this class works same as SyncVideoPlayer but uses a PBO - * Use it only to test + contribute to its functionality - */ -public class VideoPBOPlayer extends VLCPlayer { - private static final Marker IT = MarkerManager.getMarker("VideoPlayer"); +public class SyncVideoPlayer extends SyncBasePlayer { + private static final Marker IT = MarkerManager.getMarker("SyncVideoPlayer"); private final int texture; - private final int pbo; private volatile int width = 1; private volatile int height = 1; - private ByteBuffer buffer; - private ByteBuffer pboBuffer; + private volatile ByteBuffer buffer; private volatile Throwable exception; - private final BufferHelper bufferHelper; protected final Executor playerThreadEx; protected final ReentrantLock renderLock = new ReentrantLock(); @@ -50,7 +38,7 @@ public class VideoPBOPlayer extends VLCPlayer { * PlayerAPI (including all Players) are intended to be rewrited in 2.1.0 */ @Deprecated - public VideoPBOPlayer(Executor playerThreadEx) { this(null, playerThreadEx, DEFAULT_BUFFER_HELPER); } + public SyncVideoPlayer(Executor playerThreadEx) { this(null, playerThreadEx, DEFAULT_BUFFER_HELPER); } /** @@ -60,8 +48,7 @@ public class VideoPBOPlayer extends VLCPlayer { * @deprecated Future replacement is a static method inside {@link PlayerAPI}. * PlayerAPI (including all Players) are intended to be rewrited in 2.1.0 */ - @Deprecated - public VideoPBOPlayer(MediaPlayerFactory factory, Executor playerThreadEx) { this(factory, playerThreadEx, DEFAULT_BUFFER_HELPER); } + public SyncVideoPlayer(MediaPlayerFactory factory, Executor playerThreadEx) { this(factory, playerThreadEx, DEFAULT_BUFFER_HELPER); } /** * Creates a player instance @@ -69,30 +56,27 @@ public class VideoPBOPlayer extends VLCPlayer { * @param bufferHelper helper to create IntBuffers * @deprecated mod now integrates an own MemoryTracker with a BufferAlloc */ - @Deprecated - public VideoPBOPlayer(Executor playerThreadEx, BufferHelper bufferHelper) { this(null, playerThreadEx, bufferHelper); } + public SyncVideoPlayer(Executor playerThreadEx, BufferHelper bufferHelper) { this(null, playerThreadEx, bufferHelper); } /** * Creates a player instance * @param factory custom MediaPlayerFactory instance * @param playerThreadEx executor of render thread for an async task (normally Minecraft.getInstance()) * @param bufferHelper helper to create IntBuffers - * @deprecated mod now integrates an own MemoryTracker with a BufferAlloc + * @deprecated mod now integrates an own MemoryTracker with a BufferAlloc, + * it releases the memory properly when usage is done (not bugging GC) */ - @Deprecated - public VideoPBOPlayer(MediaPlayerFactory factory, Executor playerThreadEx, BufferHelper bufferHelper) { + @Deprecated(forRemoval = true) + public SyncVideoPlayer(MediaPlayerFactory factory, Executor playerThreadEx, BufferHelper bufferHelper) { super(); this.playerThreadEx = playerThreadEx; this.texture = GL11.glGenTextures(); - this.pbo = GL15.glGenBuffers(); - this.bufferHelper = bufferHelper; this.init(factory, (mediaPlayer, nativeBuffers, bufferFormat) -> { renderLock.lock(); // we are running in a native thread!! careful try { - if (buffer == null) return; - ((Buffer) nativeBuffers[0]).rewind(); - buffer.put(nativeBuffers[0]); - ((Buffer) buffer).rewind(); + // FIXME: this increases allocation rate as HELL. we need to find out the source of the buffer pointers and allocate it directly + if (mediaPlayer.isReleased()) return; + this.buffer = nativeBuffers[0]; updateFrame.set(true); } catch (Throwable t) { if (exception == null) { @@ -107,7 +91,7 @@ public VideoPBOPlayer(MediaPlayerFactory factory, Executor playerThreadEx, Buffe try { width = sourceWidth; height = sourceHeight; - buffer = bufferHelper.create(sourceWidth * sourceHeight * 4); + buffer = ByteBuffer.allocateDirect(sourceWidth * sourceHeight * 4).order(ByteOrder.nativeOrder()); updateFrame.set(true); updateFirstFrame.set(true); } catch (Throwable t) { @@ -118,15 +102,12 @@ public VideoPBOPlayer(MediaPlayerFactory factory, Executor playerThreadEx, Buffe } finally { renderLock.unlock(); } + // TODO: This is wrong; https://wiki.videolan.org/Chroma/ return new BufferFormat("RGBA", sourceWidth, sourceHeight, new int[]{sourceWidth * 4}, new int[]{sourceHeight}); }); if (raw() == null) { GL11.glDeleteTextures(texture); } - - GL15.glBindBuffer(GL21.GL_PIXEL_UNPACK_BUFFER, pbo); - GL15.glBufferData(GL21.GL_PIXEL_UNPACK_BUFFER, this.width * this.height * 4L, GL15.GL_STREAM_DRAW); - GL15.glBindBuffer(GL21.GL_PIXEL_UNPACK_BUFFER, 0); } /** @@ -200,24 +181,8 @@ public void preRender() { if (raw() == null) return; renderLock.lock(); try { - // BIND PBO - GL15.glBindBuffer(GL21.GL_PIXEL_UNPACK_BUFFER, pbo); - - // COPY TO PBO - pboBuffer = GL15.glMapBuffer(GL21.GL_PIXEL_UNPACK_BUFFER, GL15.GL_WRITE_ONLY, this.width * this.height * 4L, pboBuffer); - pboBuffer.put(buffer); - GL15.glUnmapBuffer(GL21.GL_PIXEL_UNPACK_BUFFER); - - // PARAMS - GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); - GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, width, height, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, 0); - - // UNBIND PBO - GL21.glBindBuffer(GL21.GL_PIXEL_UNPACK_BUFFER, 0); + if (updateFrame.compareAndSet(true, false)) + RenderAPI.applyBuffer(buffer, texture, width, height, updateFirstFrame.compareAndSet(true, forceFirstFrame.get())); } finally { renderLock.unlock(); } @@ -283,11 +248,7 @@ public Dimension getMediaDimensions() { */ @Override public void release() { - playerThreadEx.execute(() -> { - GL11.glDeleteTextures(texture); - GL21.glDeleteBuffers(pbo); - if (bufferHelper == DEFAULT_BUFFER_HELPER) RenderAPI.freeByteBuffer(buffer); - }); + playerThreadEx.execute(() -> GL11.glDeleteTextures(texture)); super.release(); buffer = null; } diff --git a/src/main/java/me/srrapero720/watermedia/api/player/impl/FFmpegMusicPlayer.java b/src/main/java/me/srrapero720/watermedia/api/player/impl/FFmpegMusicPlayer.java deleted file mode 100644 index 42e3cc75..00000000 --- a/src/main/java/me/srrapero720/watermedia/api/player/impl/FFmpegMusicPlayer.java +++ /dev/null @@ -1,4 +0,0 @@ -package me.srrapero720.watermedia.api.player.impl; - -public class FFmpegMusicPlayer { -} diff --git a/src/main/java/me/srrapero720/watermedia/api/player/impl/FFmpegPlayer.java b/src/main/java/me/srrapero720/watermedia/api/player/impl/FFmpegPlayer.java deleted file mode 100644 index be0487d6..00000000 --- a/src/main/java/me/srrapero720/watermedia/api/player/impl/FFmpegPlayer.java +++ /dev/null @@ -1,173 +0,0 @@ -package me.srrapero720.watermedia.api.player.impl; - -import me.srrapero720.watermedia.api.player.IMediaPlayer; - -import java.io.FileInputStream; -import java.io.InputStream; - -public class FFmpegPlayer implements IMediaPlayer { - - public FFmpegPlayer() { - - } - - @Override - public boolean start() { - return false; - } - - @Override - public boolean startPaused() { - return false; - } - - @Override - public boolean resume() { - return false; - } - - @Override - public boolean pause() { - return false; - } - - @Override - public boolean pause(boolean paused) { - return false; - } - - @Override - public boolean stop() { - return false; - } - - @Override - public boolean togglePlay() { - return false; - } - - @Override - public boolean seek(long time) { - return false; - } - - @Override - public boolean seekQuick(long time) { - return false; - } - - @Override - public boolean fastFoward() { - return false; - } - - @Override - public boolean fastRewind() { - return false; - } - - @Override - public boolean speed(float speed) { - return false; - } - - @Override - public boolean mute() { - return false; - } - - @Override - public boolean mute(boolean muted) { - return false; - } - - @Override - public boolean unmute() { - return false; - } - - @Override - public boolean repeaat() { - return false; - } - - @Override - public boolean repeaat(boolean repeat) { - return false; - } - - @Override - public boolean usable() { - return false; - } - - @Override - public boolean loading() { - return false; - } - - @Override - public boolean buffering() { - return false; - } - - @Override - public boolean ready() { - return false; - } - - @Override - public boolean paused() { - return false; - } - - @Override - public boolean playing() { - return false; - } - - @Override - public boolean stopped() { - return false; - } - - @Override - public boolean ended() { - return false; - } - - @Override - public boolean muted() { - return false; - } - - @Override - public boolean validSource() { - return false; - } - - @Override - public boolean liveSource() { - return false; - } - - @Override - public boolean canSeek() { - return false; - } - - @Override - public long duration() { - return 0; - } - - @Override - public long time() { - return 0; - } - - @Override - public void release() { - - } -} diff --git a/src/main/java/me/srrapero720/watermedia/api/player/impl/FFmpegVideoPlayer.java b/src/main/java/me/srrapero720/watermedia/api/player/impl/FFmpegVideoPlayer.java deleted file mode 100644 index 9d0b829a..00000000 --- a/src/main/java/me/srrapero720/watermedia/api/player/impl/FFmpegVideoPlayer.java +++ /dev/null @@ -1,33 +0,0 @@ -package me.srrapero720.watermedia.api.player.impl; - -import me.srrapero720.watermedia.api.player.IMediaPlayer; -import me.srrapero720.watermedia.api.player.IVideoPlayer; - -import java.nio.ByteBuffer; - -public class FFmpegVideoPlayer extends FFmpegPlayer implements IVideoPlayer { - @Override - public int width() { - return 0; - } - - @Override - public int height() { - return 0; - } - - @Override - public ByteBuffer textureBuffer() { - return null; - } - - @Override - public int prepare() { - return 0; - } - - @Override - public int texture() { - return 0; - } -} diff --git a/src/main/java/me/srrapero720/watermedia/api/player/impl/VLCPlayer.java b/src/main/java/me/srrapero720/watermedia/api/player/impl/VLCPlayer.java index d7a611af..0e2f1d85 100644 --- a/src/main/java/me/srrapero720/watermedia/api/player/impl/VLCPlayer.java +++ b/src/main/java/me/srrapero720/watermedia/api/player/impl/VLCPlayer.java @@ -1,171 +1,4 @@ package me.srrapero720.watermedia.api.player.impl; -import me.srrapero720.watermedia.api.player.IMediaPlayer; - -public abstract class VLCPlayer implements IMediaPlayer { - - private boolean paused = false; - private boolean muted = false; - private boolean live = false; - private boolean started = false; - - @Override - public boolean start() { - return false; - } - - @Override - public boolean startPaused() { - return false; - } - - @Override - public boolean resume() { - return false; - } - - @Override - public boolean pause() { - return false; - } - - @Override - public boolean pause(boolean paused) { - return false; - } - - @Override - public boolean stop() { - return false; - } - - @Override - public boolean togglePlay() { - return false; - } - - @Override - public boolean seek(long time) { - return false; - } - - @Override - public boolean seekQuick(long time) { - return false; - } - - @Override - public boolean fastFoward() { - return false; - } - - @Override - public boolean fastRewind() { - return false; - } - - @Override - public boolean speed(float speed) { - return false; - } - - @Override - public boolean mute() { - return false; - } - - @Override - public boolean mute(boolean muted) { - return false; - } - - @Override - public boolean unmute() { - return false; - } - - @Override - public boolean repeaat() { - return false; - } - - @Override - public boolean repeaat(boolean repeat) { - return false; - } - - @Override - public boolean usable() { - return false; - } - - @Override - public boolean loading() { - return false; - } - - @Override - public boolean buffering() { - return false; - } - - @Override - public boolean ready() { - return false; - } - - @Override - public boolean paused() { - return false; - } - - @Override - public boolean playing() { - return false; - } - - @Override - public boolean stopped() { - return false; - } - - @Override - public boolean ended() { - return false; - } - - @Override - public boolean muted() { - return false; - } - - @Override - public boolean validSource() { - return false; - } - - @Override - public boolean liveSource() { - return false; - } - - @Override - public boolean canSeek() { - return false; - } - - @Override - public long duration() { - return 0; - } - - @Override - public long time() { - return 0; - } - - @Override - public void release() { - - } +public abstract class VLCPlayer { } diff --git a/src/main/java/me/srrapero720/watermedia/api/player/old/MusicPlayer.java b/src/main/java/me/srrapero720/watermedia/api/player/old/MusicPlayer.java deleted file mode 100644 index 4dd71867..00000000 --- a/src/main/java/me/srrapero720/watermedia/api/player/old/MusicPlayer.java +++ /dev/null @@ -1,15 +0,0 @@ -package me.srrapero720.watermedia.api.player.old; - -import me.srrapero720.watermedia.api.player.PlayerAPI; -import uk.co.caprica.vlcj.factory.MediaPlayerFactory; - -public class MusicPlayer extends VLCPlayer { - - public MusicPlayer(MediaPlayerFactory factory) { - super(factory, null, null); - } - - public MusicPlayer() { - super(PlayerAPI.getFactorySoundOnly(), null, null); - } -} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/api/player/vlc/MusicPlayer.java b/src/main/java/me/srrapero720/watermedia/api/player/vlc/MusicPlayer.java new file mode 100644 index 00000000..95c6c40a --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/player/vlc/MusicPlayer.java @@ -0,0 +1,4 @@ +package me.srrapero720.watermedia.api.player.vlc; + +public class MusicPlayer { +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/api/player/vlc/SimplePlayer.java b/src/main/java/me/srrapero720/watermedia/api/player/vlc/SimplePlayer.java new file mode 100644 index 00000000..d3c75f13 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/player/vlc/SimplePlayer.java @@ -0,0 +1,4 @@ +package me.srrapero720.watermedia.api.player.vlc; + +public class SimplePlayer { +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/api/player/old/VideoPlayer.java b/src/main/java/me/srrapero720/watermedia/api/player/vlc/VideoPlayer.java similarity index 77% rename from src/main/java/me/srrapero720/watermedia/api/player/old/VideoPlayer.java rename to src/main/java/me/srrapero720/watermedia/api/player/vlc/VideoPlayer.java index cd1ee735..79ca2fcb 100644 --- a/src/main/java/me/srrapero720/watermedia/api/player/old/VideoPlayer.java +++ b/src/main/java/me/srrapero720/watermedia/api/player/vlc/VideoPlayer.java @@ -1,30 +1,42 @@ -package me.srrapero720.watermedia.api.player.old; +package me.srrapero720.watermedia.api.player.vlc; import me.srrapero720.watermedia.api.player.PlayerAPI; +import me.srrapero720.watermedia.api.player.SyncBasePlayer; import me.srrapero720.watermedia.api.rendering.RenderAPI; import org.apache.logging.log4j.Marker; import org.apache.logging.log4j.MarkerManager; import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; +import org.lwjgl.opengl.GL15; +import org.lwjgl.opengl.GL21; import uk.co.caprica.vlcj.factory.MediaPlayerFactory; import uk.co.caprica.vlcj.player.embedded.videosurface.callback.BufferFormat; import java.awt.*; +import java.nio.Buffer; import java.nio.ByteBuffer; -import java.nio.ByteOrder; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantLock; -import static org.watermedia.WaterMedia.LOGGER; +import static me.srrapero720.watermedia.WaterMedia.LOGGER; -public class VideoPlayer extends VLCPlayer { - private static final Marker IT = MarkerManager.getMarker("SyncVideoPlayer"); +/** + * WARNING: Experimental + * this class works same as SyncVideoPlayer but uses a PBO + * Use it only to test + contribute to its functionality + */ +public class VideoPlayer extends SyncBasePlayer { + private static final Marker IT = MarkerManager.getMarker("VideoPlayer"); private final int texture; + private final int pbo; private volatile int width = 1; private volatile int height = 1; - private volatile ByteBuffer buffer; + private ByteBuffer buffer; + private ByteBuffer pboBuffer; private volatile Throwable exception; + private final BufferHelper bufferHelper; protected final Executor playerThreadEx; protected final ReentrantLock renderLock = new ReentrantLock(); @@ -49,6 +61,7 @@ public class VideoPlayer extends VLCPlayer { * @deprecated Future replacement is a static method inside {@link PlayerAPI}. * PlayerAPI (including all Players) are intended to be rewrited in 2.1.0 */ + @Deprecated public VideoPlayer(MediaPlayerFactory factory, Executor playerThreadEx) { this(factory, playerThreadEx, DEFAULT_BUFFER_HELPER); } /** @@ -57,6 +70,7 @@ public class VideoPlayer extends VLCPlayer { * @param bufferHelper helper to create IntBuffers * @deprecated mod now integrates an own MemoryTracker with a BufferAlloc */ + @Deprecated public VideoPlayer(Executor playerThreadEx, BufferHelper bufferHelper) { this(null, playerThreadEx, bufferHelper); } /** @@ -64,20 +78,22 @@ public class VideoPlayer extends VLCPlayer { * @param factory custom MediaPlayerFactory instance * @param playerThreadEx executor of render thread for an async task (normally Minecraft.getInstance()) * @param bufferHelper helper to create IntBuffers - * @deprecated mod now integrates an own MemoryTracker with a BufferAlloc, - * it releases the memory properly when usage is done (not bugging GC) + * @deprecated mod now integrates an own MemoryTracker with a BufferAlloc */ - @Deprecated(forRemoval = true) + @Deprecated public VideoPlayer(MediaPlayerFactory factory, Executor playerThreadEx, BufferHelper bufferHelper) { super(); this.playerThreadEx = playerThreadEx; this.texture = GL11.glGenTextures(); + this.pbo = GL15.glGenBuffers(); + this.bufferHelper = bufferHelper; this.init(factory, (mediaPlayer, nativeBuffers, bufferFormat) -> { renderLock.lock(); // we are running in a native thread!! careful try { - // FIXME: this increases allocation rate as HELL. we need to find out the source of the buffer pointers and allocate it directly - if (mediaPlayer.isReleased()) return; - this.buffer = nativeBuffers[0]; + if (buffer == null) return; + ((Buffer) nativeBuffers[0]).rewind(); + buffer.put(nativeBuffers[0]); + ((Buffer) buffer).rewind(); updateFrame.set(true); } catch (Throwable t) { if (exception == null) { @@ -92,7 +108,7 @@ public VideoPlayer(MediaPlayerFactory factory, Executor playerThreadEx, BufferHe try { width = sourceWidth; height = sourceHeight; - buffer = ByteBuffer.allocateDirect(sourceWidth * sourceHeight * 4).order(ByteOrder.nativeOrder()); + buffer = bufferHelper.create(sourceWidth * sourceHeight * 4); updateFrame.set(true); updateFirstFrame.set(true); } catch (Throwable t) { @@ -103,12 +119,15 @@ public VideoPlayer(MediaPlayerFactory factory, Executor playerThreadEx, BufferHe } finally { renderLock.unlock(); } - // TODO: This is wrong; https://wiki.videolan.org/Chroma/ return new BufferFormat("RGBA", sourceWidth, sourceHeight, new int[]{sourceWidth * 4}, new int[]{sourceHeight}); }); if (raw() == null) { GL11.glDeleteTextures(texture); } + + GL15.glBindBuffer(GL21.GL_PIXEL_UNPACK_BUFFER, pbo); + GL15.glBufferData(GL21.GL_PIXEL_UNPACK_BUFFER, this.width * this.height * 4L, GL15.GL_STREAM_DRAW); + GL15.glBindBuffer(GL21.GL_PIXEL_UNPACK_BUFFER, 0); } /** @@ -182,8 +201,24 @@ public void preRender() { if (raw() == null) return; renderLock.lock(); try { - if (updateFrame.compareAndSet(true, false)) - RenderAPI.applyBuffer(buffer, texture, width, height, updateFirstFrame.compareAndSet(true, forceFirstFrame.get())); + // BIND PBO + GL15.glBindBuffer(GL21.GL_PIXEL_UNPACK_BUFFER, pbo); + + // COPY TO PBO + pboBuffer = GL15.glMapBuffer(GL21.GL_PIXEL_UNPACK_BUFFER, GL15.GL_WRITE_ONLY, this.width * this.height * 4L, pboBuffer); + pboBuffer.put(buffer); + GL15.glUnmapBuffer(GL21.GL_PIXEL_UNPACK_BUFFER); + + // PARAMS + GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); + GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, width, height, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, 0); + + // UNBIND PBO + GL21.glBindBuffer(GL21.GL_PIXEL_UNPACK_BUFFER, 0); } finally { renderLock.unlock(); } @@ -249,7 +284,11 @@ public Dimension getMediaDimensions() { */ @Override public void release() { - playerThreadEx.execute(() -> GL11.glDeleteTextures(texture)); + playerThreadEx.execute(() -> { + GL11.glDeleteTextures(texture); + GL21.glDeleteBuffers(pbo); + if (bufferHelper == DEFAULT_BUFFER_HELPER) RenderAPI.freeByteBuffer(buffer); + }); super.release(); buffer = null; } diff --git a/src/main/java/me/srrapero720/watermedia/api/rendering/RenderAPI.java b/src/main/java/me/srrapero720/watermedia/api/rendering/RenderAPI.java new file mode 100644 index 00000000..ce6f97ed --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/rendering/RenderAPI.java @@ -0,0 +1,298 @@ +package me.srrapero720.watermedia.api.rendering; + +import me.srrapero720.watermedia.api.WaterMediaAPI; +import me.srrapero720.watermedia.api.image.ImageRenderer; +import me.srrapero720.watermedia.api.rendering.memory.MemoryAlloc; +import me.srrapero720.watermedia.loader.ILoader; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.MarkerManager; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.IntBuffer; + +/** + * RenderApi is a tool class for OpenGL rendering compatible with all minecraft versions + */ +public class RenderAPI extends WaterMediaAPI { + public static final Marker IT = MarkerManager.getMarker(RenderAPI.class.getSimpleName()); + + /** + * Creates a DirectByteBuffer unsafe using {@link org.lwjgl.system.MemoryUtil.MemoryAllocator MemoryAllocator} + * + *

In case class was missing uses instead {@link java.nio.DirectByteBuffer#allocateDirect(int) DirectByteBuffer#allocateDirect(int)}

+ * @param size size of the buffer + * @return DirectByteBuffer + */ + public static ByteBuffer createByteBuffer(int size) { + return MemoryAlloc.create(size); + } + + /** + * Resizes direct buffer unsafe using {@link org.lwjgl.system.MemoryUtil.MemoryAllocator MemoryAllocator} + * + *

In case class was missing causes a {@link UnsupportedOperationException}

+ * @param buffer buffer to be resized + * @param newSize new size of the buffer + * @return resized DirectByteBuffer + */ + public static ByteBuffer resizeByteBuffer(ByteBuffer buffer, int newSize) { + return MemoryAlloc.resize(buffer, newSize); + } + + /** + * Deletes the direct buffer unsafe using {@link org.lwjgl.system.MemoryUtil.MemoryAllocator MemoryAllocator} + * + *

In case class was missing fallbacks into unsafe cleaner

+ * @param buffer buffer to free + */ + public static void freeByteBuffer(ByteBuffer buffer) { + MemoryAlloc.free(buffer); + } + + public static BufferedImage convertImageFormat(BufferedImage originalImage) { + // If image type is already good then no conversion needed, so we use the original image. + if(originalImage.getType() == BufferedImage.TYPE_INT_ARGB) return originalImage; + + // Convert the image to the expected format. + BufferedImage newImage = new BufferedImage(originalImage.getWidth(), + originalImage.getHeight(), BufferedImage.TYPE_INT_ARGB); + Graphics g = newImage.getGraphics(); + g.drawImage(originalImage, 0, 0, null); + g.dispose(); + return newImage; + } + + public static ByteBuffer[] getRawImageBuffer(BufferedImage[] images) { + ByteBuffer[] buffers = new ByteBuffer[images.length]; + for (int i = 0; i < images.length; i++) { + buffers[i] = getRawImageBuffer(images[i]); + } + return buffers; + } + + /** + * Converts the format and stores the pixels into a ByteBuffer ready to be used by OpenGL + * @param image Image to convert + * @return ByteBuffer of the image + */ + public static ByteBuffer getRawImageBuffer(BufferedImage image) { + image = convertImageFormat(image); + int[] pixels = ((DataBufferInt) convertImageFormat(image).getRaster().getDataBuffer()).getData(); + + ByteBuffer buffer = createByteBuffer(image.getWidth() * image.getHeight() * 4); + buffer.asIntBuffer().put(pixels); + + /* + * FLIP method changes what class type returns in new JAVA versions, in runtime causes a JVM crash by that + */ + ((Buffer) buffer).flip(); + return buffer; + } + + /** + * Creates a new texture id based on a {@link ByteBuffer buffer} + * (used internally by {@link ImageRenderer} + * @param image image to process + * @param width image width + * @param height image height + * @return texture id for OpenGL + */ + public static int uploadBufferTexture(ByteBuffer image, int width, int height) { + int textureID = GL11.glGenTextures(); //Generate texture ID + GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureID); // Bind texture ID + + //Setup wrap mode + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE); + + //Setup texture scaling filtering + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); + + // prevents random crash; when values are too high it causes a jvm crash, caused weird behavior when game is paused + GL11.glPixelStorei(GL11.GL_UNPACK_ROW_LENGTH, GL11.GL_ZERO); + GL11.glPixelStorei(GL11.GL_UNPACK_SKIP_PIXELS, GL11.GL_ZERO); + GL11.glPixelStorei(GL11.GL_UNPACK_SKIP_ROWS, GL11.GL_ZERO); + + //Send texel data to OpenGL + GL11.glTexImage2D(GL11.GL_TEXTURE_2D, GL11.GL_ZERO, GL11.GL_RGBA8, width, height, 0, GL12.GL_BGRA, GL12.GL_UNSIGNED_INT_8_8_8_8_REV, image); + + //Return the texture ID, so we can bind it later again + return textureID; + } + + /** + * Creates a new texture id based on a {@link IntBuffer buffer} + * (used internally by {@link ImageRenderer} + * @param image image to process + * @param width image width + * @param height image height + * @return texture id for OpenGL + */ + public static int uploadBufferTexture(IntBuffer image, int width, int height) { + int textureID = GL11.glGenTextures(); //Generate texture ID + GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureID); // Bind texture ID + + //Setup wrap mode + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE); + + //Setup texture scaling filtering + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); + + // prevents random crash; when values are too high it causes a jvm crash, caused weird behavior when game is paused + GL11.glPixelStorei(GL11.GL_UNPACK_ROW_LENGTH, GL11.GL_ZERO); + GL11.glPixelStorei(GL11.GL_UNPACK_SKIP_PIXELS, GL11.GL_ZERO); + GL11.glPixelStorei(GL11.GL_UNPACK_SKIP_ROWS, GL11.GL_ZERO); + + //Send texel data to OpenGL + GL11.glTexImage2D(GL11.GL_TEXTURE_2D, GL11.GL_ZERO, GL11.GL_RGBA8, width, height, 0, GL12.GL_BGRA, GL12.GL_UNSIGNED_INT_8_8_8_8_REV, image); + + //Return the texture ID, so we can bind it later again + return textureID; + } + + + /** + * Creates a new texture id based on a {@link BufferedImage} instance + * (used internally by {@link ImageRenderer} + * @param image image to process + * @param width buffer width (can be image width) + * @param height buffer height (can be image height) + * @return texture id for OpenGL + * @deprecated use instead {@link RenderAPI#getRawImageBuffer(BufferedImage)} and {@link RenderAPI#uploadBufferTexture(ByteBuffer, int, int)} + */ + @Deprecated(forRemoval = true) + public static int applyBuffer(BufferedImage image, int width, int height) { + image = convertImageFormat(image); + int[] pixels = ((DataBufferInt) convertImageFormat(image).getRaster().getDataBuffer()).getData(); + + ByteBuffer buffer = createByteBuffer(width * height * 4); + buffer.asIntBuffer().put(pixels); + + /* + * FLIP method changes what class type returns in new JAVA versions, in runtime causes a JVM crash by that + */ + ((Buffer) buffer).flip(); + + int textureID = GL11.glGenTextures(); //Generate texture ID + GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureID); // Bind texture ID + + //Setup wrap mode + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE); + + //Setup texture scaling filtering + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); + + // prevents random crash; when values are too high it causes a jvm crash, caused weird behavior when game is paused + GL11.glPixelStorei(GL11.GL_UNPACK_ROW_LENGTH, GL11.GL_ZERO); + GL11.glPixelStorei(GL11.GL_UNPACK_SKIP_PIXELS, GL11.GL_ZERO); + GL11.glPixelStorei(GL11.GL_UNPACK_SKIP_ROWS, GL11.GL_ZERO); + + //Send texel data to OpenGL + GL11.glTexImage2D(GL11.GL_TEXTURE_2D, GL11.GL_ZERO, GL11.GL_RGBA8, width, height, 0, GL12.GL_BGRA, GL12.GL_UNSIGNED_INT_8_8_8_8_REV, buffer); + + //Return the texture ID, so we can bind it later again + return textureID; + } + + /** + * Process a buffer to be used in a OpenGL texture id + * @param videoBuffer IntBuffer to be processed + * @param glTexture texture ID from OpenGL + * @param videoWidth buffer width + * @param videoHeight buffer height + * @param firstFrame if was the first frame + * @deprecated use instead {@link RenderAPI#getRawImageBuffer(BufferedImage)} and {@link RenderAPI#uploadBufferTexture(IntBuffer, int, int)} + */ + @Deprecated(forRemoval = true) + public static void applyBuffer(IntBuffer videoBuffer, int glTexture, int videoWidth, int videoHeight, boolean firstFrame) { + GL11.glBindTexture(GL11.GL_TEXTURE_2D, glTexture); + + //Setup wrap mode + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE); + + //Setup texture scaling filtering + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); + + GL11.glPixelStorei(GL11.GL_UNPACK_ROW_LENGTH, GL11.GL_ZERO); + GL11.glPixelStorei(GL11.GL_UNPACK_SKIP_PIXELS, GL11.GL_ZERO); + GL11.glPixelStorei(GL11.GL_UNPACK_SKIP_ROWS, GL11.GL_ZERO); + + if (firstFrame) {GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA, videoWidth, videoHeight, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, videoBuffer); + } else GL11.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, videoWidth, videoHeight, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, videoBuffer); + } + + /** + * Process a buffer to be used in a OpenGL texture id + * @param videoBuffer ByteBuffer to be processed + * @param glTexture texture ID from OpenGL + * @param videoWidth buffer width + * @param videoHeight buffer height + * @param firstFrame if was the first frame + */ + public static void applyBuffer(ByteBuffer videoBuffer, int glTexture, int videoWidth, int videoHeight, boolean firstFrame) { + GL11.glBindTexture(GL11.GL_TEXTURE_2D, glTexture); + + //Setup wrap mode + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE); + + //Setup texture scaling filtering + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); + + GL11.glPixelStorei(GL11.GL_UNPACK_ROW_LENGTH, GL11.GL_ZERO); + GL11.glPixelStorei(GL11.GL_UNPACK_SKIP_PIXELS, GL11.GL_ZERO); + GL11.glPixelStorei(GL11.GL_UNPACK_SKIP_ROWS, GL11.GL_ZERO); + + if (firstFrame) {GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA, videoWidth, videoHeight, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, videoBuffer); + } else GL11.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, videoWidth, videoHeight, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, videoBuffer); + } + + public static ByteBuffer getTextureBuffer(int textureId, int width, int height) { + ByteBuffer buffer = createByteBuffer(width * height * 4); + GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureId); + GL11.glGetTexImage(textureId, 0, GL12.GL_BGRA, GL12.GL_UNSIGNED_INT_8_8_8_8_REV, buffer); + return buffer; + } + + public static void deleteTexture(int texture) { + GL11.glDeleteTextures(texture); + } + + public static void deleteTexture(int[] textures) { + GL11.glDeleteTextures(textures); + } + + @Override + public Priority priority() { + return Priority.LOW; + } + + @Override + public boolean prepare(ILoader bootCore) throws Exception { + return true; + } + + @Override + public void start(ILoader bootCore) throws Exception { + + } + + @Override + public void release() { + + } +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/api/rendering/manager/RenderManager.java b/src/main/java/me/srrapero720/watermedia/api/rendering/manager/RenderManager.java new file mode 100644 index 00000000..e3e43f5d --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/rendering/manager/RenderManager.java @@ -0,0 +1,5 @@ +package me.srrapero720.watermedia.api.rendering.manager; + +class RenderManager { + +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/api/rendering/manager/RenderState.java b/src/main/java/me/srrapero720/watermedia/api/rendering/manager/RenderState.java new file mode 100644 index 00000000..e2064cec --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/rendering/manager/RenderState.java @@ -0,0 +1,4 @@ +package me.srrapero720.watermedia.api.rendering.manager; + +public class RenderState { +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/api/rendering/memory/MemoryAlloc.java b/src/main/java/me/srrapero720/watermedia/api/rendering/memory/MemoryAlloc.java new file mode 100644 index 00000000..2e737cb5 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/api/rendering/memory/MemoryAlloc.java @@ -0,0 +1,38 @@ +package me.srrapero720.watermedia.api.rendering.memory; + +import org.lwjgl.system.MemoryUtil; + +import java.nio.ByteBuffer; + +public class MemoryAlloc { + private static MemoryUtil.MemoryAllocator ALLOCATOR; + + public static ByteBuffer create(int pSize) { + if (ALLOCATOR == null) ALLOCATOR = MemoryUtil.getAllocator(false); + + long i = ALLOCATOR.malloc(pSize); + if (i == 0L) { + throw new OutOfMemoryError("Failed to allocate " + pSize + " bytes"); + } else { + return MemoryUtil.memByteBuffer(i, pSize); + } + } + + public static ByteBuffer resize(ByteBuffer pBuffer, int pByteSize) { + if (ALLOCATOR == null) ALLOCATOR = MemoryUtil.getAllocator(false); + + long i = ALLOCATOR.realloc(MemoryUtil.memAddress0(pBuffer), pByteSize); + if (i == 0L) { + throw new OutOfMemoryError("Failed to resize buffer from " + pBuffer.capacity() + " bytes to " + pByteSize + " bytes"); + } else { + return MemoryUtil.memByteBuffer(i, pByteSize); + } + } + + public static void free(ByteBuffer pBuffer) { + if (pBuffer == null) return; + if (ALLOCATOR == null) ALLOCATOR = MemoryUtil.getAllocator(false); + + ALLOCATOR.free(MemoryUtil.memAddress0(pBuffer)); + } +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/core/WaterInternalAPI.java b/src/main/java/me/srrapero720/watermedia/core/WaterInternalAPI.java index eadd309a..0789dab1 100644 --- a/src/main/java/me/srrapero720/watermedia/core/WaterInternalAPI.java +++ b/src/main/java/me/srrapero720/watermedia/core/WaterInternalAPI.java @@ -1,6 +1,6 @@ package me.srrapero720.watermedia.core; -import org.watermedia.api.WaterMediaAPI; +import me.srrapero720.watermedia.api.WaterMediaAPI; public abstract class WaterInternalAPI extends WaterMediaAPI { } \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/core/cache/CacheCore.java b/src/main/java/me/srrapero720/watermedia/core/cache/CacheCore.java deleted file mode 100644 index 52046360..00000000 --- a/src/main/java/me/srrapero720/watermedia/core/cache/CacheCore.java +++ /dev/null @@ -1,161 +0,0 @@ -package me.srrapero720.watermedia.core.cache; - -import me.srrapero720.watermedia.core.WaterInternalAPI; -import me.srrapero720.watermedia.core.config.WaterConfig; -import org.watermedia.WaterMedia; -import org.watermedia.tools.DataTool; -import org.watermedia.tools.ArgTool; -import org.apache.logging.log4j.Marker; -import org.apache.logging.log4j.MarkerManager; - -import java.io.*; -import java.nio.file.Files; -import java.util.Base64; -import java.util.HashMap; -import java.util.Map; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; - -import static org.watermedia.WaterMedia.LOGGER; - -public class CacheCore extends WaterInternalAPI { - private static final Marker IT = MarkerManager.getMarker(CacheCore.class.getSimpleName()); - private static final Map ENTRIES = new HashMap<>(); - - private static File dir; - private static File index; - - @Override - public Priority priority() { - return Priority.HIGHEST; - } - - @Override - public boolean prepare(WaterMedia.ILoader bootCore) throws Exception { - dir = new File(WaterConfig.vlcInstallPath, "cache/pictures"); - index = new File(dir, "index"); - return true; - } - - @Override - public void start(WaterMedia.ILoader bootCore) throws Exception { - if (!dir.exists() && !dir.mkdirs()) - throw new IOException("Failed to create cache directories"); - - if (index.exists()) { - try (DataInputStream in = new DataInputStream(new GZIPInputStream(Files.newInputStream(index.toPath())))) { - while (in.available() != 0) { - Entry entry = Entry.read(in); - ENTRIES.put(entry.url, entry); - } - } - } - } - - @Override - public void release() { - try (DataOutputStream out = new DataOutputStream(new GZIPOutputStream(Files.newOutputStream(index.toPath())))) { - for (Entry entry: ENTRIES.values()) { - entry.write(out); - } - } catch (Exception e) { - LOGGER.error(IT, "Failed to save cache indexes", e); - } - } - - public static Entry get(String url) { - return ENTRIES.get(url); - } - - public static Entry create(String url, String tag, String mimetype, long timestamp, long expiration) { - return ENTRIES.put(url, new Entry(url, tag, mimetype, timestamp, expiration)); - } - - public enum Type { - IMAGE, AUDIO, VIDEO; - } - - public static final class Entry { - public final File file; - public final String url; - public final String tag; - public final Type type; - public final String extension; - public final long requestTime; - public final long expiration; - - public Entry(String url, String tag, String mimetype, long requestTime, long expiration) { - this(url, tag, getTypeExtension(mimetype), requestTime, expiration); - } - - private Entry(String url, String tag, ArgTool typeExtension, long requestTime, long expiration) { - this.url = url; - this.file = entry$genFile(this.url); - this.tag = tag; - this.type = typeExtension.key(); - this.extension = typeExtension.value(); - this.requestTime = requestTime; - this.expiration = expiration; - } - - // TODO: MADE BETTER MIMETYPE VALIDATION USING REGEX - private static ArgTool getTypeExtension(String mimetype) { - if (mimetype == null) - throw new IllegalArgumentException("mimetype is null"); - - String[] typeExtension = mimetype.split("/"); - if (typeExtension.length == 0) { - throw new IllegalArgumentException("Mimetype is empty"); - } else { - return new ArgTool<>(Type.valueOf(typeExtension[0]), typeExtension.length == 1 ? "" : typeExtension[1]); - } - } - - private void write(DataOutputStream out) throws IOException { - out.writeUTF(this.url); - out.writeUTF(this.tag == null ? "null" : this.tag); - out.writeUTF(this.type.name()); - out.writeUTF(this.extension); - out.writeLong(this.requestTime); - out.writeLong(this.expiration); - } - - public void storeFile(byte[] bytes) throws IOException { - try (OutputStream out = Files.newOutputStream(file.toPath())) { - out.write(bytes); - } - } - - public void storeFile(InputStream inputStream) throws IOException { - try (InputStream in = inputStream; OutputStream out = Files.newOutputStream(file.toPath())) { - int readed; - byte[] data = new byte[1024 * 4]; - while ((readed = in.read(data)) != -1) { - out.write(data, 0, readed); - } - } - } - - public void refresh(String url, String tag, String mimetype, long requestTime, long expiration) { - ENTRIES.put(url, new Entry(url, tag, mimetype, requestTime, expiration)); - } - - public InputStream getInputStream() throws IOException { - return Files.newInputStream(file.toPath()); - } - - private static Entry read(DataInputStream in) throws IOException { - String url = in.readUTF(); - String tag = in.readUTF(); - ArgTool typeExtension = new ArgTool<>(Type.valueOf(in.readUTF()), in.readUTF()); - long time = in.readLong(); - long expireTime = in.readLong(); - return new Entry(url, !tag.isEmpty() ? tag : null, typeExtension, time, expireTime); - } - - private static File entry$genFile(String url) { - String n = DataTool.encodeHex(url); - return new File(dir, n != null ? n : Base64.getEncoder().encodeToString(url.getBytes())); - } - } -} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/core/compress/CompressCore.java b/src/main/java/me/srrapero720/watermedia/core/compress/CompressCore.java index ec5daf96..338bdec0 100644 --- a/src/main/java/me/srrapero720/watermedia/core/compress/CompressCore.java +++ b/src/main/java/me/srrapero720/watermedia/core/compress/CompressCore.java @@ -1,8 +1,8 @@ package me.srrapero720.watermedia.core.compress; import me.srrapero720.watermedia.core.WaterInternalAPI; -import org.watermedia.WaterMedia; -import org.watermedia.tools.ThreadTool; +import me.srrapero720.watermedia.loader.ILoader; +import me.srrapero720.watermedia.tools.ThreadTool; import net.sf.sevenzipjbinding.SevenZip; import java.util.concurrent.ExecutorService; @@ -16,7 +16,7 @@ public Priority priority() { } @Override - public boolean prepare(WaterMedia.ILoader bootCore) throws Exception { + public boolean prepare(ILoader bootCore) throws Exception { SevenZip.initSevenZipFromPlatformJAR(); if (EX == null || EX.isTerminated()) { EX = null; @@ -25,7 +25,7 @@ public boolean prepare(WaterMedia.ILoader bootCore) throws Exception { } @Override - public void start(WaterMedia.ILoader bootCore) throws Exception { EX = ThreadTool.executorReduced("decompressor"); } + public void start(ILoader bootCore) throws Exception { EX = ThreadTool.executorReduced("decompressor"); } @Override public void release() { EX.shutdown(); } diff --git a/src/main/java/me/srrapero720/watermedia/core/config/ConfigCore.java b/src/main/java/me/srrapero720/watermedia/core/config/ConfigCore.java index fc8c3521..6f68f349 100644 --- a/src/main/java/me/srrapero720/watermedia/core/config/ConfigCore.java +++ b/src/main/java/me/srrapero720/watermedia/core/config/ConfigCore.java @@ -1,10 +1,11 @@ package me.srrapero720.watermedia.core.config; -import org.watermedia.WaterMedia; +import me.srrapero720.watermedia.WaterMedia; import me.srrapero720.watermedia.core.WaterInternalAPI; import me.srrapero720.watermedia.core.config.values.ConfigField; import me.srrapero720.watermedia.core.config.values.RangeOf; import me.srrapero720.watermedia.core.config.values.WaterConfigFile; +import me.srrapero720.watermedia.loader.ILoader; import java.io.*; import java.lang.reflect.Field; @@ -131,12 +132,12 @@ public Priority priority() { } @Override - public boolean prepare(WaterMedia.ILoader bootCore) throws Exception { + public boolean prepare(ILoader bootCore) throws Exception { return true; } @Override - public void start(WaterMedia.ILoader bootCore) throws Exception { + public void start(ILoader bootCore) throws Exception { loadConfiguration(bootCore.cwd().resolve("config/watermedia.wt"), config); } diff --git a/src/main/java/me/srrapero720/watermedia/core/config/WaterConfig.java b/src/main/java/me/srrapero720/watermedia/core/config/WaterConfig.java index ac240012..ed8712a0 100644 --- a/src/main/java/me/srrapero720/watermedia/core/config/WaterConfig.java +++ b/src/main/java/me/srrapero720/watermedia/core/config/WaterConfig.java @@ -1,7 +1,7 @@ package me.srrapero720.watermedia.core.config; -import org.watermedia.WaterMedia; +import me.srrapero720.watermedia.WaterMedia; import me.srrapero720.watermedia.core.config.values.ConfigField; import me.srrapero720.watermedia.core.config.values.WaterConfigFile; diff --git a/src/main/java/me/srrapero720/watermedia/core/config/support/CustomDirectoryProvider.java b/src/main/java/me/srrapero720/watermedia/core/config/support/CustomDirectoryProvider.java index cc0d59c9..ce097892 100644 --- a/src/main/java/me/srrapero720/watermedia/core/config/support/CustomDirectoryProvider.java +++ b/src/main/java/me/srrapero720/watermedia/core/config/support/CustomDirectoryProvider.java @@ -1,6 +1,6 @@ package me.srrapero720.watermedia.core.config.support; -import org.watermedia.WaterMedia; +import me.srrapero720.watermedia.WaterMedia; import me.srrapero720.watermedia.core.config.WaterConfig; import uk.co.caprica.vlcj.discovery.ProviderPriority; import uk.co.caprica.vlcj.discovery.providers.DiscoveryPathProvider; diff --git a/src/main/java/me/srrapero720/watermedia/loader/ILoader.java b/src/main/java/me/srrapero720/watermedia/loader/ILoader.java new file mode 100644 index 00000000..97ee8299 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/loader/ILoader.java @@ -0,0 +1,49 @@ +package me.srrapero720.watermedia.loader; + +import java.io.File; +import java.nio.file.Path; +import me.srrapero720.watermedia.WaterMedia; + +/** + * Custom impl for {@link WaterMedia} loaders + * by default contains impl for Minecraft modloaders like MinecraftForge, NeoForge and Fabric + */ +public interface ILoader { + Path TMP_DEFAULT = new File(System.getProperty("java.io.tmpdir")).toPath().toAbsolutePath().resolve("watermedia"); + Path CWD_DEFAULT = new File("run").toPath().toAbsolutePath(); + + /** + * Name of the loader, preferable the bootstrap name + * @return loader name + */ + String name(); + + /** + * Tmp path is normally java.io.tmp directory; you can set up your custom tmp dir, + * @return Path instance to tmp directory + */ + Path tmp(); + + /** + * Absolute path to current working directory + * @return Current working directory + */ + Path cwd(); + + /** + * Check if the current environment is client side + * @return if was client-side + */ + boolean client(); + + /** + * Default {@link ILoader} instance to be used in non-minecraft instances + * @see ILoader + */ + ILoader DEFAULT = new ILoader() { + @Override public String name() { return "Default"; } + @Override public Path tmp() { return TMP_DEFAULT; } + @Override public Path cwd() { return CWD_DEFAULT; } + @Override public boolean client() { return true; } + }; +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/loader/IModule.java b/src/main/java/me/srrapero720/watermedia/loader/IModule.java new file mode 100644 index 00000000..1aa591b4 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/loader/IModule.java @@ -0,0 +1,19 @@ +package me.srrapero720.watermedia.loader; + +/** + * New jar module attached to WATERMeDIA + */ +public interface IModule { + /** + * Provides the module name to identify it internally on crash reports + * @return module name + */ + String name(); + + /** + * Provides the classloader of the module (in case environment do odd things on jar modules) + * @return module classloader + */ + ClassLoader classLoader(); + +} diff --git a/src/main/java/me/srrapero720/watermedia/loader/McFabricLoader.java b/src/main/java/me/srrapero720/watermedia/loader/McFabricLoader.java new file mode 100644 index 00000000..9df6a09a --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/loader/McFabricLoader.java @@ -0,0 +1,27 @@ +package me.srrapero720.watermedia.loader; + +import me.srrapero720.watermedia.WaterMedia; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.api.EnvType; +import net.fabricmc.loader.api.FabricLoader; + +import java.nio.file.Path; + +public class McFabricLoader implements ClientModInitializer, ILoader { + private static final Path CWD = FabricLoader.getInstance().getGameDir(); + + @Override + public void onInitializeClient() { + try { + WaterMedia.prepare(this).start(); + } catch (Exception e) { + throw new RuntimeException("Failed starting " + WaterMedia.NAME + " for " + name() + ": " + e.getMessage(), e); + } + } + + @Override public String name() { return "Fabric"; } + @Override public Path cwd() { return CWD; } + @Override public Path tmp() { return ILoader.TMP_DEFAULT; } + + @Override public boolean client() { return FabricLoader.getInstance().getEnvironmentType().equals(EnvType.CLIENT); } +} diff --git a/src/main/java/org/watermedia/loader/McForgeLoader.java b/src/main/java/me/srrapero720/watermedia/loader/McForgeLoader.java similarity index 71% rename from src/main/java/org/watermedia/loader/McForgeLoader.java rename to src/main/java/me/srrapero720/watermedia/loader/McForgeLoader.java index b4c63a6f..b04c62f3 100644 --- a/src/main/java/org/watermedia/loader/McForgeLoader.java +++ b/src/main/java/me/srrapero720/watermedia/loader/McForgeLoader.java @@ -1,6 +1,6 @@ -package org.watermedia.loader; +package me.srrapero720.watermedia.loader; -import org.watermedia.WaterMedia; +import me.srrapero720.watermedia.WaterMedia; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.loading.FMLLoader; import org.apache.logging.log4j.Marker; @@ -9,7 +9,7 @@ import java.nio.file.Path; @Mod(WaterMedia.ID) -public class McForgeLoader implements WaterMedia.ILoader { +public class McForgeLoader implements ILoader { private static final Marker IT = MarkerManager.getMarker("ForgeLoader"); public McForgeLoader() { @@ -21,7 +21,7 @@ public McForgeLoader() { } @Override public String name() { return "Forge"; } - @Override public Path tmp() { return WaterMedia.DEFAULT_LOADER.tmp(); } - @Override public Path cwd() { return WaterMedia.DEFAULT_LOADER.cwd(); } + @Override public Path tmp() { return ILoader.TMP_DEFAULT; } + @Override public Path cwd() { return ILoader.CWD_DEFAULT; } @Override public boolean client() { return FMLLoader.getDist().isClient(); } } diff --git a/src/main/java/org/watermedia/loader/McNeoForgeLoader.java b/src/main/java/me/srrapero720/watermedia/loader/McNeoForgeLoader.java similarity index 74% rename from src/main/java/org/watermedia/loader/McNeoForgeLoader.java rename to src/main/java/me/srrapero720/watermedia/loader/McNeoForgeLoader.java index d960adbb..baf1a089 100644 --- a/src/main/java/org/watermedia/loader/McNeoForgeLoader.java +++ b/src/main/java/me/srrapero720/watermedia/loader/McNeoForgeLoader.java @@ -1,13 +1,13 @@ -package org.watermedia.loader; +package me.srrapero720.watermedia.loader; -import org.watermedia.WaterMedia; +import me.srrapero720.watermedia.WaterMedia; import net.neoforged.fml.common.Mod; import net.neoforged.fml.loading.FMLLoader; import java.nio.file.Path; @Mod(WaterMedia.ID) -public class McNeoForgeLoader implements WaterMedia.ILoader { +public class McNeoForgeLoader implements ILoader { public McNeoForgeLoader() { try { WaterMedia.prepare(this).start(); @@ -17,7 +17,7 @@ public McNeoForgeLoader() { } @Override public String name() { return "NeoForge"; } - @Override public Path tmp() { return WaterMedia.DEFAULT_LOADER.tmp(); } + @Override public Path tmp() { return TMP_DEFAULT; } @Override public Path cwd() { return FMLLoader.getGamePath(); } @Override public boolean client() { return FMLLoader.getDist().isClient(); } } diff --git a/src/main/java/org/watermedia/tools/DataTool.java b/src/main/java/me/srrapero720/watermedia/tools/DataTool.java similarity index 54% rename from src/main/java/org/watermedia/tools/DataTool.java rename to src/main/java/me/srrapero720/watermedia/tools/DataTool.java index 95d49657..b7d0727d 100644 --- a/src/main/java/org/watermedia/tools/DataTool.java +++ b/src/main/java/me/srrapero720/watermedia/tools/DataTool.java @@ -1,36 +1,20 @@ -package org.watermedia.tools; - -import com.google.gson.Gson; +package me.srrapero720.watermedia.tools; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.lang.reflect.Array; -import java.lang.reflect.Type; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.function.Function; - -import static org.watermedia.WaterMedia.LOGGER; +import java.util.ServiceLoader; public class DataTool { - public static final Gson GSON = new Gson(); + public static final String USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 Edg/112.0.1722.68"; private static final int DEFAULT_BUFFER_SIZE = 8192; private static final int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8; private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); - public static long orElse(String s, int o) { - try { - return Integer.parseInt(s); - } catch (NumberFormatException e) { - return o; - } - } - - public static long orElse(String s, long o) { + public static long parseLongOr(String s, long o) { try { return Long.parseLong(s); } catch (NumberFormatException e) { @@ -38,58 +22,27 @@ public static long orElse(String s, long o) { } } - public static String orElse(String s, String s1) { - return s == null ? s1 : s; - } - - public static T fromJSON(String s, Type t) { - return GSON.fromJson(s, t); - } - - public static T fromJSON(InputStreamReader s, Type t) { - return GSON.fromJson(s, t); - } - - public static int[] filter(int[] its, int v) { + public static int[] filterValue(int[] its, int v) { int size = 0; - for (int i: its) if (i != v) size++; + for (int i : its) if (i != v) size++; int[] result = new int[size]; int pos = 0; - for (int i: its) + for (int i : its) if (i != v) result[pos++] = i; return result; } - @SuppressWarnings("all") - public static T[] concat(T[] array, T... values) { + public static T[] concatArray(T[] array, T... values) { Object t = Array.newInstance(array.getClass().getComponentType(), array.length + values.length); System.arraycopy(array, 0, t, 0, array.length); System.arraycopy(values, 0, t, array.length, values.length); return (T[]) t; } - public static int[] unbox(List arr) { - int[] result = new int[arr.size()]; - for (int i = 0; i < arr.size(); i++) { - result[i] = arr.get(i); - } - return result; - } - - public static int[] unbox(Integer[] arr) { - int[] result = new int[arr.length]; - int i = 0; - while (i < arr.length) { - result[i] = arr[i]; - i++; - } - return result; - } - - public static List toList(Iterable s) { + public static List toList(ServiceLoader s) { List r = new ArrayList<>(); for (T t: s) r.add(t); return r; @@ -97,6 +50,9 @@ public static List toList(Iterable s) { public static byte[] readAllBytes(InputStream stream) throws IOException { int len = Integer.MAX_VALUE; + if (len < 0) { + throw new IllegalArgumentException("len < 0"); + } List bufs = null; byte[] result = null; @@ -157,19 +113,7 @@ public static byte[] readAllBytes(InputStream stream) throws IOException { return result; } - public static String encodeHex(String string) { - try { - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - byte[] bytes = digest.digest(string.getBytes(StandardCharsets.UTF_8)); - - return encodeHex(bytes); - } catch (Exception e) { - LOGGER.error("Failed to digest and encode string {}", string, e); - return null; - } - } - - public static String encodeHex(byte[] bytes) { + public static String encodeHexString(byte[] bytes) { char[] hexChars = new char[bytes.length * 2]; for (int j = 0; j < bytes.length; j++) { int v = bytes[j] & 0xFF; @@ -178,29 +122,4 @@ public static String encodeHex(byte[] bytes) { } return new String(hexChars); } - - public static ArgTool getArgument(String argument) { - return new ArgTool(argument); - } - - // I hate this bullshit with my life - @SuppressWarnings("unchecked") - public static V[] getValueFrom(T[] arr, Function how2) { - V[] result = (V[]) new Object[arr.length]; - for (int i = 0; i < arr.length; i++) { - var thing = arr[i]; - result[i] = how2.apply(thing); - } - return result; - } - - // I hate this bullshit with my life - public static int[] getIntValueFrom(T[] arr, Function how2) { - int[] result = new int[arr.length]; - for (int i = 0; i < arr.length; i++) { - var thing = arr[i]; - result[i] = how2.apply(thing); - } - return result; - } } \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/tools/IOTool.java b/src/main/java/me/srrapero720/watermedia/tools/IOTool.java new file mode 100644 index 00000000..9e5a1cb4 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/tools/IOTool.java @@ -0,0 +1,124 @@ +package me.srrapero720.watermedia.tools; + +import me.srrapero720.watermedia.api.image.decoders.GifDecoder; +import net.sf.sevenzipjbinding.*; +import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream; +import net.sf.sevenzipjbinding.simple.ISimpleInArchive; +import net.sf.sevenzipjbinding.simple.ISimpleInArchiveItem; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.MarkerManager; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import static me.srrapero720.watermedia.WaterMedia.LOGGER; + +public class IOTool { + private static final Marker IT = MarkerManager.getMarker("Tools"); + + public static String readString(Path from) { + try { + byte[] bytes = Files.readAllBytes(from); + return new String(bytes, StandardCharsets.UTF_8); + } catch (Exception e) { + return null; + } + } + + public static GifDecoder readGif(Path path) { + try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(path.toFile().toPath()))) { + GifDecoder gif = new GifDecoder(); + int status = gif.read(in); + if (status == GifDecoder.STATUS_OK) return gif; + + throw new IOException("Failed to process GIF - Decoder status: " + status); + } catch (Exception e) { + LOGGER.error(IT, "Failed loading GIF from WaterMedia resources", e); + } + return null; + } + + public static boolean rmdirs(Path path) { + return rmdirs(path.toFile()); + } + + public static boolean rmdirs(File root) { + File[] files = root.listFiles(); + + if (files == null || files.length == 0) return root.delete(); + for (File f: files) { + File[] childs = f.listFiles(); + if (childs != null && childs.length != 0 && !rmdirs(f)) return false; + if (!f.delete()) return false; + } + return true; + } + + public static void un7zip(Marker it, Path zipPath) throws IOException { un7zip(it, zipPath, zipPath.getParent()); } + public static void un7zip(Marker it, Path zipPath, Path destPath) throws IOException { + try (RandomAccessFile file = new RandomAccessFile(zipPath.toFile(), "r"); + IInArchive archive = SevenZip.openInArchive(null, new RandomAccessFileInStream(file))) { + + ISimpleInArchive simpleInArchive = archive.getSimpleInterface(); + for (ISimpleInArchiveItem i: simpleInArchive.getArchiveItems()) { + Path destination = destPath.resolve(i.getPath()); + if (i.isFolder()) { + if (!destination.toFile().mkdirs()) { + LOGGER.error(it, "Failed to create directory '{}'", destination.toAbsolutePath().toString()); + } + continue; + } + ExtractOperationResult result; + + result = i.extractSlow(data -> { +// BufferedOutputStream output = new BufferedOutputStream(Files.newOutputStream(destination)); + return data.length; // Return amount of consumed data + }); + + if (result != ExtractOperationResult.OK) { + throw new IOException("Failed to extract file '"+ destination.toAbsolutePath() + "', status code: " + result.name()); + } + } + } + + } + + public static void unzip(Marker it, Path zipFilePath) throws IOException { unzip(it, zipFilePath, zipFilePath.getParent()); } + public static void unzip(Marker it, Path zipFilePath, Path destDirectory) throws IOException { + LOGGER.debug(it, "Unzipping file from '{}' to directory '{}'", zipFilePath, destDirectory); + if (zipFilePath.toString().endsWith(".7z")) throw new IOException("Attempted to extract a 7z as a .zip"); + File destDir = destDirectory.toFile(); + if (!destDir.exists()) destDir.mkdirs(); + + try (ZipInputStream zipIn = new ZipInputStream(Files.newInputStream(zipFilePath))) { + ZipEntry entry = zipIn.getNextEntry(); + // iterates over entries in the zip file + while (entry != null) { + String filePath = destDirectory + File.separator + entry.getName(); + if (!entry.isDirectory()) { + // if the entry is a file, extracts it + unzip$extract(zipIn, filePath); + } else { + // if the entry is a directory, make the directory + File dir = new File(filePath); + dir.mkdirs(); + } + zipIn.closeEntry(); + entry = zipIn.getNextEntry(); + } + } + } + + private static void unzip$extract(ZipInputStream zipIn, String filePath) throws IOException { + try (BufferedOutputStream output = new BufferedOutputStream(Files.newOutputStream(Paths.get(filePath)))) { + byte[] bytesIn = new byte[4096]; + int read; + while ((read = zipIn.read(bytesIn)) != -1) output.write(bytesIn, 0, read); + } + } +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/tools/JarTool.java b/src/main/java/me/srrapero720/watermedia/tools/JarTool.java new file mode 100644 index 00000000..613a9a9c --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/tools/JarTool.java @@ -0,0 +1,185 @@ +package me.srrapero720.watermedia.tools; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import me.srrapero720.watermedia.api.image.decoders.GifDecoder; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.MarkerManager; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.*; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static me.srrapero720.watermedia.WaterMedia.LOGGER; + +public class JarTool { + static final Marker IT = MarkerManager.getMarker("Tools"); + private static final Gson GSON = new Gson(); + + @Deprecated + public static String readString(ClassLoader loader, String source) { + try { + byte[] bytes = DataTool.readAllBytes(readResourceAsStream$byClassLoader(source, loader)); + return new String(bytes, Charset.defaultCharset()); + } catch (Exception e) { + return null; + } + } + + @Deprecated + public static boolean copyAsset(ClassLoader loader, String source, Path dest) { + try (InputStream is = readResourceAsStream$byClassLoader(source, loader)) { + if (is == null) throw new FileNotFoundException("Resource was not found in " + source); + + File destParent = dest.getParent().toFile(); + if (!destParent.exists() && !destParent.mkdirs()) LOGGER.fatal(IT, "Cannot be created parent directories to {}", dest.toString()); + Files.copy(is, dest, StandardCopyOption.REPLACE_EXISTING); + return true; + } catch (Exception e) { + LOGGER.fatal(IT, "Failed to extract from (JAR) {} to {} due to unexpected error", source, dest, e); + } + return false; + } + + @Deprecated + public static List readStringList(ClassLoader loader, String source) { + List result = new ArrayList<>(); + try (InputStreamReader reader = new InputStreamReader(readResourceAsStream$byClassLoader(source, loader))) { + result.addAll(GSON.fromJson(reader, new TypeToken>() {}.getType())); + } catch (Exception e) { + LOGGER.fatal(IT, "Exception trying to read JSON from {}", source, e); + } + + return result; + } + + @Deprecated + public static BufferedImage readImage(ClassLoader loader, String path) { + try (InputStream in = readResourceAsStream$byClassLoader(path, loader)) { + BufferedImage image = ImageIO.read(in); + if (image != null) return image; + else throw new FileNotFoundException("result of BufferedImage was null"); + } catch (Exception e) { + throw new IllegalStateException("Failed loading BufferedImage from resources", e); + } + } + + @Deprecated + public static GifDecoder readGif(ClassLoader loader, String path) { + try (BufferedInputStream in = new BufferedInputStream(readResourceAsStream$byClassLoader(path, loader))) { + GifDecoder gif = new GifDecoder(); + int status = gif.read(in); + if (status == GifDecoder.STATUS_OK) return gif; + + throw new IOException("Failed to process GIF - Decoder status: " + status); + } catch (Exception e) { + throw new IllegalStateException("Failed loading GIF from resources", e); + } + } + + // WITHOUT CLASSLOADER OPTIONS + public static String readString(String from) { + try (InputStream is = readResourceAsStream(from)) { + byte[] bytes = DataTool.readAllBytes(is); + return new String(bytes, Charset.defaultCharset()); + } catch (Exception e) { + return null; + } + } + public static boolean copyAsset(String origin, Path dest) { + try (InputStream is = readResourceAsStream(origin)) { + if (is == null) throw new FileNotFoundException("Resource was not found in " + origin); + + File destParent = dest.getParent().toFile(); + if (!destParent.exists() && !destParent.mkdirs()) LOGGER.fatal(IT, "Cannot be created parent directories to {}", dest.toString()); + Files.copy(is, dest, StandardCopyOption.REPLACE_EXISTING); + return true; + } catch (Exception e) { + LOGGER.fatal(IT, "Failed to extract from (JAR) {} to {} due to unexpected error", origin, dest, e); + } + return false; + } + + public static List readStringList(String path) { + List result = new ArrayList<>(); + try (InputStreamReader reader = new InputStreamReader(readResourceAsStream(path))) { + result.addAll(new Gson().fromJson(reader, new TypeToken>() {}.getType())); + } catch (Exception e) { + LOGGER.fatal(IT, "Exception trying to read JSON from {}", path, e); + } + + return result; + } + + public static String[] readArrayAndParse(String path, Map values) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(readResourceAsStream(path)))) { + String[] keyset = values.keySet().toArray(new String[0]); + String[] str = new Gson().fromJson(reader, new TypeToken() {}.getType()); + + String v; + for (int i = 0; i < str.length; i++) { + v = str[i]; + for (String s : keyset) { + str[i] = v.replace("{" + s + "}", values.get(s)); + } + } + return str; + } + } + + public static String[] readArray(String path) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(readResourceAsStream(path)))) { + return new Gson().fromJson(reader, new TypeToken() {}.getType()); + } + } + + public static BufferedImage readImage(String path) { + try (InputStream in = readResourceAsStream(path)) { + BufferedImage image = ImageIO.read(in); + if (image != null) return image; + else throw new FileNotFoundException("result of BufferedImage was null"); + } catch (Exception e) { + throw new IllegalStateException("Failed loading BufferedImage from resources", e); + } + } + + public static GifDecoder readGif(String path) { + try (BufferedInputStream in = new BufferedInputStream(readResourceAsStream(path))) { + GifDecoder gif = new GifDecoder(); + int status = gif.read(in); + if (status == GifDecoder.STATUS_OK) return gif; + + throw new IOException("Failed to process GIF - Decoder status: " + status); + } catch (Exception e) { + throw new IllegalStateException("Failed loading GIF from resources", e); + } + } + + public static InputStream readResourceAsStream(String source) { + return readResourceAsStream$byClassLoader(source, JarTool.class.getClassLoader()); // InputStream still can be null + } + + private static InputStream readResourceAsStream$byClassLoader(String source, ClassLoader classLoader) { + InputStream is = classLoader.getResourceAsStream(source); + if (is == null && source.startsWith("/")) is = classLoader.getResourceAsStream(source.substring(1)); + return is; + } + + public static URL readResource(String source) { + return readResource$byClassLoader(source, JarTool.class.getClassLoader()); // URL still can be null + } + + private static URL readResource$byClassLoader(String source, ClassLoader classLoader) { + URL is = classLoader.getResource(source); + if (is == null && source.startsWith("/")) is = classLoader.getResource(source.substring(1)); + return is; + } +} \ No newline at end of file diff --git a/src/main/java/me/srrapero720/watermedia/tools/PairTool.java b/src/main/java/me/srrapero720/watermedia/tools/PairTool.java new file mode 100644 index 00000000..1ef5c0a9 --- /dev/null +++ b/src/main/java/me/srrapero720/watermedia/tools/PairTool.java @@ -0,0 +1,34 @@ +package me.srrapero720.watermedia.tools; + +import java.util.function.Supplier; + +public class PairTool implements Supplier { + + private final K left; + private final V right; + public PairTool(K left, V right) { + this.left = left; + this.right = right; + } + + public K getLeft() { + return left; + } + + public V getRight() { + return right; + } + + public K getKey() { + return left; + } + + public V getValue() { + return right; + } + + @Override + public V get() { + return right; + } +} diff --git a/src/main/java/org/watermedia/tools/ThreadTool.java b/src/main/java/me/srrapero720/watermedia/tools/ThreadTool.java similarity index 60% rename from src/main/java/org/watermedia/tools/ThreadTool.java rename to src/main/java/me/srrapero720/watermedia/tools/ThreadTool.java index a450a38c..7e5a90de 100644 --- a/src/main/java/org/watermedia/tools/ThreadTool.java +++ b/src/main/java/me/srrapero720/watermedia/tools/ThreadTool.java @@ -1,4 +1,4 @@ -package org.watermedia.tools; +package me.srrapero720.watermedia.tools; import org.apache.logging.log4j.Marker; import org.apache.logging.log4j.MarkerManager; @@ -8,7 +8,7 @@ import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; -import static org.watermedia.WaterMedia.LOGGER; +import static me.srrapero720.watermedia.WaterMedia.LOGGER; public class ThreadTool { private static final Marker IT = MarkerManager.getMarker("ThreadTool"); @@ -28,25 +28,40 @@ public static int minThreads() { return 10; } - public static ScheduledExecutorService executorReduced(String name, int priority) { - return Executors.newScheduledThreadPool(ThreadTool.minThreads(), ThreadTool.factory("WaterMedia-Worker-" + name, priority)); + @Deprecated + public static Thread thread(int priority, Runnable runnable) { + Thread thread = thread$basic(runnable); + thread.setPriority(priority); + thread.start(); + return thread; } - public static ScheduledExecutorService executorReduced(String name) { - return Executors.newScheduledThreadPool(ThreadTool.minThreads(), ThreadTool.factory("WaterMedia-Worker-" + name, Thread.NORM_PRIORITY)); + @Deprecated + public static Thread thread(Runnable runnable) { + Thread thread = thread$basic(runnable); + thread.start(); + return thread; } - public static ScheduledExecutorService executorOne(String name) { - return Executors.newScheduledThreadPool(1, ThreadTool.factory("WaterMedia-" + name, Thread.NORM_PRIORITY)); + private static Thread thread$basic(Runnable runnable) { + Thread thread = new Thread(runnable); + thread.setName("WaterMedia-Task-" + (++TWC)); + thread.setUncaughtExceptionHandler(EXCEPTION_HANDLER); + thread.setDaemon(true); + return thread; + } + + public static ScheduledExecutorService executorReduced(String name) { + return Executors.newScheduledThreadPool(ThreadTool.minThreads(), ThreadTool.factory("wm-worker-" + name, Thread.NORM_PRIORITY)); } public static ThreadFactory factory(String name, int priority) { - final var count = new AtomicInteger(); - final var handler = Thread.currentThread().getUncaughtExceptionHandler(); + AtomicInteger count = new AtomicInteger(); + Thread.UncaughtExceptionHandler handler = Thread.currentThread().getUncaughtExceptionHandler(); return r -> { Thread t = new Thread(r); t.setName(name + "-" + count.incrementAndGet()); - t.setDaemon(true); // we must not keep working + t.setDaemon(true); t.setUncaughtExceptionHandler(((t1, e2) -> { EXCEPTION_HANDLER.uncaughtException(t1, e2); handler.uncaughtException(t1, e2); @@ -55,28 +70,4 @@ public static ThreadFactory factory(String name, int priority) { return t; }; } - - @Deprecated - public static Thread thread(int priority, Runnable runnable) { - Thread t = thread$basic(runnable); - t.setPriority(priority); - t.start(); - return t; - } - - @Deprecated - public static Thread thread(Runnable runnable) { - Thread t = thread$basic(runnable); - t.start(); - return t; - } - - @Deprecated - private static Thread thread$basic(Runnable runnable) { - Thread t = new Thread(runnable); - t.setName("WaterMedia-Task-" + (++TWC)); - t.setUncaughtExceptionHandler(EXCEPTION_HANDLER); - t.setDaemon(true); - return t; - } } diff --git a/src/main/java/org/watermedia/WaterMedia.java b/src/main/java/org/watermedia/WaterMedia.java deleted file mode 100644 index ec204bd5..00000000 --- a/src/main/java/org/watermedia/WaterMedia.java +++ /dev/null @@ -1,102 +0,0 @@ -package org.watermedia; - -import com.sun.jna.Platform; -import org.watermedia.api.WaterMediaAPI; -import org.watermedia.tools.DataTool; -import org.watermedia.tools.JarTool; -import org.watermedia.tools.ArgTool; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.Marker; -import org.apache.logging.log4j.MarkerManager; - -import java.io.File; -import java.nio.file.Path; -import java.util.*; - -public final class WaterMedia { - private static final Marker IT = MarkerManager.getMarker("Bootstrap"); - public static final String ID = "watermedia"; - public static final String NAME = "WATERMeDIA"; - public static final String VERSION = JarTool.readString("/watermedia/version.cfg"); - public static final String USER_AGENT = "WaterMedia/" + VERSION; - public static final Logger LOGGER = LogManager.getLogger(ID); - - private static final Path DEFAULT_TEMP = new File(System.getProperty("java.io.tmpdir")).toPath().toAbsolutePath().resolve("watermedia"); - private static final Path DEFAULT_CWD = new File("run").toPath().toAbsolutePath(); - public static final ILoader DEFAULT_LOADER = new ILoader() { - @Override public String name() { return "Default"; } - @Override public Path tmp() { return DEFAULT_TEMP; } - @Override public Path cwd() { return DEFAULT_CWD; } - @Override public boolean client() { return true; } - }; - - private static final ArgTool NO_BOOT = DataTool.getArgument("watermedia.no_boot"); - private static final ArgTool FAIL_HARD = DataTool.getArgument("watermedia.fail_hard"); - private static ILoader bootstrap; - private static WaterMedia instance; - - private WaterMedia() {} - - public static WaterMedia prepare(ILoader boot) { - if (boot == null) throw new NullPointerException("Bootstrap is null"); - if (instance != null) throw new IllegalStateException(NAME + " is already prepared"); - LOGGER.info(IT, "Preparing '{}' for '{}'", NAME, boot.name()); - LOGGER.info(IT, "Loading {} version '{}'", NAME, VERSION); - LOGGER.info(IT, "Detected OS: {} ({})", System.getProperty("os.name"), Platform.ARCH); - - if (NO_BOOT.getAsBoolean()) - LOGGER.warn(IT, "{} argument detected, API booting is disabled", NO_BOOT.key()); - - if (FAIL_HARD.getAsBoolean()) - LOGGER.warn(IT, "{} argument detected, crashes will be threw at the minimal exception", NO_BOOT.key()); - - if (!boot.client()) - LOGGER.warn(IT, "{} is installed on server-side, please report issues to the dependent mod author instead of {} author", NAME, NAME); - - WaterMedia.bootstrap = boot; - return instance = new WaterMedia(); - } - - public void start() throws Exception { - if (NO_BOOT.getAsBoolean()) return; - if (!bootstrap.client()) return; - - final var modules = DataTool.toList(ServiceLoader.load(WaterMediaAPI.class)); - modules.sort(Comparator.comparingInt(e -> e.priority().ordinal())); - - for (WaterMediaAPI m: modules) { - LOGGER.info(IT, "Starting '{}'", m.getClass().getSimpleName()); - if (!m.prepare(bootstrap)) { - LOGGER.warn(IT, "Module '{}' refuses to be loaded, skipping", m.getClass().getSimpleName()); - continue; - } - m.start(bootstrap); - LOGGER.info(IT, "Module '{}' loaded successfully", m.getClass().getSimpleName()); - } - LOGGER.info(IT, "Startup finished"); - } - - public static ILoader getLoader() { return bootstrap; } - - public static String asResource(String path) { return WaterMedia.ID + ":" + path; } - - public static class UnsupportedTLException extends Exception { - public UnsupportedTLException() { - super("TLauncher is NOT supported by " + NAME + ", please stop using it (and consider safe alternatives like SKLauncher or MultiMC)"); - LOGGER.fatal(IT, "############################## ILLEGAL LAUNCHER DETECTED ######################################"); - LOGGER.fatal(IT, "{} refuses to load sensitive modules in a INFECTED launcher, please stop using TLauncher dammit", NAME); - LOGGER.fatal(IT, "Because TLauncher infects sensitive files (which {} includes) and we prefer avoid any risk", NAME); - LOGGER.fatal(IT, "Consider use safe alternative like SKLauncher or BUY the game and use the CurseForge Launcher"); - LOGGER.fatal(IT, "And please avoid Feather Launcher, TLauncher Legacy or any CRACKED LAUNCHER WITH A BAD REPUTATION"); - LOGGER.fatal(IT, "############################## ILLEGAL LAUNCHER DETECTED ######################################"); - } - } - - public interface ILoader { - String name(); - Path tmp(); - Path cwd(); - boolean client(); - } -} \ No newline at end of file diff --git a/src/main/java/org/watermedia/WaterMediaApp.java b/src/main/java/org/watermedia/WaterMediaApp.java deleted file mode 100644 index 415dbb42..00000000 --- a/src/main/java/org/watermedia/WaterMediaApp.java +++ /dev/null @@ -1,234 +0,0 @@ -package org.watermedia; - -import me.srrapero720.watermedia.Main; -import org.watermedia.api.MathAPI; -import org.watermedia.tools.ThreadTool; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import javax.swing.text.DefaultCaret; -import java.awt.*; -import java.io.File; -import java.nio.file.Path; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.concurrent.Executor; -import java.util.logging.*; - -public class WaterMediaApp { - public static final Logger LOGGER = Logger.getLogger("WaterMedia"); - public static final Executor EXECUTOR = ThreadTool.executorReduced("app-task"); - public static final String JAR_PATH = WaterMediaApp.class.getProtectionDomain().getCodeSource().getLocation().toString().replace("file:///", "").replace("file:/", ""); - public static final Path ROOT_PATH = new File(JAR_PATH).getParentFile().getParentFile().toPath(); - public static final DateFormat FORMAT = new SimpleDateFormat("HH:mm:ss"); - public static final ClassLoader CLASS_LOADER = Main.class.getClassLoader(); - - public static final JTextArea CONSOLE = new JTextArea(); - public static final JFrame WINDOW = new JFrame(WaterMedia.NAME + ": Diagnosis Tool"); - public static final JPanel PANEL = new JPanel(new BorderLayout()); - public static final JPanel ACTIONS_PANEL = new JPanel(new FlowLayout(FlowLayout.RIGHT)); - - public static boolean test_weAreOnModsFolder() { - var root = ROOT_PATH.toFile(); - var files = root.listFiles(); - for (var f: files == null ? new File[0] : files) { - if (f.getName().equals("mods")) { - return true; - } - } - - return false; - } - - public static void task_collectLogs() { - var file = ROOT_PATH.toFile(); - for (var f: file.listFiles()) { - - } - } - - public static void task_collectCrashReports() { - - } - - public static void task_collectHsErrPid() { - - } - - public static void task_collectOsDetails() { - - } - - public static void task_collectNetworkDetails() { - - } - - public static void runDiagnosisTool() { - - } - - public static void runLogCollections() { - for (Component c: ACTIONS_PANEL.getComponents()) c.setEnabled(false); - EXECUTOR.execute(WaterMediaApp::task_collectLogs); - EXECUTOR.execute(WaterMediaApp::task_collectCrashReports); - EXECUTOR.execute(WaterMediaApp::task_collectHsErrPid); - EXECUTOR.execute(WaterMediaApp::task_collectOsDetails); - EXECUTOR.execute(WaterMediaApp::task_collectNetworkDetails); - EXECUTOR.execute(() -> { - long logPrinted = System.currentTimeMillis(); - long targetTime = logPrinted + 10000; // +10 secs - int count = 10; - while (System.currentTimeMillis() < targetTime) { - if (logPrinted < System.currentTimeMillis()) { - LOGGER.info("Closing in " + count--); - logPrinted = System.currentTimeMillis() + 1000; - } - } - WINDOW.dispose(); - }); - } - - private static void onCreate() { - LOGGER.info("Welcome to the diagnosis tool"); - LOGGER.info("==== Click on any of the options to start ===="); - - if (!test_weAreOnModsFolder()) { - for (var c: ACTIONS_PANEL.getComponents()) { - c.setEnabled(false); - LOGGER.severe("WaterMedia is not placed on Mods folder, please ask for personal support on Discord"); - } - } - } - - private static void createWindow() { - WINDOW.setSize(1280, 720); - WINDOW.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - WINDOW.setLocationRelativeTo(null); - WINDOW.setIconImage(new ImageIcon(CLASS_LOADER.getResource("icon.png")).getImage()); - createLayout(); - PANEL.add(ACTIONS_PANEL, BorderLayout.SOUTH); - WINDOW.add(PANEL); - WINDOW.setVisible(true); - onCreate(); - } - - private static void createLayout() { - PANEL.setBackground(new Color(MathAPI.argb(255, 40, 40, 40))); - - JPanel imagePanel = new JPanel(new BorderLayout(10, 0)); - imagePanel.setBackground(new Color(MathAPI.argb(255, 20 ,20, 20))); // Establecer el fondo oscuro - - JLabel logo = new JLabel(); - logo.setIcon(new ImageIcon(new ImageIcon(CLASS_LOADER.getResource("banner.png")).getImage().getScaledInstance(600, 100, Image.SCALE_FAST))); - imagePanel.add(logo, BorderLayout.WEST); - PANEL.add(imagePanel, BorderLayout.NORTH); - - JLabel info = new JLabel(); - info.setText("Diagnosis Tool"); - info.setFont(new Font("Default", Font.BOLD, 24)); - info.setForeground(Color.WHITE); - info.setBorder(new EmptyBorder(0, 0, 0, 20)); - imagePanel.add(info, BorderLayout.EAST); - - CONSOLE.setEditable(true); - CONSOLE.setLineWrap(true); - CONSOLE.setWrapStyleWord(true); - CONSOLE.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14)); - CONSOLE.setBackground(Color.DARK_GRAY); - CONSOLE.setForeground(Color.WHITE); - CONSOLE.setMargin(new Insets(5, 5, 5, 5)); - DefaultCaret caret = new DefaultCaret(); - caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE); - caret.setVisible(true); - CONSOLE.setCaret(caret); - - JScrollPane scrollPane = new JScrollPane(CONSOLE); - // Configurar la política de desplazamiento vertical - scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); - scrollPane.setPreferredSize(new Dimension(400, 300)); - - ConsoleHandler handler = new ConsoleHandler(); - handler.setFormatter(new ConsoleFormatter(CONSOLE)); - LOGGER.addHandler(handler); - LOGGER.setUseParentHandlers(false); - - PANEL.add(scrollPane, BorderLayout.CENTER); - - // BUTTONS PANEL - ACTIONS_PANEL.setBackground(new Color(MathAPI.argb(255, 20 ,20, 20))); // Establecer el fondo oscuro - - // Crear el botón y agregarlo al panel - JButton generateReport = new JButton("Collect Crash Report"); - generateReport.setText("Collect Crash Report"); - generateReport.setBackground(Color.BLACK); // Color de fondo del botón - generateReport.setForeground(Color.WHITE); // Color del texto del botón - generateReport.setFocusPainted(false); // Eliminar el borde de enfoque - generateReport.setMargin(new Insets(15, 25, 15, 25)); - generateReport.setFont(new Font("Default", Font.PLAIN, 16)); - generateReport.addActionListener(e -> runLogCollections()); - - JButton diagnosis = new JButton("Diagnosis"); - - diagnosis.setBackground(Color.BLACK); // Color de fondo del botón - diagnosis.setForeground(Color.WHITE); // Color del texto del botón - diagnosis.setFocusPainted(false); // Eliminar el borde de enfoque - diagnosis.setMargin(new Insets(15, 25, 15, 25)); - diagnosis.setFont(new Font("Default", Font.PLAIN, 16)); - diagnosis.addActionListener(e -> { - for (Component c: ACTIONS_PANEL.getComponents()) c.setEnabled(false); - runDiagnosisTool(); - }); - - ACTIONS_PANEL.add(diagnosis); - ACTIONS_PANEL.add(generateReport); - } - - public static void main(String... args) { - createWindow(); - } - - private static class ConsoleFormatter extends Formatter { - public static final String RESET = "\u001B[0m"; - public static final String RED = "\u001B[31m"; - public static final String GREEN = "\u001B[32m"; - public static final String YELLOW = "\u001B[33m"; - public static final String BLUE = "\u001B[34m"; - public static final String PURPLE = "\u001B[35m"; - - JTextArea textArea; - public ConsoleFormatter(JTextArea textArea) { - this.textArea = textArea; - } - - @Override - public String format(LogRecord record) { - String color = getColorLevel(record.getLevel()); - String time = "[" + FORMAT.format(new Date(record.getMillis())) + "]"; - String threadLevel = "[" + Thread.currentThread().getName() + "/" + record.getLevel().toString().substring(0, 4) + "]"; - String loggerMarker = "[" + record.getLoggerName() + "/" + "]"; - - String r = time + " " + threadLevel + " " + loggerMarker + ": " + record.getMessage() + (record.getThrown() != null ? record.getThrown().toString() : "") + "\n"; - - textArea.append(r); - textArea.setCaretPosition(textArea.getDocument().getLength()); - return color + r + RESET; - } - - public String getColorLevel(Level level) { - if (level.equals(Level.SEVERE)) { - return RED; - } else if (level.equals(Level.WARNING)) { - return YELLOW; - } else if (level.equals(Level.INFO)) { - return GREEN; - } else if (level.equals(Level.CONFIG)) { - return BLUE; - } else if (level.intValue() <= Level.FINE.intValue()) { - return PURPLE; - } else { - return RESET; - } - } - } -} diff --git a/src/main/java/org/watermedia/api/MediaAPI.java b/src/main/java/org/watermedia/api/MediaAPI.java deleted file mode 100644 index 8ec1265f..00000000 --- a/src/main/java/org/watermedia/api/MediaAPI.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.watermedia.api; - -import org.watermedia.WaterMedia; -import org.watermedia.api.media.ImageSource; -import org.watermedia.api.media.MediaSource; -import org.watermedia.api.network.MRL; - -import java.util.ArrayList; -import java.util.List; - -public class MediaAPI extends WaterMediaAPI { - - @Override - public Priority priority() { - return Priority.NORMAL; - } - - @Override - public boolean prepare(WaterMedia.ILoader bootCore) throws Exception { - return false; - } - - @Override - public void start(WaterMedia.ILoader bootCore) throws Exception { - - } - - @Override - public void release() { - } -} diff --git a/src/main/java/org/watermedia/api/MemoryAPI.java b/src/main/java/org/watermedia/api/MemoryAPI.java deleted file mode 100644 index 9bd5a8b7..00000000 --- a/src/main/java/org/watermedia/api/MemoryAPI.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.watermedia.api; - -import org.lwjgl.system.MemoryUtil; -import org.watermedia.WaterMedia; -import org.watermedia.videolan4j.VideoLan4J; - -import java.nio.ByteBuffer; - -public class MemoryAPI extends WaterMediaAPI { - private static MemoryUtil.MemoryAllocator ALLOCATOR; - - /** - * Creates a DirectByteBuffer unsafe using {@link MemoryUtil.MemoryAllocator MemoryAllocator} - * @param size size of the buffer - * @return native buffer - */ - public static ByteBuffer allocate(int size) { - if (ALLOCATOR == null) - throw new IllegalStateException("MemoryAPI is uninitialized"); - - long i = ALLOCATOR.malloc(size); - if (i == 0L) { - throw new OutOfMemoryError("Failed to allocate " + size + " bytes"); - } else { - return MemoryUtil.memByteBuffer(i, size); - } - } - - public static ByteBuffer resize(ByteBuffer buffer, int size) { - if (ALLOCATOR == null) - throw new IllegalStateException("MemoryAPI is uninitialized"); - if (buffer == null) - throw new NullPointerException("ByteBuffer is null"); - if (!buffer.isDirect()) - throw new UnsupportedOperationException("ByteBuffer must be direct and allocated by the MemoryAPI"); - - long i = ALLOCATOR.realloc(MemoryUtil.memAddress0(buffer), size); - if (i == 0L) { - throw new OutOfMemoryError("Failed to resize buffer from " + buffer.capacity() + " bytes to " + size + " bytes"); - } else { - return MemoryUtil.memByteBuffer(i, size); - } - } - - public static void deallocate(ByteBuffer... buffers) { - if (ALLOCATOR == null) - throw new IllegalStateException("MemoryAPI is uninitialized"); - - if (buffers == null) return; - for (ByteBuffer b: buffers) { - if (!b.isDirect()) continue; - ALLOCATOR.free(MemoryUtil.memAddress0(b)); - } - } - - @Override - public Priority priority() { - return Priority.HIGH; - } - - @Override - public boolean prepare(WaterMedia.ILoader bootCore) throws Exception { - return ALLOCATOR == null; - } - - @Override - public void start(WaterMedia.ILoader bootCore) throws Exception { - ALLOCATOR = MemoryUtil.getAllocator(false); - // REPLACE JAVA WAY FOR LWJGL WAY - VideoLan4J.setBufferAllocator(MemoryAPI::allocate); - VideoLan4J.setBufferDeallocator(MemoryAPI::deallocate); - } - - @Override - public void release() { - ALLOCATOR = null; - } -} diff --git a/src/main/java/org/watermedia/api/NetworkAPI.java b/src/main/java/org/watermedia/api/NetworkAPI.java deleted file mode 100644 index 73188383..00000000 --- a/src/main/java/org/watermedia/api/NetworkAPI.java +++ /dev/null @@ -1,104 +0,0 @@ -package org.watermedia.api; - -import me.srrapero720.watermedia.api.MediaContext; -import org.watermedia.WaterMedia; -import org.watermedia.api.network.MRL; -import org.watermedia.core.network.patchs.AbstractPatch; -import org.watermedia.tools.DataTool; -import org.apache.logging.log4j.Marker; -import org.apache.logging.log4j.MarkerManager; - -import java.net.*; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; -import java.util.ServiceLoader; - -import static org.watermedia.WaterMedia.LOGGER; - -public class NetworkAPI extends WaterMediaAPI { - public static final Marker IT = MarkerManager.getMarker(NetworkAPI.class.getSimpleName()); - - private static final ServiceLoader PATCHES = ServiceLoader.load(AbstractPatch.class); - - public static void patchMRL(MRL mrl, MediaContext context) { - try { - for (AbstractPatch patch: PATCHES) { - if (!patch.validate(mrl)) continue; - patch.patch(context, mrl); - } - } catch (Exception e) { - LOGGER.error(IT, "Failed to patch URL '{}'", mrl.getUri(), e); - } - } - - public static String[] getPatchNames() { - ArrayList r = new ArrayList<>(); - for (AbstractPatch patch: PATCHES) { - r.add(patch.name()); - } - return r.toArray(new String[0]); - } - - /** - * Parses a query string from a {@link URL#getQuery()} in a Map - * @param query query string - * @return map with all values as a String - */ - public static Map decodeQuery(String query) { - final var result = new HashMap(); - final var params = query.split("&"); - for (String p: params) { - var keyVal = p.split("="); - if (keyVal.length == 2) { - result.put(keyVal[0], keyVal[1]); - } - } - return result; - } - - /** - * Encodes a map of string into a Query string - * @param map map of params - * @return encoded string with all values - */ - public static String encodeQuery(Map map) { - var builder = new StringBuilder(); - map.forEach((k, v) -> { - builder.append(k).append("="); - if (v instanceof Map valueMap) { - builder.append(DataTool.GSON.toJson(valueMap)); - } else { - builder.append(v); - } - - builder.append("&"); - }); - - if (builder.charAt(builder.length() - 1) == '&') { - builder.deleteCharAt(builder.length() - 1); - } - return "?" + URLEncoder.encode(builder.toString(), StandardCharsets.UTF_8); - } - - @Override - public Priority priority() { - return Priority.LOW; - } - - @Override - public boolean prepare(WaterMedia.ILoader bootCore) throws Exception { - return true; - } - - @Override - public void start(WaterMedia.ILoader bootCore) throws Exception { - - } - - @Override - public void release() { - - } -} \ No newline at end of file diff --git a/src/main/java/org/watermedia/api/RenderAPI.java b/src/main/java/org/watermedia/api/RenderAPI.java deleted file mode 100644 index 6337b145..00000000 --- a/src/main/java/org/watermedia/api/RenderAPI.java +++ /dev/null @@ -1,134 +0,0 @@ -package org.watermedia.api; - -import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL12; -import org.watermedia.WaterMedia; -import org.watermedia.videolan4j.VideoLan4J; - -import java.awt.*; -import java.awt.image.BufferedImage; -import java.awt.image.DataBufferByte; -import java.nio.ByteBuffer; - -public class RenderAPI extends WaterMediaAPI { - - /** - * Converts the image on other formats into 4-BYTE RGBA (java ABGR) - * - *

RenderAPI only works with RGBA buffer format

- * - * @param oldImage image to convert into the desired format - * @return converted image to the desired format, same object if already has the right format - */ - public static BufferedImage convertFormat(BufferedImage oldImage) { - if (oldImage.getType() == BufferedImage.TYPE_4BYTE_ABGR) return oldImage; // no conversion needed when is what we want - - // Convert the image to the expected format. - final var newImage = new BufferedImage(oldImage.getWidth(), oldImage.getHeight(), BufferedImage.TYPE_4BYTE_ABGR); - Graphics g = newImage.getGraphics(); - g.drawImage(oldImage, 0, 0, null); - g.dispose(); - return newImage; - } - - public static ByteBuffer[] getByteBuffers(BufferedImage[] images) { - ByteBuffer[] buffers = new ByteBuffer[images.length]; - for (int i = 0; i < images.length; i++) { - buffers[i] = getByteBuffer(images[i]); - } - return buffers; - } - - /** - * Converts the format and stores the pixels into a ByteBuffer ready to be used in OpenGL - * @param image Image to convert - * @return ByteBuffer of the image - */ - public static ByteBuffer getByteBuffer(BufferedImage image) { - image = convertFormat(image); - byte[] pixels = ((DataBufferByte) convertFormat(image).getRaster().getDataBuffer()).getData(); - - ByteBuffer buffer = MemoryAPI.allocate(image.getWidth() * image.getHeight() * 4); - buffer.put(pixels); - buffer.flip(); - return buffer; - } - - public static int genTexture() { - int texture = GL11.glGenTextures(); - - // Bind - GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture); - - //Setup wrap mode - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE); - - //Setup texture scaling filtering (no dark textures) - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); - - // Unbind - GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0); - - return texture; - } - - public static void delTexture(int texture) { - GL11.glDeleteTextures(texture); - } - - /** - * Process a buffer to be used in a OpenGL texture id - * @param buffer ByteBuffer to be processed - * @param texture texture ID from OpenGL - * @param width buffer width - * @param height buffer height - * @param firstFrame if was the first frame - */ - public static void uploadBuffer(ByteBuffer buffer, int texture, int width, int height, boolean firstFrame) { - GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture); - - GL11.glPixelStorei(GL11.GL_UNPACK_ROW_LENGTH, GL11.GL_ZERO); - GL11.glPixelStorei(GL11.GL_UNPACK_SKIP_PIXELS, GL11.GL_ZERO); - GL11.glPixelStorei(GL11.GL_UNPACK_SKIP_ROWS, GL11.GL_ZERO); - - if (firstFrame) - GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA, width, height, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buffer); - else - GL11.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, width, height, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buffer); - } - - /** - * Downloads a buffer from the GPU - * @param texture texture ID from OpenGL - * @param width buffer width - * @param height buffer height - * @return allocated buffer with the raw texture data - */ - public static ByteBuffer downloadBuffer(int texture, int width, int height) { - ByteBuffer buffer = MemoryAPI.allocate(width * height * 4); - GL11.glGetTexImage(texture, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buffer); - return buffer; - } - - @Override - public Priority priority() { - return Priority.NORMAL; - } - - @Override - public boolean prepare(WaterMedia.ILoader bootCore) throws Exception { - return true; - } - - @Override - public void start(WaterMedia.ILoader bootCore) throws Exception { - - } - - @Override - public void release() { - - } -} diff --git a/src/main/java/org/watermedia/api/SearchAPI.java b/src/main/java/org/watermedia/api/SearchAPI.java deleted file mode 100644 index 543e0cad..00000000 --- a/src/main/java/org/watermedia/api/SearchAPI.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.watermedia.api; - -public class SearchAPI { - public static void search(String term) { - - } -} diff --git a/src/main/java/org/watermedia/api/math/MathEase.java b/src/main/java/org/watermedia/api/math/MathEase.java deleted file mode 100644 index b06477c9..00000000 --- a/src/main/java/org/watermedia/api/math/MathEase.java +++ /dev/null @@ -1,194 +0,0 @@ -package org.watermedia.api.math; - -import org.watermedia.api.MathAPI; - -public enum MathEase { - EASE_IN { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeIn(start, end, value); - } - }, - EASE_OUT { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeOut(start, end, value); - } - }, - EASE_IN_OUT { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeInOut(start, end, value); - } - }, - EASE_OUT_IN { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeOutIn(start, end, value); - } - }, - EASE_IN_SINE { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeInSine(start, end, value); - } - }, - EASE_OUT_SINE { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeOutSine(start, end, value); - } - }, - EASE_IN_OUT_SINE { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeInOutSine(start, end, value); - } - }, - EASE_IN_CUBIC { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeInCubic(start, end, value); - } - }, - EASE_OUT_CUBIC { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeOutCubic(start, end, value); - } - }, - EASE_IN_OUT_CUBIC { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeInOutCubic(start, end, value); - } - }, - EASE_IN_QUAD { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeInQuad(start, end, value); - } - }, - EASE_OUT_QUAD { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeOutQuad(start, end, value); - } - }, - EASE_IN_OUT_QUAD { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeInOutQuad(start, end, value); - } - }, - EASE_IN_ELASTIC { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeInElastic(start, end, value); - } - }, - EASE_OUT_ELASTIC { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeOutElastic(start, end, value); - } - }, - EASE_IN_OUT_ELASTIC { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeInOutElastic(start, end, value); - } - }, - EASE_IN_QUINT { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeInQuint(start, end, value); - } - }, - EASE_OUT_QUINT { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeOutQuint(start, end, value); - } - }, - EASE_IN_OUT_QUINT { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeInOutQuint(start, end, value); - } - }, - EASE_IN_CIRCLE { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeInCircle(start, end, value); - } - }, - EASE_OUT_CIRCLE { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeOutCircle(start, end, value); - } - }, - EASE_IN_OUT_CIRCLE { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeInOutCircle(start, end, value); - } - }, - EASE_IN_EXPO { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeInExpo(start, end, value); - } - }, - EASE_OUT_EXPO { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeOutExpo(start, end, value); - } - }, - EASE_IN_OUT_EXPO { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeInOutExpo(start, end, value); - } - }, - EASE_IN_BACK { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeInBack(start, end, value); - } - }, - EASE_OUT_BACK { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeOutBack(start, end, value); - } - }, - EASE_IN_OUT_BACK { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeInOutBack(start, end, value); - } - }, - EASE_IN_BOUNCE { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeInBounce(start, end, value); - } - }, - EASE_OUT_BOUNCE { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeOutBounce(start, end, value); - } - }, - EASE_IN_OUT_BOUNCE { - @Override - public double apply(double start, double end, double value) { - return MathAPI.easeInOutBounce(start, end, value); - } - }; - - public abstract double apply(double start, double end, double value); -} \ No newline at end of file diff --git a/src/main/java/org/watermedia/api/media/AudioSource.java b/src/main/java/org/watermedia/api/media/AudioSource.java deleted file mode 100644 index ce275a24..00000000 --- a/src/main/java/org/watermedia/api/media/AudioSource.java +++ /dev/null @@ -1,158 +0,0 @@ -package org.watermedia.api.media; - -public class AudioSource extends MediaSource { - @Override - public int width() { - return 0; - } - - @Override - public int height() { - return 0; - } - - @Override - public boolean start() { - return false; - } - - @Override - public boolean startPaused() { - return false; - } - - @Override - public boolean resume() { - return false; - } - - @Override - public boolean pause() { - return false; - } - - @Override - public boolean pause(boolean paused) { - return false; - } - - @Override - public boolean stop() { - return false; - } - - @Override - public boolean togglePlay() { - return false; - } - - @Override - public boolean seek(long time) { - return false; - } - - @Override - public boolean seekQuick(long time) { - return false; - } - - @Override - public boolean foward() { - return false; - } - - @Override - public boolean rewind() { - return false; - } - - @Override - public boolean speed(float speed) { - return false; - } - - @Override - public boolean repeat() { - return false; - } - - @Override - public boolean repeat(boolean repeat) { - return false; - } - - @Override - public boolean usable() { - return false; - } - - @Override - public boolean loading() { - return false; - } - - @Override - public boolean buffering() { - return false; - } - - @Override - public boolean ready() { - return false; - } - - @Override - public boolean paused() { - return false; - } - - @Override - public boolean playing() { - return false; - } - - @Override - public boolean stopped() { - return false; - } - - @Override - public boolean ended() { - return false; - } - - @Override - public boolean validSource() { - return false; - } - - @Override - public boolean liveSource() { - return false; - } - - @Override - public boolean canSeek() { - return false; - } - - @Override - public long duration() { - return 0; - } - - @Override - public long time() { - return 0; - } - - @Override - public void release() { - - } - - @Override - public int texture() { - return 0; - } -} diff --git a/src/main/java/org/watermedia/api/media/ImageSource.java b/src/main/java/org/watermedia/api/media/ImageSource.java deleted file mode 100644 index 873e6add..00000000 --- a/src/main/java/org/watermedia/api/media/ImageSource.java +++ /dev/null @@ -1,306 +0,0 @@ -package org.watermedia.api.media; - -import org.watermedia.api.MathAPI; -import org.watermedia.api.MemoryAPI; -import org.watermedia.api.RenderAPI; -import org.watermedia.tools.DataTool; -import org.watermedia.tools.ThreadTool; - -import java.awt.image.BufferedImage; -import java.nio.ByteBuffer; -import java.util.*; -import java.util.concurrent.ConcurrentLinkedDeque; - -public class ImageSource extends MediaSource { - private static final List ACTIVE_MEDIA = new ArrayList<>(); - - // media info - private final int[] widths; - private final int[] heights; - private final long[] delays; - public final int texture = RenderAPI.genTexture(); - private final ByteBuffer[] buffers; - - // State - private final long duration; - private int textureIndex = 0; - private float speed = 1.0f; - private boolean repeat = true; - private boolean firstFrame = true; - private State state = State.WAITING; - private final Deque queueState = new ConcurrentLinkedDeque<>(); - - // CLOCK - private long time; - private long systemTime = System.currentTimeMillis(); - private boolean clock = false; - - public ImageSource(BufferedImage image) { - this(new BufferedImage[] { image }, new long[1]); - } - - public ImageSource(BufferedImage[] images, long[] delays) { - this( - DataTool.getValueFrom(images, RenderAPI::getByteBuffer), - DataTool.getIntValueFrom(images, BufferedImage::getWidth), - DataTool.getIntValueFrom(images, BufferedImage::getHeight), - delays - ); - } - - public ImageSource(ByteBuffer buffer, int width, int height) { - this(new ByteBuffer[] { buffer }, new int[] { width }, new int[] { height }, new long[1]); - } - - public ImageSource(ByteBuffer[] buffers, int[] width, int[] height, long[] delays) { - this.widths = width; - this.heights = height; - this.buffers = buffers; - this.delays = delays; - this.duration = MathAPI.sumArray(delays); - } - - @Override - public int width() { - return this.widths[this.textureIndex]; - } - - @Override - public int height() { - return this.heights[this.textureIndex]; - } - - @Override - public boolean start() { - this.state = State.PLAYING; - return true; - } - - @Override - public boolean startPaused() { - this.state = State.PAUSED; - return true; - } - - @Override - public boolean resume() { - // TODO: we should not ensure any other state? - this.state = State.PLAYING; - return true; - } - - @Override - public boolean pause() { - // TODO: we should not ensure any other state? - this.state = State.PAUSED; - return true; - } - - @Override - public boolean pause(boolean paused) { - // TODO: we should not ensure any other state? - this.state = paused ? State.PAUSED : State.PLAYING; - return false; - } - - @Override - public boolean stop() { - // TODO: we should not ensure any other state? - this.state = State.STOPPED; - return true; - } - - @Override - public boolean togglePlay() { - if (this.state == State.PLAYING || this.state == State.PAUSED) { - this.state = this.state == State.PLAYING ? State.PAUSED : State.PLAYING; - return true; - } else { - return false; - } - } - - @Override - public boolean seek(long time) { - if (time > this.duration) { - time = time % this.duration; - } - if (time < 0) { - time = 0; - } - this.time = time; - return true; - } - - @Override - public boolean seekQuick(long time) { - return seek(time); - } - - @Override - public boolean foward() { - return seek(this.time + 5000L); - } - - @Override - public boolean rewind() { - return seek(this.time - 5000L); - } - - @Override - public boolean speed(float speed) { - if (speed < 0 || speed > 2) { - return false; - } - this.speed = speed; - return true; - } - - @Override - public boolean repeat() { - return this.repeat(true); - } - - @Override - public boolean repeat(boolean repeat) { - this.repeat = repeat; - return true; - } - - @Override - public boolean usable() { - return true; // once created is ready - } - - @Override - public boolean loading() { - return this.state == State.LOADING; // once created is ready - } - - @Override - public boolean buffering() { - return this.state == State.BUFFERING; // no buffering - } - - @Override - public boolean ready() { - return this.state != State.ERROR; // once created is ready - } - - @Override - public boolean paused() { - return this.state == State.PAUSED; - } - - @Override - public boolean playing() { - return this.state == State.PLAYING; - } - - @Override - public boolean stopped() { - return this.state == State.STOPPED; - } - - @Override - public boolean ended() { - return this.state == State.ENDED; - } - - @Override - public boolean validSource() { - return true; - } - - @Override - public boolean liveSource() { - return false; - } - - @Override - public boolean canSeek() { - return this.duration > 0; - } - - @Override - public long duration() { - return this.duration; - } - - @Override - public long time() { - return this.time; - } - - @Override - public void release() { - this.state = State.ENDED; - RenderAPI.delTexture(this.texture); // free GPU memory - MemoryAPI.deallocate(this.buffers); // free RAM - Arrays.fill(this.buffers, null); - } - - @Override - public int texture() { - return textureInTime(this.time); - } - - private void setState(State state) { - this.queueState.add(state); - } - - // calculate texture - private int textureInTime(long time) { - for (int i = 0; i < this.delays.length; i++) { - time -= this.delays[i]; - if (time <= 0) { - this.uploadTexture(i); - return this.texture; - } - } - this.uploadTexture(this.buffers.length - 1); - return this.texture; - } - - // upload texture - private void uploadTexture(int index) { - if (this.buffers[index] == null) - throw new IllegalStateException("Current MediaSource is released"); - - RenderAPI.uploadBuffer(this.buffers[index], this.texture, this.widths[index], this.heights[index], this.firstFrame); - this.firstFrame = false; - } - - // FIXME: this is not working - private void run() { - // calculate delta - long delta = System.currentTimeMillis() - this.systemTime; - - // Update state - State state = this.queueState.peek(); - if (state != null) { - switch (state) { - case WAITING, LOADING, PAUSED, STOPPED, BUFFERING, ENDED, ERROR -> { - this.clock = false; - } - case PLAYING -> this.clock = true; - } - this.state = state; - } - - // compute clocking - if (this.clock) { - this.time = Math.max(this.time + delta, this.duration); // start counting - } - this.systemTime = System.currentTimeMillis(); - - if (this.time == this.duration) { // as expected - if (this.repeat) { - this.time = 0; - } else { - this.state = State.ENDED; - this.queueState.clear(); - } - } - } -} diff --git a/src/main/java/org/watermedia/api/media/MediaSource.java b/src/main/java/org/watermedia/api/media/MediaSource.java deleted file mode 100644 index 61714444..00000000 --- a/src/main/java/org/watermedia/api/media/MediaSource.java +++ /dev/null @@ -1,97 +0,0 @@ - -package org.watermedia.api.media; - -public abstract class MediaSource { - public static final long NO_DURATION = -1; - public MediaSource() { - - } - - public abstract int width(); - - public abstract int height(); - - public abstract boolean start(); - - public abstract boolean startPaused(); - - public abstract boolean resume(); - - public abstract boolean pause(); - - public abstract boolean pause(boolean paused); - - public abstract boolean stop(); - - public abstract boolean togglePlay(); - - public abstract boolean seek(long time); - - public abstract boolean seekQuick(long time); - - public abstract boolean foward(); - - public abstract boolean rewind(); - - public abstract boolean speed(float speed); - - public abstract boolean repeat(); - - public abstract boolean repeat(boolean repeat); - - // status - public abstract boolean usable(); - - public abstract boolean loading(); - - public abstract boolean buffering(); - - public abstract boolean ready(); - - public abstract boolean paused(); - - public abstract boolean playing(); - - public abstract boolean stopped(); - - public abstract boolean ended(); - - public abstract boolean validSource(); - - public abstract boolean liveSource(); - - public abstract boolean canSeek(); - - public abstract long duration(); - - public abstract long time(); - - public abstract void release(); - /** - * Provides the active texture ID of the media - * Images can calculate the time by their own - * @return OpenGL texture id - */ - public abstract int texture(); - - public enum State { - WAITING, - LOADING, - BUFFERING, - PLAYING, - PAUSED, - STOPPED, - ENDED, - ERROR; - - public static final State[] VALUES = values(); - - public static State of(int state) { - if (state > VALUES.length) - throw new IllegalArgumentException("You exceeded the allowed state numbers"); - if (state < 0) - throw new IllegalArgumentException("What the fuck have in your brain to ask a negative state"); - return VALUES[state]; - } - } -} diff --git a/src/main/java/org/watermedia/api/media/VideoLanSource.java b/src/main/java/org/watermedia/api/media/VideoLanSource.java deleted file mode 100644 index 2c7b84b2..00000000 --- a/src/main/java/org/watermedia/api/media/VideoLanSource.java +++ /dev/null @@ -1,183 +0,0 @@ -package org.watermedia.api.media; - -import com.sun.jna.ptr.IntByReference; -import org.watermedia.videolan4j.binding.internal.libvlc_instance_t; -import org.watermedia.videolan4j.binding.internal.libvlc_media_player_t; -import org.watermedia.videolan4j.binding.lib.LibVlc; - -import java.awt.*; - -public class VideoLanSource extends MediaSource { - private final IntByReference width = new IntByReference(); - private final IntByReference height = new IntByReference(); - private final libvlc_instance_t core; - private final libvlc_media_player_t raw; - - public VideoLanSource() { - this.raw = null; - this.core = null; - } - - @Override - public int width() { - int result = LibVlc.libvlc_video_get_size(this.raw, 0, this.width, this.height); - if (result != 0) - return 0; - - return this.width.getValue(); - } - - @Override - public int height() { - int result = LibVlc.libvlc_video_get_size(this.raw, 0, this.width, this.height); - if (result != 0) - return 0; - - return this.height.getValue(); - } - - @Override - public boolean start() { - return false; - } - - @Override - public boolean startPaused() { - return false; - } - - @Override - public boolean resume() { - return false; - } - - @Override - public boolean pause() { - return false; - } - - @Override - public boolean pause(boolean paused) { - return false; - } - - @Override - public boolean stop() { - return false; - } - - @Override - public boolean togglePlay() { - return false; - } - - @Override - public boolean seek(long time) { - return false; - } - - @Override - public boolean seekQuick(long time) { - return false; - } - - @Override - public boolean foward() { - return false; - } - - @Override - public boolean rewind() { - return false; - } - - @Override - public boolean speed(float speed) { - return false; - } - - @Override - public boolean repeat() { - return false; - } - - @Override - public boolean repeat(boolean repeat) { - return false; - } - - @Override - public boolean usable() { - return false; - } - - @Override - public boolean loading() { - return false; - } - - @Override - public boolean buffering() { - return false; - } - - @Override - public boolean ready() { - return false; - } - - @Override - public boolean paused() { - return false; - } - - @Override - public boolean playing() { - return false; - } - - @Override - public boolean stopped() { - return false; - } - - @Override - public boolean ended() { - return false; - } - - @Override - public boolean validSource() { - return false; - } - - @Override - public boolean liveSource() { - return false; - } - - @Override - public boolean canSeek() { - return false; - } - - @Override - public long duration() { - return 0; - } - - @Override - public long time() { - return 0; - } - - @Override - public void release() { - - } - - @Override - public int texture() { - return 0; - } -} diff --git a/src/main/java/org/watermedia/api/media/VideoSource.java b/src/main/java/org/watermedia/api/media/VideoSource.java deleted file mode 100644 index 570fa82a..00000000 --- a/src/main/java/org/watermedia/api/media/VideoSource.java +++ /dev/null @@ -1,152 +0,0 @@ -package org.watermedia.api.media; - -public class VideoSource extends MediaSource { - public VideoSource(int width, int height) { - super(width, height); - } - - @Override - public boolean start() { - return false; - } - - @Override - public boolean startPaused() { - return false; - } - - @Override - public boolean resume() { - return false; - } - - @Override - public boolean pause() { - return false; - } - - @Override - public boolean pause(boolean paused) { - return false; - } - - @Override - public boolean stop() { - return false; - } - - @Override - public boolean togglePlay() { - return false; - } - - @Override - public boolean seek(long time) { - return false; - } - - @Override - public boolean seekQuick(long time) { - return false; - } - - @Override - public boolean foward() { - return false; - } - - @Override - public boolean rewind() { - return false; - } - - @Override - public boolean speed(float speed) { - return false; - } - - @Override - public boolean repeat() { - return false; - } - - @Override - public boolean repeat(boolean repeat) { - return false; - } - - @Override - public boolean usable() { - return false; - } - - @Override - public boolean loading() { - return false; - } - - @Override - public boolean buffering() { - return false; - } - - @Override - public boolean ready() { - return false; - } - - @Override - public boolean paused() { - return false; - } - - @Override - public boolean playing() { - return false; - } - - @Override - public boolean stopped() { - return false; - } - - @Override - public boolean ended() { - return false; - } - - @Override - public boolean validSource() { - return false; - } - - @Override - public boolean liveSource() { - return false; - } - - @Override - public boolean canSeek() { - return false; - } - - @Override - public long duration() { - return 0; - } - - @Override - public long time() { - return 0; - } - - @Override - public void release() { - - } - - @Override - public int texture() { - return 0; - } -} diff --git a/src/main/java/org/watermedia/api/media/meta/MediaQuality.java b/src/main/java/org/watermedia/api/media/meta/MediaQuality.java deleted file mode 100644 index d8bd4c85..00000000 --- a/src/main/java/org/watermedia/api/media/meta/MediaQuality.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.watermedia.api.media.meta; - -/** - * Quality preference. - */ -public enum MediaQuality { - /** - * Qualities same or below 240p threshold - */ - LOWEST(240), - - /** - * Qualities same or below 480p threshold - */ - LOWER(480), - - /** - * Qualities below 540p threshold - */ - LOW(540), - - /** - * Qualities same or below 720p threshold - */ - AVERAGE(720), - - /** - * Qualities same or below 1080p threshold - */ - HIGH(1080), - - /** - * Qualities same or below 2K threshold - */ - HIGHER(1440), - - /** - * Qualities same or below 4K threshold - */ - HIGHEST(2160); - - private final int threadshool; - MediaQuality(int threshold) { - this.threadshool = threshold; - } - - public static final MediaQuality[] VALUES = values(); - - public static MediaQuality calculate(int width) { // TODO: evaluate height for tiktok reels - if (width >= LOWEST.threadshool && width < LOWER.threadshool) { - return LOWEST; - } else if (width >= LOWER.threadshool && width < LOW.threadshool) { - return LOWER; - } else if (width >= LOW.threadshool && width < AVERAGE.threadshool) { - return LOW; - } else if (width >= AVERAGE.threadshool && width < HIGH.threadshool) { - return AVERAGE; - } else if (width >= HIGH.threadshool && width < HIGHER.threadshool) { - return HIGH; - } else if (width >= HIGHER.threadshool && width < HIGHEST.threadshool) { - return HIGHER; - } else { - return HIGHEST; - } - } - - public MediaQuality getNext() { - var ordinal = this.ordinal(); - if (ordinal >= VALUES.length) { - return null; - } - return VALUES[ordinal]; - } - - public MediaQuality getBack() { - var ordinal = this.ordinal(); - if (ordinal == 0) { - return null; - } - return VALUES[ordinal]; - } -} diff --git a/src/main/java/org/watermedia/api/media/meta/MediaType.java b/src/main/java/org/watermedia/api/media/meta/MediaType.java deleted file mode 100644 index 1b2f9680..00000000 --- a/src/main/java/org/watermedia/api/media/meta/MediaType.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.watermedia.api.media.meta; - -public enum MediaType { - IMAGE, - AUDIO, - VIDEO, - SUBTITLES, - UNKNOWN; - - public static MediaType getByMimetype(String mimetype) { - String[] mm = mimetype.split("/"); - String type = mm[0].toUpperCase(); - String format = mm.length == 1 ? null : mm[1].toLowerCase(); - - return switch (type) { - case "VIDEO" -> VIDEO; - case "AUDIO" -> AUDIO; - case "TEXT" -> format != null && (format.equals("str") || format.equals("plain")) ? SUBTITLES : UNKNOWN; - default -> UNKNOWN; - }; - } -} diff --git a/src/main/java/org/watermedia/api/network/MRL.java b/src/main/java/org/watermedia/api/network/MRL.java deleted file mode 100644 index 778b3ab4..00000000 --- a/src/main/java/org/watermedia/api/network/MRL.java +++ /dev/null @@ -1,273 +0,0 @@ -package org.watermedia.api.network; - -import me.srrapero720.watermedia.api.MediaContext; -import org.watermedia.api.media.meta.MediaQuality; -import org.watermedia.api.media.meta.MediaType; - -import java.io.File; -import java.io.Serializable; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLConnection; -import java.util.*; -import java.util.function.Function; - -public class MRL implements Comparable, Serializable { - private static final Map MEDIA_URIS = new HashMap<>(); - - public static MRL get(File file) { return get(file.toURI()); } - public static MRL get(URI uri) { return MEDIA_URIS.computeIfAbsent(uri, MRL::new); } - public static MRL get(String url) { - try { - return MEDIA_URIS.computeIfAbsent(new URI(url), MRL::get); - } catch (URISyntaxException e) { - throw new IllegalArgumentException("URL is not valid '" + url + "'", e); - } - } - - // instance - private final URI uri; - private final List sources = new ArrayList<>(); - private final List usages = new ArrayList<>(); - private Metadata metadata; - private long expires; - private boolean patched; - - private MRL(URI uri) { - this.uri = uri; - this.sources.add(new Source(uri)); - } - - public MRL addUsage(MediaContext context) { - this.usages.add(context); - return this; - } - - public MRL removeUsage(MediaContext context) { - this.usages.remove(context); - return this; - } - - public int usages() { - return this.usages.size(); - } - - public boolean hasUsages() { - return !this.usages.isEmpty(); - } - - public URI getUri() { - return uri; - } - - public Source[] getSources() { - return sources.toArray(new Source[0]); - } - - public boolean patched() { - return patched; - } - - public void apply(Patch patch) { - this.sources.clear(); - this.sources.addAll(patch.sources); - this.metadata = patch.metadata; - this.patched = true; - } - - public int size() { - return sources.size(); - } - - @Override - public int compareTo(URI o) { - return o.compareTo(this.uri); - } - - public static class Source { - private final URI uri; - private final List slaves; - private final Map qualities; - private URLConnection connection; - private MediaType type; - - private URI fallbackUri; - private MediaType fallbackType; - private boolean live; - - public Source(URI uri) { - this.uri = uri; - this.slaves = new ArrayList<>(); - this.qualities = new HashMap<>(); - } - - public Source(URI uri, List slaves, Map qualities) { - this.uri = uri; - this.slaves = slaves; - this.qualities = qualities; - } - - public URI fallbackUri() { - return this.fallbackUri; - } - - public boolean live() { - return this.live; - } - - public int size() { - return qualities.isEmpty() ? 1 : qualities.size(); - } - - public URI uri(MediaContext context, MediaQuality quality) { - if (qualities.isEmpty()) return this.uri; - - URI uri = qualities.get(quality); - MediaQuality currentQuality = context.preferLowerQuality() ? quality.getBack() : quality.getNext(); - while (uri == null && currentQuality != null) { - uri = qualities.get(currentQuality); - currentQuality = context.preferLowerQuality() ? currentQuality.getBack() : currentQuality.getNext(); - } - return uri == null ? this.uri : uri; - } - - public URI highQualityUri() { - if (qualities.isEmpty()) return this.uri; - - URI uri = qualities.get(MediaQuality.HIGHEST); - MediaQuality currentQuality = MediaQuality.HIGH; - while (uri == null && currentQuality != null) { - uri = qualities.get(currentQuality); - currentQuality = currentQuality.getBack(); - } - - return uri == null ? this.uri : uri; - } - - public URI lowerQualityUri() { - if (qualities.isEmpty()) return this.uri; - - URI uri = qualities.get(MediaQuality.LOWEST); - MediaQuality currentQuality = MediaQuality.LOW; - while (uri == null && currentQuality != null) { - uri = qualities.get(currentQuality); - currentQuality = currentQuality.getNext(); - } - - return uri == null ? this.uri : uri; - } - - public Slave[] slaves() { - return slaves.stream().filter(slave -> slave.type == MediaType.AUDIO || slave.type == MediaType.SUBTITLES).toArray(v -> new Slave[0]); - } - - @Override - public String toString() { - return "Source{" + - "source=" + uri + - ", slaves=" + Arrays.toString(slaves.toArray(new Slave[0])) + - ", qualities=" + Arrays.toString(qualities.values().toArray(new URI[0])) + - ", type=" + type + - ", fallbackUri=" + fallbackUri + - ", fallbackType=" + fallbackType + - ", live=" + live + - '}'; - } - } - - public record Slave(MediaType type, URI slave) {} - - public record Metadata(String name, String author, String platform, String description, URI thumbnailURI, long duration) { - - } - - public static class Patch { - private final List sources = new ArrayList<>(); - private Metadata metadata; - - - public Patch setMetadata(Metadata metadata) { - this.metadata = metadata; - return this; - } - - public SourceBuilder addSource() { - return new SourceBuilder(); - } - - public class SourceBuilder { - private URI uri; - private MediaType type; - private MediaType fallbackType; - private URI fallbackUri; - private boolean isLive; - private final Map qualities = new HashMap<>(); - private final List slaves = new ArrayList<>(); - - private SourceBuilder() {} - - public SourceBuilder addSlave(Slave slave) { - this.slaves.add(slave); - return this; - } - - public SourceBuilder setUri(URI uri) { - this.uri = uri; - return this; - } - - public SourceBuilder setFallbackUri(URI uri) { - this.fallbackUri = uri; - return this; - } - - public SourceBuilder setIsLive(boolean live) { - this.isLive = live; - return this; - } - - public SourceBuilder setType(MediaType type) { - this.type = type; - return this; - } - - public SourceBuilder setFallbackType(MediaType fallbackType) { - this.fallbackType = fallbackType; - return this; - } - - public SourceBuilder putQuality(MediaQuality quality, URI uri) { - this.qualities.put(quality, uri); - return this; - } - - public SourceBuilder putQualityIfAbsent(MediaQuality quality, Function uri) { - this.qualities.computeIfAbsent(quality, uri); - return this; - } - - public SourceBuilder putQualityIfAbsent(MediaQuality quality, URI uri) { - this.qualities.computeIfAbsent(quality, q -> uri); - return this; - } - - public Patch build() { - if ((uri == null && qualities.isEmpty())) - throw new IllegalStateException("Uri is null and qualities is empty"); - - if (uri == null) { - uri = qualities.values().toArray(o -> new URI[0])[0]; - } - - var source = new Source(uri, slaves, qualities); - source.fallbackUri = this.fallbackUri; - source.live = this.isLive; - source.type = this.type; - source.fallbackType = this.fallbackType; - - Patch.this.sources.add(source); - return Patch.this; - } - } - } -} diff --git a/src/main/java/org/watermedia/core/NetworkCore.java b/src/main/java/org/watermedia/core/NetworkCore.java deleted file mode 100644 index 3f3b2151..00000000 --- a/src/main/java/org/watermedia/core/NetworkCore.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.watermedia.core; - -import me.srrapero720.watermedia.api.MediaContext; -import org.watermedia.api.network.MRL; -import org.watermedia.core.network.patchs.AbstractPatch; - -import java.util.ServiceLoader; - -public class NetworkCore extends WaterMediaCore { - private static final ServiceLoader PATCHES = ServiceLoader.load(AbstractPatch.class); - - public void patch(MediaContext context, MRL media) { - - } -} diff --git a/src/main/java/org/watermedia/core/WaterMediaCore.java b/src/main/java/org/watermedia/core/WaterMediaCore.java deleted file mode 100644 index fac06beb..00000000 --- a/src/main/java/org/watermedia/core/WaterMediaCore.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.watermedia.core; - -public abstract class WaterMediaCore { -} diff --git a/src/main/java/org/watermedia/core/network/NetworkRequest.java b/src/main/java/org/watermedia/core/network/NetworkRequest.java deleted file mode 100644 index cc5fee1b..00000000 --- a/src/main/java/org/watermedia/core/network/NetworkRequest.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.watermedia.core.network; - -import org.apache.logging.log4j.Marker; -import org.apache.logging.log4j.MarkerManager; -import org.watermedia.tools.ThreadTool; - -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -public class NetworkRequest implements Runnable { - private static final Marker IT = MarkerManager.getMarker("ImageAPI"); - private static final DateFormat FORMAT = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z"); - private static final ExecutorService EXECUTOR = Executors.newScheduledThreadPool(ThreadTool.minThreads(), ThreadTool.factory("ImageFetch-Worker", Thread.NORM_PRIORITY + 1)); - - - public NetworkRequest() { - - } - - - public void start() { - EXECUTOR.execute(this); - } - - @Override - public void run() { - - } - - - - -} diff --git a/src/main/java/org/watermedia/core/network/patchs/AbstractPatch.java b/src/main/java/org/watermedia/core/network/patchs/AbstractPatch.java deleted file mode 100644 index ea3c6562..00000000 --- a/src/main/java/org/watermedia/core/network/patchs/AbstractPatch.java +++ /dev/null @@ -1,109 +0,0 @@ -package org.watermedia.core.network.patchs; - -import me.srrapero720.watermedia.api.MediaContext; -import org.watermedia.WaterMedia; -import org.watermedia.api.network.MRL; -import org.watermedia.tools.NetTool; - -import java.net.URI; - -/** - * Service class - * - */ -public abstract class AbstractPatch { - - /** - * Get the name patch name - * - * @return class name by default - */ - public String name() { - return this.getClass().getSimpleName(); - } - - /** - * Platform brand name of the patcher - * - * @return brand name. e.j: Twitter (X) - */ - public abstract String platform(); - - /** - * Get the valid user agent for this source - * - * @return WaterMedia user agent by default - */ - public String userAgent() { - return WaterMedia.USER_AGENT; - } - - /** - * Provides the active status for the patch in case patch has any temporal restriction or runs out for - * custom online validation on bootstrap - */ - public abstract boolean active(MediaContext context); - - /** - * Validates if the {@link MRL} can be processed by this patcher - * - * @param source MediaSource instance to be patched. - * @see MRL - * @see MRL#get(URI) - */ - public abstract boolean validate(MRL source); - - /** - * Patches the provided MediaSource - * - * @param source URL to patch - * @throws PatchException if URL is null or invalid in this patch - */ - public abstract void patch(MediaContext context, MRL source) throws PatchException; - - /** - * Executes a patch test, validating patch is working and up-to-date - * Not working patches are not added on the patches registry - * - * @param url media url, preferred an HTTP url - * @param context test context - */ - public abstract void test(MediaContext context, String url); - - /** - * Returns the name of the Fixer - * - * @return redirect call to {@link #name()} - * @see #name() - */ - @Override - public String toString() { - return name(); - } - - public static class PatchException extends Exception { - public PatchException(String uri, String message) { - super("Failed to patch URI '" + uri + "'; " + message); - } - - public PatchException(String uri, Exception e) { - super("Failed to patch URI '" + uri + "'; " + e.getLocalizedMessage(), e); - } - - public PatchException(URI source, String message) { - this(source.toString(), message); - } - - public PatchException(URI source, Exception message) { - this(source.toString(), message); - } - - public PatchException(MRL source, String message) { - this(source.getUri(), message); - } - - public PatchException(MRL source, Exception e) { - this(source.getUri(), e); - } - } -} \ No newline at end of file diff --git a/src/main/java/org/watermedia/core/network/patchs/DrivePatch.java b/src/main/java/org/watermedia/core/network/patchs/DrivePatch.java deleted file mode 100644 index ef64c0ec..00000000 --- a/src/main/java/org/watermedia/core/network/patchs/DrivePatch.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.watermedia.core.network.patchs; - -import me.srrapero720.watermedia.api.MediaContext; -import org.watermedia.api.network.MRL; - -import java.net.URI; - -public class DrivePatch extends AbstractPatch { - private static final String API_KEY = "AIzaSyBiFNT6TTo506kCYYwA2NHqs36TlXC1DMo"; // Get your own api key, is easy and free. - private static final String API_URL = "https://www.googleapis.com/drive/v3/files/%s?alt=media&key=%s"; - private static final String PATH_PREFIX = "/file/d/"; - private static final String PATH_SUFFIX = "/view"; - - @Override - public String platform() { - return "Google Drive"; - } - - @Override - public boolean active(MediaContext context) { - return true; - } - - @Override - public boolean validate(MRL source) { - var host = source.getUri().getHost(); - var path = source.getUri().getPath(); - return host.equals("drive.google.com") && path.startsWith("/file/d/"); - } - - @Override - public void patch(MediaContext context, MRL source) throws PatchException { - var uri = source.getUri(); - var path = uri.getPath(); - - var pre = path.substring(PATH_PREFIX.length()); - var fileId = pre.substring(0, pre.length() - PATH_SUFFIX.length()); - - try { - // PATCH BUILDING - var patch = new MRL.Patch(); - patch.addSource() - .setUri(new URI(String.format(API_URL, fileId, API_KEY))) - .build(); - source.apply(patch); - } catch (Exception e) { - throw new PatchException(source, e); - } - } - - @Override - public void test(MediaContext context, String url) { - - } -} diff --git a/src/main/java/org/watermedia/core/network/patchs/DropboxPatch.java b/src/main/java/org/watermedia/core/network/patchs/DropboxPatch.java deleted file mode 100644 index 73f311d9..00000000 --- a/src/main/java/org/watermedia/core/network/patchs/DropboxPatch.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.watermedia.core.network.patchs; - -import me.srrapero720.watermedia.api.MediaContext; -import org.watermedia.api.network.MRL; - -import java.net.URI; - -public class DropboxPatch extends AbstractPatch { - @Override - public String platform() { - return "Dropbox"; - } - - @Override - public boolean active(MediaContext context) { - return true; - } - - @Override - public boolean validate(MRL source) { - var host = source.getUri().getHost(); - var query = source.getUri().getQuery(); - return host.contains("dropbox.com") && query.equals("dl=0"); - } - - @Override - public void patch(MediaContext context, MRL source) throws PatchException { - var url = source.getUri().toString(); - try { - var r = url.replace("dl=0", "dl=1"); - var uri = new URI(r); - - source.apply(new MRL.Patch() - .addSource() - .setUri(uri) - .build() - ); - } catch (Exception e) { - throw new PatchException(source, e); - } - } - - @Override - public void test(MediaContext context, String url) { - - } -} diff --git a/src/main/java/org/watermedia/core/network/patchs/ImgurPatch.java b/src/main/java/org/watermedia/core/network/patchs/ImgurPatch.java deleted file mode 100644 index 7f24c89f..00000000 --- a/src/main/java/org/watermedia/core/network/patchs/ImgurPatch.java +++ /dev/null @@ -1,315 +0,0 @@ -package org.watermedia.core.network.patchs; - -import com.google.gson.annotations.SerializedName; -import me.srrapero720.watermedia.api.MediaContext; -import org.watermedia.api.media.meta.MediaType; -import org.watermedia.api.network.MRL; -import org.watermedia.tools.DataTool; -import org.watermedia.tools.NetTool; - -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.text.SimpleDateFormat; -import java.util.Date; - -public class ImgurPatch extends AbstractPatch { - private static final String API_URL = "https://api.imgur.com/3"; - private static final String API_KEY = "685cdf74b1229b9"; - - private static final String IMAGE_URL = API_URL + "/image/%s?client_id=" + API_KEY; - private static final String GALLERY_URL = API_URL + "/gallery/%s?client_id=" + API_KEY; - private static final String TAG_GALLERY_URL = API_URL + "/gallery/t/%s/%s?client_id=" + API_KEY; - - @Override - public String platform() { - return "Imgur"; - } - - @Override - public boolean active(MediaContext context) { - return true; - } - - @Override - public boolean validate(MRL source) { - return source.getUri().getHost().equals("imgur.com"); // i.imgur.com - doesn't need patches - } - - @Override - public void patch(MediaContext context, MRL source) throws PatchException { - final var path = source.getUri().getPath(); - final var fragment = source.getUri().getFragment(); - - // SPECIAL PARSE - final boolean gallery = path.startsWith("/gallery/") || path.startsWith("/a/"); - final boolean tagGallery = fragment != null && fragment.contains("#/t/"); - - final var pathSplit = path.substring(1).split("/"); // remove first slash and get the path splitted - final var idSplit = pathSplit[1].split("-"); // i hope imgur do their job and IDs don't contains "-" - - final var tag = fragment != null ? fragment.substring("#/t/".length()) : null; - final var id = idSplit[idSplit.length - 1]; - - try { - if (gallery) { // open it as a gallery - final var url = tagGallery ? String.format(TAG_GALLERY_URL, tag, id) : String.format(GALLERY_URL, id); - final Response res = DataTool.fromJSON(connectResponse(url), Response.class); - - if (res.data == null || res.data.images.length == 0) - throw new NullPointerException("Response is successfully but data is null or images are empty"); - - // METADATA BUILDING - var metadata = new MRL.Metadata( - res.data.title, - res.data.accountUrl, - this.platform(), - DataTool.orElse(res.data.desc, res.data.images[0].desc), - null, - 0 - ); - - // PATCH BUILDING - var patch = new MRL.Patch(); - for (Image image: res.data.images) { - patch.addSource() - .setUri(new URI(image.link)) - .setType(MediaType.getByMimetype(image.type)) - .build(); - } - - patch.setMetadata(metadata); - source.apply(patch); - - } else { // assume is a simple image - final var url = String.format(IMAGE_URL, id); - final Response res = DataTool.fromJSON(connectResponse(url), Response.class); - - if (res.data == null) - throw new NullPointerException("Response is successfully but data is null or images are empty"); - - // METADATA BUILDING - var metadata = new MRL.Metadata( - res.data.title, - res.data.accountUrl, - this.platform(), - res.data.desc, - null, - 0 - ); - - // PATCH BUILDING - var patch = new MRL.Patch(); - patch.addSource() - .setUri(new URI(res.data.link)) - .setType(MediaType.getByMimetype(res.data.type)) - .build(); - patch.setMetadata(metadata); - - source.apply(patch); - } - } catch (Exception e) { - throw new PatchException(source, e); - } - } - - @Override - public void test(MediaContext context, String url) { - - } - - public String connectResponse(String url) throws IOException { - HttpURLConnection conn = NetTool.connect(url, "GET"); - int code = conn.getResponseCode(); - switch (code) { - case HttpURLConnection.HTTP_NOT_FOUND -> throw new NullPointerException("Image or gallery not founded"); - case HttpURLConnection.HTTP_FORBIDDEN, HttpURLConnection.HTTP_UNAUTHORIZED -> throw new UnsupportedOperationException("Access denied by Imgur"); - default -> { - if (code != HttpURLConnection.HTTP_OK) - throw new UnsupportedOperationException("Imgur responses with a unexpected status code: " + code); - } - } - - try(InputStream in = conn.getInputStream()) { - return new String(DataTool.readAllBytes(in), StandardCharsets.UTF_8); - } finally { - conn.disconnect(); - } - } - - public static class Gallery { - - @SerializedName("id") - public String id; - - @SerializedName("title") - public String title; - - @SerializedName("description") - public String desc; - - @SerializedName("datetime") - public long date; - - @SerializedName("cover") - public String cover; - - @SerializedName("account_url") - public String accountUrl; - - @SerializedName("account_id") - public int accountId; - - @SerializedName("privacy") - public String privacy; - - @SerializedName("layout") - public String layout; - - @SerializedName("views") - public int views; - - @SerializedName("link") - public String link; - - @SerializedName("ups") - public int ups; - - @SerializedName("downs") - public int downs; - - @SerializedName("points") - public int points; - - @SerializedName("score") - public int score; - - @SerializedName("is_album") - public boolean album; - - @SerializedName("vote") - public String vote; - - @SerializedName("comment_count") - public int commentCount; - - @SerializedName("images_count") - public int imagesCount; - - @SerializedName("images") - public Image[] images; - - public String getFormattedDate() { - Date time = new Date(date * 1000); - return new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(time); - } - } - - public static class Image { - - @SerializedName("id") - public String id; - - @SerializedName("deletehash") - public String deleteHash; - - @SerializedName("account_id") - public int accountId; - - @SerializedName("account_url") - public String accountUrl; - - @SerializedName("ad_type") - public int adType; - - @SerializedName("ad_url") - public String adUrl; - - @SerializedName("title") - public String title; - - @SerializedName("description") - public String desc; - - @SerializedName("name") - public String name; - - @SerializedName("type") - public String type; - - @SerializedName("width") - public int width; - - @SerializedName("height") - public int height; - - @SerializedName("size") - public int size; - - @SerializedName("views") - public int views; - - @SerializedName("section") - public String section; - - @SerializedName("vote") - public String vote; - - @SerializedName("bandwidth") - public long bandwidth; - - @SerializedName("animated") - public boolean anim; - - @SerializedName("favorite") - public boolean fav; - - @SerializedName("in_gallery") - public boolean gallery; - - @SerializedName("in_most_viral") - public boolean viral; - - @SerializedName("has_sound") - public boolean sound; - - @SerializedName("is_ad") - public boolean ad; - - @SerializedName("nsfw") - public String nsfw; - - @SerializedName("link") - public String link; - - // @SerializedName("tags") - // public Object[] tags; - - @SerializedName("datetime") - public long date; - - @SerializedName("mp4") - public String mp4; - - @SerializedName("hls") - public String hls; - - public String getFormattedDate() { - Date time = new Date(date * 1000); - return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(time); - } - } - - public static class Response { - @SerializedName("data") - public T data; - - @SerializedName("success") - public boolean success; - - @SerializedName("status") - public int status; - } -} diff --git a/src/main/java/org/watermedia/core/network/patchs/KickPatch.java b/src/main/java/org/watermedia/core/network/patchs/KickPatch.java deleted file mode 100644 index 271c9c47..00000000 --- a/src/main/java/org/watermedia/core/network/patchs/KickPatch.java +++ /dev/null @@ -1,281 +0,0 @@ -package org.watermedia.core.network.patchs; - -import com.google.gson.annotations.Expose; -import com.google.gson.annotations.SerializedName; -import me.srrapero720.watermedia.api.MediaContext; -import org.watermedia.api.media.meta.MediaType; -import org.watermedia.api.network.MRL; -import org.watermedia.tools.DataTool; -import org.watermedia.tools.NetTool; - -import java.io.IOException; -import java.io.InputStream; -import java.io.Serializable; -import java.net.HttpURLConnection; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; - -import static org.watermedia.WaterMedia.LOGGER; - -public class KickPatch extends AbstractPatch { - private static final String API_URL = "https://kick.com/api/v1/"; - - @Override - public String platform() { - return "Kick"; - } - - @Override - public boolean active(MediaContext context) { - return true; - } - - @Override - public boolean validate(MRL source) { - return source.getUri().getHost().equals("kick.com"); - } - - @Override - public void patch(MediaContext context, MRL source) throws PatchException { - var path = source.getUri().getPath(); - - try { - if (path.contains("/video/")) { - String[] pathSplit = path.split("/"); - String videoId = pathSplit[pathSplit.length - 1]; - Video video = DataTool.fromJSON(this.connectToKick(API_URL + "video/" + videoId), Video.class); - - // METADATA BUILDING - var metadata = new MRL.Metadata( - video.livestream.title, - video.livestream.channel.name, - this.platform(), - "", - new URI(video.livestream.thumbnail), - video.livestream.duration - ); - - // PATCH BUILDING - var patch = new MRL.Patch(); - patch.setMetadata(metadata); - - // PATCH APPLY - source.apply(patch.addSource() - .setUri(new URI(video.url)) - .setIsLive(false) - .setType(MediaType.VIDEO) - // TODO: put offline banner as fallback uri. - // .setFallbackUri(new URI(video.livestream.thumbnail)) - // .setFallbackType(MediaType.IMAGE) - .build() - ); - } else { - String channelId = path.substring(1); - Channel ch = DataTool.fromJSON(this.connectToKick(API_URL + "channels/" + channelId), Channel.class); - if (ch.livestream == null || !ch.livestream.is_live) { - LOGGER.warn("Streamer {} is not online", ch.slug); - } - - // METADATA BUILDING - var metadata = new MRL.Metadata( - ch.livestream.title, - ch.user.username, - this.platform(), - ch.livestream.slug, - ch.offline_banner.getSrcset()[0], - ch.livestream.duration - ); - - // PATCH BUILDING - var patch = new MRL.Patch(); - patch.setMetadata(metadata); - - // PATCH APPLY - source.apply(patch.addSource() - .setUri(new URI(ch.url)) - .setIsLive(true) - .setType(MediaType.VIDEO) - .setFallbackUri(ch.offline_banner.getSrcset()[0]) - .build() - ); - } - } catch (Exception e) { - throw new PatchException(source, e); - } - } - - @Override - public void test(MediaContext context, String url) { - - } - - public String connectToKick(String url) throws IOException { - HttpURLConnection conn = NetTool.connect(url, "GET"); - int code = conn.getResponseCode(); - - switch (code) { - case HttpURLConnection.HTTP_NOT_FOUND -> throw new NullPointerException("Streamer or Vod was not found"); - case HttpURLConnection.HTTP_FORBIDDEN, HttpURLConnection.HTTP_UNAUTHORIZED -> throw new UnsupportedOperationException("Access denied by Kick"); - default -> { - if (code != HttpURLConnection.HTTP_OK) - throw new UnsupportedOperationException("Kick responses with a unexpected status code: " + code); - } - } - - try (InputStream in = conn.getInputStream()) { - return new String(DataTool.readAllBytes(in), StandardCharsets.UTF_8); - } finally { - conn.disconnect(); - } - } - - public static class Channel implements Serializable { - - @SerializedName("id") - @Expose - public int id; - - @SerializedName("user_id") - @Expose - public int userId; - - @SerializedName("slug") - @Expose - public String slug; - - @SerializedName("playback_url") - @Expose - public String url; - - @SerializedName("livestream") - @Expose - public Livestream livestream; - - @SerializedName("user") - @Expose - public User user; - - @SerializedName("offline_banner_image") - @Expose - public OfflineBanner offline_banner; - - public static class Livestream implements Serializable { - @SerializedName("id") - @Expose - public int id; - - @SerializedName("slug") - @Expose - public String slug; - - @SerializedName("is_live") - @Expose - public boolean is_live; - - @SerializedName("duration") - @Expose - public long duration; - - @SerializedName("session_title") - @Expose - public String title; - } - - public static class OfflineBanner implements Serializable { - - @SerializedName("src") - @Expose - public String src; - - @SerializedName("srcset") - @Expose - public String srcset; - - public URI[] getSrcset() throws URISyntaxException { - String[] urls = srcset.split(" "); - URI[] result = new URI[urls.length / 2]; - - for (int i = 0, j = 0; i < urls.length; i = i + 2, j++) { - result[j] = new URI(urls[i]); - } - - return result; - } - } - } - - public static class User { - - @SerializedName("username") - @Expose - public String username; - - @SerializedName("profile_pic") - @Expose - public String profile_pic; - } - - public static class Video implements Serializable { - @SerializedName("id") - @Expose - public int id; - - @SerializedName("slug") - @Expose - public int name; - - @SerializedName("live_stream_id") - @Expose - public int streamId; - - @SerializedName("source") - @Expose - public String url; - - @SerializedName("livestream") - @Expose - public Livestream livestream; - - - public static class Livestream { - @SerializedName("id") - @Expose - public int id; - - @SerializedName("slug") - @Expose - public String slug; - - @SerializedName("session_title") - @Expose - public String title; - - @SerializedName("is_live") - @Expose - public boolean is_live; - - @SerializedName("duration") - @Expose - public long duration; - - @SerializedName("channel") - @Expose - public Channel channel; - - @SerializedName("thumbnail") - @Expose - public String thumbnail; - - public static class Channel { - @SerializedName("id") - @Expose - public int id; - - @SerializedName("slug") - @Expose - public String name; - } - } - } -} \ No newline at end of file diff --git a/src/main/java/org/watermedia/core/network/patchs/LightshotPatch.java b/src/main/java/org/watermedia/core/network/patchs/LightshotPatch.java deleted file mode 100644 index 4c47d59f..00000000 --- a/src/main/java/org/watermedia/core/network/patchs/LightshotPatch.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.watermedia.core.network.patchs; - -import me.srrapero720.watermedia.api.MediaContext; -import org.watermedia.api.media.meta.MediaType; -import org.watermedia.api.network.MRL; -import org.watermedia.tools.DataTool; -import org.watermedia.tools.NetTool; - -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.util.regex.Pattern; - -public class LightshotPatch extends AbstractPatch { - private static final Pattern HTML_PATTERN = Pattern.compile("]*class=\"no-click screenshot-image\"[^>]*src=\"(https://[^\"]+)\""); - - @Override - public String platform() { - return "Lightshot"; - } - - @Override - public boolean active(MediaContext context) { - return true; - } - - @Override - public boolean validate(MRL source) { - var host = source.getUri().getHost(); - return host.equals("prnt.sc"); - } - - @Override - public void patch(MediaContext context, MRL source) throws PatchException { - try { - var html = connectToLightshot(source.getUri()); - var matcher = HTML_PATTERN.matcher(html); - - if (!matcher.find()) throw new NullPointerException("No match found"); - - source.apply(new MRL.Patch() - .addSource() - .setUri(new URI(matcher.group(1))) - .setType(MediaType.IMAGE) - .build() - ); - } catch (Exception e) { - throw new PatchException(source, e); - } - } - - @Override - public void test(MediaContext context, String url) { - - } - - public String connectToLightshot(URI url) throws IOException { - HttpURLConnection conn = NetTool.connect(url, "GET"); - int code = conn.getResponseCode(); - - switch (code) { - case HttpURLConnection.HTTP_NOT_FOUND -> throw new NullPointerException("Image was not found"); - case HttpURLConnection.HTTP_FORBIDDEN, HttpURLConnection.HTTP_UNAUTHORIZED -> throw new UnsupportedOperationException("Access denied by Lightshot"); - default -> { - if (code != HttpURLConnection.HTTP_OK) - throw new UnsupportedOperationException("Lightshot responses with a unexpected status code: " + code); - } - } - - try (InputStream in = conn.getInputStream()) { - return new String(DataTool.readAllBytes(in), StandardCharsets.UTF_8); - } finally { - conn.disconnect(); - } - } -} diff --git a/src/main/java/org/watermedia/core/network/patchs/OneDrivePatch.java b/src/main/java/org/watermedia/core/network/patchs/OneDrivePatch.java deleted file mode 100644 index febd33e7..00000000 --- a/src/main/java/org/watermedia/core/network/patchs/OneDrivePatch.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.watermedia.core.network.patchs; - -import com.google.gson.annotations.SerializedName; -import me.srrapero720.watermedia.api.MediaContext; -import org.watermedia.api.network.MRL; - -public class OneDrivePatch extends AbstractPatch { - - @Override - public String platform() { - return ""; - } - - @Override - public boolean active(MediaContext context) { - return false; - } - - @Override - public boolean validate(MRL source) { - return false; - } - - @Override - public void patch(MediaContext context, MRL source) throws PatchException { - - } - - @Override - public void test(MediaContext context, String url) { - - } - - public static class Item { - private String id; - private String name; - private long size; - @SerializedName("@content.downloadUrl") - private String url; - - public Item() {} - - public Item(String id, String name, long size, String url) { - this.id = id; - this.name = name; - this.size = size; - this.url = url; - } - - public String getId() { - return id; - } - - public String getName() { - return name; - } - - public long getSize() { - return size; - } - - public String getUrl() { - return url; - } - } -} diff --git a/src/main/java/org/watermedia/core/network/patchs/StreamablePatch.java b/src/main/java/org/watermedia/core/network/patchs/StreamablePatch.java deleted file mode 100644 index 0c0eb5c6..00000000 --- a/src/main/java/org/watermedia/core/network/patchs/StreamablePatch.java +++ /dev/null @@ -1,142 +0,0 @@ -package org.watermedia.core.network.patchs; - -import com.google.gson.annotations.Expose; -import com.google.gson.annotations.SerializedName; -import me.srrapero720.watermedia.api.MediaContext; -import org.watermedia.api.media.meta.MediaType; -import org.watermedia.api.network.MRL; -import org.watermedia.tools.DataTool; -import org.watermedia.tools.NetTool; - -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URI; - -import static java.net.HttpURLConnection.*; - -public class StreamablePatch extends AbstractPatch { - private static final String API_URL = "https://api.streamable.com/videos/"; - - @Override - public String platform() { - return "Streamable"; - } - - @Override - public boolean validate(MRL source) { - return source.getUri().getHost().equals("streamable.com"); - } - - @Override - public MRL patch(MRL source, MediaContext context) throws PatchException { - String videoId = source.getUri().getPath().substring(1); - try { - HttpURLConnection conn = NetTool.connect(API_URL + videoId, "GET"); - int code = conn.getResponseCode(); - switch (code) { - case HTTP_NOT_FOUND -> throw new NullPointerException("Video doesn't exists"); - case HTTP_FORBIDDEN, HTTP_UNAUTHORIZED -> throw new NullPointerException("Video isn't public or streamable denied us the access"); - case HTTP_INTERNAL_ERROR, HTTP_UNAVAILABLE, HTTP_BAD_GATEWAY -> throw new NullPointerException("Streamable is not available value now"); - default -> { - if (code != HTTP_ACCEPTED) { - throw new UnsupportedOperationException("Streamable responses with the unexpected status code: " + code); - } - } - } - - try (InputStreamReader in = new InputStreamReader(conn.getInputStream())) { - Video video = DataTool.fromJSON(in, Video.class); - - // METADATA BUILDING - var metadata = new MRL.Metadata(video.title, "", this.platform(), "", new URI(video.thumbnail_url), (long) (video.files.mp4.duration * 1000)); - - // PATCH BUILDING - var patch = new MRL.Patch(); - patch.setMetadata(metadata); - patch.addSource() - .setUri(new URI(video.files.mp4.url)) - .setType(MediaType.VIDEO) - .build(); - - // PATCH APPLY - source.apply(patch); - - return source; - } finally { - conn.disconnect(); - } - } catch (Exception e) { - throw new PatchException(source, e); - } - } - - public static class File { - @SerializedName("status") - @Expose - public int status; - - @SerializedName("url") - @Expose - public String url; - - @SerializedName("framerate") - @Expose - public int framerate; - - @SerializedName("width") - @Expose - public int width; - - @SerializedName("height") - @Expose - public int height; - - @SerializedName("bitrate") - @Expose - public int bitrate; - - @SerializedName("size") - @Expose - public int size; - - @SerializedName("duration") - @Expose - public float duration; - } - - public static class Media { - @SerializedName("mp4") - @Expose - public File mp4; - - @SerializedName("mp4-mobile") - @Expose - public File mp4_mobile; - } - - public static class Video { - @SerializedName("status") - @Expose - public int status; - - @SerializedName("percent") - @Expose - public int percent; - - @SerializedName("message") - @Expose - public String message; - - @SerializedName("files") - @Expose - public Media files; - - @SerializedName("thumbnail_url") - @Expose - public String thumbnail_url; - - @SerializedName("title") - @Expose - public String title; - } -} diff --git a/src/main/java/org/watermedia/core/network/patchs/TwitchPatch.java b/src/main/java/org/watermedia/core/network/patchs/TwitchPatch.java deleted file mode 100644 index 49276316..00000000 --- a/src/main/java/org/watermedia/core/network/patchs/TwitchPatch.java +++ /dev/null @@ -1,160 +0,0 @@ -package org.watermedia.core.network.patchs; - -import com.google.gson.JsonParser; -import com.google.gson.annotations.SerializedName; -import me.srrapero720.watermedia.api.MediaContext; -import org.watermedia.api.media.meta.MediaType; -import org.watermedia.api.media.meta.MediaQuality; -import org.watermedia.api.network.MRL; -import org.watermedia.api.NetworkAPI; -import org.watermedia.core.network.NetworkStream; -import org.watermedia.tools.DataTool; -import org.watermedia.tools.NetTool; - -import java.io.IOException; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Map; - -import static org.watermedia.tools.NetTool.USER_AGENT; - -public class TwitchPatch extends AbstractPatch { - public static final String API_AUTH_URL = "https://gql.twitch.tv/gql"; - public static final String API_STEAM_LIVE = "https://usher.ttvnw.net/api/channel/hls/%s.m3u8"; - public static final String API_STREAM_VOD = "https://usher.ttvnw.net/vod/%s.m3u8"; - public static final String CLIENT_ID = "kimne78kx3ncx6brgo4mv6wki5h1ko"; - - @Override - public String platform() { - return "Twitch"; - } - - @Override - public boolean active(MediaContext context) { - return true; - } - - @Override - public boolean validate(MRL source) { - final var host = source.getUri().getHost(); - final var path = source.getUri().getPath(); - return (host.contains(".twitch.tv") || host.equals("twitch.tv")) && (path.startsWith("/") && path.length() > 5); - } - - @Override - public void patch(MediaContext context, MRL source) throws PatchException { - var path = source.getUri().getPath().substring(1).split("/"); - var video = path[0].equals("videos") && path.length >= 2; - var id = video ? path[1] : path[0]; - - try { - final GQLData data = getAccessToken(id, video); - final String url = String.format(video ? API_STREAM_VOD : API_STEAM_LIVE, id) + getQuery(data.accessToken.signature, data.accessToken.value); - - // TODO: METADATA BUILDING - MRL.Metadata metadata = null; //new MediaURI.Metadata(); - - // PATCH BUILDING - var patch = new MRL.Patch().setMetadata(metadata); - - for (NetworkStream quality: NetworkStream.parse(getStreamString(url))) { - var uri = new URI(quality.getUrl()); - var sourceBuilder = patch.addSource(); - sourceBuilder.setType(MediaType.VIDEO); - sourceBuilder.setIsLive(!video); - sourceBuilder.putQualityIfAbsent(MediaQuality.calculate(quality.getWidth()), q -> uri); - // TODO: put offline banner as fallback URI -// sourceBuilder.setFallbackUri(new URI()); -// sourceBuilder.setFallbackType(MediaType.IMAGE); - sourceBuilder.build(); - } - - source.apply(patch); - } catch (Exception e) { - throw new PatchException(source, e); - } - } - - @Override - public void test(MediaContext context, String url) { - - } - - private static String getStreamString(String apiUrl) throws IOException { - HttpURLConnection conn = NetTool.connect(apiUrl, "GET"); - conn.setRequestProperty("x-donate-to", "https://ttv.lol/donate"); - - int code = conn.getResponseCode(); - if (code == HttpURLConnection.HTTP_NOT_FOUND) throw new NullPointerException("Stream not found"); - - return new String(DataTool.readAllBytes(code == HttpURLConnection.HTTP_OK ? conn.getInputStream() : conn.getErrorStream())); - } - - private static GQLData getAccessToken(String id, boolean isVOD) throws IOException { - HttpURLConnection conn = NetTool.connect(API_AUTH_URL, "POST"); - conn.setDoOutput(true); - conn.setRequestProperty("Client-ID", CLIENT_ID); - conn.setRequestProperty("User-Agent", USER_AGENT); - conn.setRequestProperty("Content-Type", "application/json; charset=utf-8"); - - try (OutputStream os = conn.getOutputStream()) { - // Variables mapping - Map variables = new HashMap<>(); - variables.put("isLive", !isVOD); - variables.put("isVod", isVOD); - variables.put("login", !isVOD ? id : ""); - variables.put("vodID", isVOD ? id : ""); - variables.put("playerType", "site"); - - // Main JSON mapping - Map jsonMap = new HashMap<>(); - jsonMap.put("operationName", "PlaybackAccessToken_Template"); - jsonMap.put("query", "query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, $isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, params: {platform: \"web\", playerBackend: \"mediaplayer\", playerType: $playerType}) @include(if: $isLive) { value signature authorization { isForbidden forbiddenReasonCode } __typename } videoPlaybackAccessToken(id: $vodID, params: {platform: \"web\", playerBackend: \"mediaplayer\", playerType: $playerType}) @include(if: $isVod) { value signature __typename }}"); - jsonMap.put("variables", variables); - - os.write(DataTool.GSON.toJson(jsonMap).getBytes(StandardCharsets.UTF_8)); - } - - String dataJson = new String(DataTool.readAllBytes(conn.getInputStream())); - String json = JsonParser.parseString(dataJson).getAsJsonObject().get("data").getAsJsonObject().toString(); - - return DataTool.fromJSON(json, GQLData.class); - } - - private static String getQuery(String signature, String token) { - final Map query = new HashMap<>(); - - query.put("acmb", "e30%%3D"); - query.put("allow_source", true); - query.put("fast_bread", true); - query.put("p", "7370379"); - query.put("play_session_id", "21efcd962e7b3fbc891bac088214aa63"); - query.put("player_backend", "mediaplayer"); - query.put("playlist_include_framerate", true); - query.put("reassignments_supported", true); - query.put("sig", signature); - query.put("supported_codecs", "avc1"); - query.put("token", token); - query.put("transcode_mode", "cbr_v1"); - query.put("cdm", "wv"); - query.put("player_version", "1.21.0"); - - return NetworkAPI.encodeQuery(query); - } - - public static class GQLData { - @SerializedName(value = "videoPlaybackAccessToken", alternate = "streamPlaybackAccessToken") - public Token accessToken; - - public static class Token { - @SerializedName("signature") - public String signature; - - @SerializedName("value") - public String value; - } - } -} \ No newline at end of file diff --git a/src/main/java/org/watermedia/core/network/patchs/TwitterPatch.java b/src/main/java/org/watermedia/core/network/patchs/TwitterPatch.java deleted file mode 100644 index 6bd61f59..00000000 --- a/src/main/java/org/watermedia/core/network/patchs/TwitterPatch.java +++ /dev/null @@ -1,267 +0,0 @@ -package org.watermedia.core.network.patchs; - -import com.google.gson.annotations.SerializedName; -import me.srrapero720.watermedia.api.MediaContext; -import org.watermedia.api.media.meta.MediaType; -import org.watermedia.api.media.meta.MediaQuality; -import org.watermedia.api.network.MRL; -import org.watermedia.tools.DataTool; -import org.watermedia.tools.NetTool; - -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URI; -import java.util.regex.Pattern; - -public class TwitterPatch extends AbstractPatch { - private static final String API_URL = "https://cdn.syndication.twimg.com/tweet-result?id=%s&token=%s&lang=en"; - private static final String API_KEY = "watermedia-java-x-access-token"; - private static final Pattern ID_PATTERN = Pattern.compile("^/([A-Za-z0-9_]+)/status/(\\d+)$"); - private static final Pattern RES_PATTERN = Pattern.compile("(\\d+)x(\\d+)"); - - private static final String __TYPE_T = "Tweet"; - private static final String __TYTE_TOMB = "TweetTombstone"; - - @Override - public String platform() { - return "Twitter (X)"; - } - - @Override - public boolean active(MediaContext context) { - return true; - } - - @Override - public boolean validate(MRL source) { - var host = source.getUri().getHost(); - var path = source.getUri().getPath(); - return (host.equals("www.x.com") || host.equals("x.com") || host.equals("www.twitter.com") || host.equals("twitter.com")) - && ID_PATTERN.matcher(path).matches(); - } - - @Override - public void patch(MediaContext context, MRL source) throws PatchException { - try { - final var m = ID_PATTERN.matcher(source.getUri().getPath()); - if (!m.matches()) throw new Exception("No twitter ID match found"); - final var url = String.format(API_URL, m.group(2), API_KEY); - - final var conn = NetTool.connect(url, "GET"); - - int code = conn.getResponseCode(); - switch (code) { - case HttpURLConnection.HTTP_INTERNAL_ERROR -> throw new Exception("Twitter died"); - case HttpURLConnection.HTTP_NOT_FOUND -> throw new NullPointerException("Tweet not found"); - case HttpURLConnection.HTTP_FORBIDDEN, HttpURLConnection.HTTP_UNAUTHORIZED -> - throw new UnsupportedOperationException("Twitter blocked us API access - URL: " + url); - default -> { - if (code != HttpURLConnection.HTTP_OK) - throw new UnsupportedOperationException("Unexpected response from twitter (" + code + ") - URL: " + url); - } - } - - try (final InputStream in = conn.getInputStream()) { - final var tweet = DataTool.fromJSON(new String(DataTool.readAllBytes(in)), Tweet.class); - final var patch = new MRL.Patch(); - - if (tweet.typename.equals(__TYTE_TOMB)) { - throw new UnsupportedOperationException("Tomb received: " + tweet.tombstone.text); - } - - // METADATA BUILDING - final var metadata = new MRL.Metadata( - null, - tweet.user.name, - this.platform(), - tweet.text, - new URI(tweet.user.profileImageUrlHttps), - tweet.video != null ? tweet.video.durationMs : 0 - ); - - for (MediaDetail details: tweet.mediaDetails) { - final var mediaSource = patch.addSource().setIsLive(false); - - switch (details.type) { - case "photo" -> { - mediaSource.setType(MediaType.IMAGE); - mediaSource.setUri(new URI(details.mediaUrlHttps)); - } - case "video" -> { - mediaSource.setType(MediaType.VIDEO); - for (VideoVariant videoVariant: details.videoInfo.variants) { - var matcher = RES_PATTERN.matcher(videoVariant.url); - if (videoVariant.url.contains(".mp4") && matcher.find()) { - String width = matcher.group(1); - mediaSource.putQualityIfAbsent(MediaQuality.calculate(Integer.parseInt(width)), new URI(videoVariant.url)); - mediaSource.setFallbackUri(new URI(details.mediaUrlHttps)); - mediaSource.setFallbackType(MediaType.IMAGE); - } - } - } - default -> throw new UnsupportedOperationException("Unsupported media type!"); - } - - mediaSource.build(); - } - - source.apply(patch.setMetadata(metadata)); - } finally { - conn.disconnect(); - } - } catch (Exception e) { - throw new PatchException(source, e); - } - } - - @Override - public void test(MediaContext context, String url) { - - } - - private static class Tweet { - @SerializedName("__typename") - public String typename; - - @SerializedName("lang") - public String lang; - - @SerializedName("favorite_count") - public int favoriteCount; - - @SerializedName("possibly_sensitive") - public boolean possiblySensitive; - - @SerializedName("created_at") - public String createdAt; - - @SerializedName("id_str") - public String idStr; - - @SerializedName("text") - public String text; - - @SerializedName("user") - public TwitterPatch.User user; - - @SerializedName("mediaDetails") - public TwitterPatch.MediaDetail[] mediaDetails; - - @SerializedName("photos") - public TwitterPatch.Photo[] photos; - - @SerializedName("video") - public TwitterPatch.Video video; - - @SerializedName("tombstone") - public Tombstone tombstone; - } - - private static class Video { - @SerializedName("aspectRatio") - public int[] aspectRatio; - - @SerializedName("contentType") - public String contentType; - - @SerializedName("durationMs") - public long durationMs; - - @SerializedName("poster") - public String poster; - - @SerializedName("variants") - public VideoVariant[] variants; - } - - private static class Photo { - @SerializedName("url") - public String url; - - @SerializedName("width") - public int width; - - @SerializedName("height") - public int height; - } - - private static class VideoVariant { - @SerializedName("bitrate") - public int bitrate; - - @SerializedName("content_type") - public String contentType; - - @SerializedName("url") - public String url; - } - - private static class VideoInfo { - @SerializedName("duration_millis") - public int durationMillis; - - @SerializedName("variants") - public VideoVariant[] variants; - } - - private static class MediaDetail { - @SerializedName("display_url") - public String displayUrl; - - @SerializedName("expanded_url") - public String expandedUrl; - - @SerializedName("media_url_https") - public String mediaUrlHttps; - - @SerializedName("type") - public String type; - - @SerializedName("video_info") - public VideoInfo videoInfo; - } - - private static class User { - @SerializedName("id_str") - public String idStr; - - @SerializedName("name") - public String name; - - @SerializedName("profile_image_url_https") - public String profileImageUrlHttps; - - @SerializedName("screen_name") - public String screenName; - - @SerializedName("verified") - public boolean verified; - - @SerializedName("is_blue_verified") - public boolean isBlueVerified; - } - - private static class Media { - @SerializedName("display_url") - public String displayUrl; - - @SerializedName("expanded_url") - public String expandedUrl; - - @SerializedName("url") - public String url; - } - - private static class Tombstone { - @SerializedName("text") - public Text text; - } - - private static class Text { - @SerializedName("text") - public String text; - - @SerializedName("rtl") - public boolean rtl; - } -} diff --git a/src/main/java/org/watermedia/core/network/patchs/YoutubePatch.java b/src/main/java/org/watermedia/core/network/patchs/YoutubePatch.java deleted file mode 100644 index 4fcf67ca..00000000 --- a/src/main/java/org/watermedia/core/network/patchs/YoutubePatch.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.watermedia.core.network.patchs; - -import me.srrapero720.watermedia.api.MediaContext; -import org.watermedia.api.network.MRL; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class YoutubePatch extends AbstractPatch { - private static final Pattern PATTERN = Pattern.compile("(?:youtu\\.be/|youtube\\.com/(?:embed/|v/|shorts/|feeds/api/videos/|watch\\?v=|watch\\?.+&v=))([^/?&#]+)"); - - @Override - public String platform() { - return "Youtube"; - } - - @Override - public boolean active(MediaContext context) { - return true; - } - - @Override - public boolean validate(MRL source) { - return PATTERN.matcher(source.toString()).find(); - } - - @Override - public void patch(MediaContext context, MRL source) throws PatchException { - Matcher matcher = PATTERN.matcher(source.toString()); - if (!matcher.find()) { - throw new PatchException(source, "Invalid Youtube URI"); - } - - throw new PatchException(source, "Path not implemented yet"); -// return source; - } - - @Override - public void test(MediaContext context, String url) { - - } -} diff --git a/src/main/java/org/watermedia/loader/FabricLoader.java b/src/main/java/org/watermedia/loader/FabricLoader.java deleted file mode 100644 index 8fd34eb2..00000000 --- a/src/main/java/org/watermedia/loader/FabricLoader.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.watermedia.loader; - -import org.watermedia.WaterMedia; -import net.fabricmc.api.ClientModInitializer; -import net.fabricmc.api.EnvType; - -import java.nio.file.Path; - -import static net.fabricmc.loader.api.FabricLoader.getInstance; - -// FABRIC IS USED AS COSMIC CUBE MOD LOADER, THIS LOADER MUST BE WORKING ON IT TOO -public class FabricLoader implements ClientModInitializer, WaterMedia.ILoader { - private static final Path CWD = getInstance().getGameDir(); - - @Override - public void onInitializeClient() { - try { - WaterMedia.prepare(this).start(); - } catch (Exception e) { - throw new RuntimeException("Failed starting " + WaterMedia.NAME + " for " + name() + ": " + e.getMessage(), e); - } - } - - @Override public String name() { return "Fabric (agnostic)"; } - @Override public Path cwd() { return CWD; } - @Override public Path tmp() { return WaterMedia.DEFAULT_LOADER.tmp(); } - - @Override public boolean client() { return getInstance().getEnvironmentType() == EnvType.CLIENT; } -} diff --git a/src/main/java/org/watermedia/tools/ArgTool.java b/src/main/java/org/watermedia/tools/ArgTool.java deleted file mode 100644 index 329bb28b..00000000 --- a/src/main/java/org/watermedia/tools/ArgTool.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.watermedia.tools; - -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.BooleanSupplier; -import java.util.function.DoubleSupplier; -import java.util.function.IntSupplier; -import java.util.function.Supplier; - - -public record ArgTool(String key, String value, AtomicReference override) implements Supplier, BooleanSupplier, IntSupplier, DoubleSupplier { - - /** - * it is thread safe - * @param key argument key - */ - public ArgTool(String key) { - this(key, System.getProperty(key), new AtomicReference<>(null)); - } - - public void override(String override) { - this.override.set(override); - } - - public String argument() { - return key; - } - - @Override - public String get() { - return determinate(); - } - - @Override - public String value() { - return determinate(); - } - - @Override - public boolean getAsBoolean() { - return Boolean.parseBoolean(determinate()); - } - - @Override - public int getAsInt() { - return Integer.parseInt(determinate()); - } - - @Override - public double getAsDouble() { - return Double.parseDouble(determinate()); - } - - @Override - public String toString() { - return "D" + key + "=" + determinate(); - } - - private String determinate() { - String plain = override.get(); - return plain == null ? value : plain; - } -} diff --git a/src/main/java/org/watermedia/tools/IOTool.java b/src/main/java/org/watermedia/tools/IOTool.java deleted file mode 100644 index 0af1c929..00000000 --- a/src/main/java/org/watermedia/tools/IOTool.java +++ /dev/null @@ -1,153 +0,0 @@ -package org.watermedia.tools; - -import net.sf.sevenzipjbinding.*; -import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream; -import org.apache.logging.log4j.Marker; -import org.apache.logging.log4j.MarkerManager; - -import java.io.*; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -import static org.watermedia.WaterMedia.LOGGER; - -public class IOTool { - private static final Marker IT = MarkerManager.getMarker("Tools"); - - public static String readString(Path from) { - try { - byte[] bytes = Files.readAllBytes(from); - return new String(bytes, StandardCharsets.UTF_8); - } catch (Exception e) { - return null; - } - } - - public static boolean rmdirs(Path path) { - return rmdirs(path.toFile()); - } - - public static boolean rmdirs(File root) { - File[] files = root.listFiles(); - - if (files == null || files.length == 0) return root.delete(); - for (File f: files) { - File[] childs = f.listFiles(); - if (childs != null && childs.length != 0 && !rmdirs(f)) return false; - if (!f.delete()) return false; - } - return true; - } - - public static void un7zip(Path zipPath) throws IOException { un7zip(zipPath, zipPath.getParent()); } - public static void un7zip(Path zipPath, Path destPath) throws IOException { - LOGGER.debug(IT, "Unzipping ZIP file '{}' to directory '{}'", zipPath, destPath); - - try (var file = new RandomAccessFile(zipPath.toFile(), "r"); - var archive = SevenZip.openInArchive(null, new RandomAccessFileInStream(file)) - ) { - final int count = archive.getNumberOfItems(); - final var extractIds = new ArrayList(); - for (int i = 0; i < count; i++) { - if ((boolean) archive.getProperty(i, PropID.IS_FOLDER)) { - File f = destPath.resolve(archive.getProperty(i, PropID.PATH).toString()).toFile(); - if (!f.exists() && !f.mkdirs()) throw new IOException("Failed to create directories"); - } else { - extractIds.add(i); - } - } - - archive.extract(DataTool.unbox(extractIds), false, new Seven7ExtractCallback(destPath, archive)); - } - } - - public static void unzip(Path zip) throws IOException { unzip(zip, zip.getParent()); } - public static void unzip(Path zipPath, Path destPath) throws IOException { - LOGGER.debug(IT, "Unzipping 7z file '{}' to directory '{}'", zipPath, destPath); - - if (!zipPath.toString().endsWith(".zip")) - throw new IOException("Attempted to extract a non .zip file"); - if (!destPath.toFile().exists() && !destPath.toFile().mkdirs()) - throw new IOException("Failed to make required directories"); - - try (var in = new ZipInputStream(Files.newInputStream(zipPath))) { - ZipEntry en = in.getNextEntry(); - while (en != null) { // iterates over entries in the zip file - String desPath = destPath + File.separator + en.getName(); - if (!en.isDirectory()) { - unzip$extract(in, desPath); // if the entry is a file, extracts it - } else { - File dir = new File(desPath); // if the entry is a directory, make the directory - dir.mkdirs(); - } - in.closeEntry(); - en = in.getNextEntry(); - } - } - } - - private static void unzip$extract(ZipInputStream zipIn, String filePath) throws IOException { - try (BufferedOutputStream output = new BufferedOutputStream(Files.newOutputStream(Paths.get(filePath)))) { - byte[] bytesIn = new byte[1024 * 8]; - int read; - while ((read = zipIn.read(bytesIn)) != -1) output.write(bytesIn, 0, read); - } - } - - private static final class Seven7ExtractCallback implements IArchiveExtractCallback { - private final IInArchive archive; - private final Path destPath; - private OutputStream out; - private int index = 0; - - public Seven7ExtractCallback(Path destPath, IInArchive archive) { - this.archive = archive; - this.destPath = destPath; - } - - @Override - public ISequentialOutStream getStream(int i, ExtractAskMode extractAskMode) throws SevenZipException { - this.index = i; - try { - this.out = Files.newOutputStream(destPath.resolve(archive.getProperty(i, PropID.PATH).toString())); - return bytes -> { - try { - out.write(bytes); - } catch (IOException e) { - throw new SevenZipException(e); - } - return bytes.length; - }; - } catch (IOException e) { - throw new SevenZipException(e); - } - } - - @Override - public void prepareOperation(ExtractAskMode extractAskMode) {} - - @Override - public void setOperationResult(ExtractOperationResult extractOperationResult) throws SevenZipException { - if (extractOperationResult != ExtractOperationResult.OK) { - LOGGER.error(IT, "Failed to extract file {}", archive.getProperty(index, PropID.PATH)); - } - - try { - out.close(); - } catch (IOException e) { - throw new SevenZipException(e); - } - } - - @Override - public void setTotal(long l) {} - - @Override - public void setCompleted(long l) {} - } -} \ No newline at end of file diff --git a/src/main/java/org/watermedia/tools/JarTool.java b/src/main/java/org/watermedia/tools/JarTool.java deleted file mode 100644 index 329d64f8..00000000 --- a/src/main/java/org/watermedia/tools/JarTool.java +++ /dev/null @@ -1,97 +0,0 @@ -package org.watermedia.tools; - -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; -import org.apache.logging.log4j.Marker; -import org.apache.logging.log4j.MarkerManager; - -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; -import java.io.*; -import java.net.URL; -import java.nio.charset.Charset; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.util.ArrayList; -import java.util.List; - -import static org.watermedia.WaterMedia.LOGGER; - -public class JarTool { - private static final Marker IT = MarkerManager.getMarker("Tools"); - - public static String readString(String from) { - try (InputStream is = readResourceAsStream(from)) { - byte[] bytes = DataTool.readAllBytes(is); - return new String(bytes, Charset.defaultCharset()); - } catch (Exception e) { - return null; - } - } - - public static boolean extract(String origin, Path dest) { - try (InputStream is = readResourceAsStream(origin)) { - if (is == null) throw new FileNotFoundException("Resource was not found in " + origin); - - File destParent = dest.getParent().toFile(); - if (!destParent.exists() && !destParent.mkdirs()) LOGGER.fatal(IT, "Cannot be created parent directories to {}", dest.toString()); - Files.copy(is, dest, StandardCopyOption.REPLACE_EXISTING); - return true; - } catch (Exception e) { - LOGGER.error(IT, "Failed to extract from (JAR) '{}' to '{}' due to unexpected error", origin, dest, e); - } - return false; - } - - public static List readList(String path) { - List result = new ArrayList<>(); - try (InputStreamReader reader = new InputStreamReader(readResourceAsStream(path))) { - result.addAll(new Gson().fromJson(reader, new TypeToken>() {}.getType())); - } catch (Exception e) { - LOGGER.error(IT, "Exception trying to read list JSON from {}", path, e); - } - - return result; - } - - public static String[] readArray(String path) { - try (BufferedReader reader = new BufferedReader(new InputStreamReader(readResourceAsStream(path)))) { - return new Gson().fromJson(reader, new TypeToken() {}.getType()); - } catch (IOException e) { - LOGGER.error(IT, "Exception trying to read array JSON from {}", path, e); - } - - return new String[0]; - } - - public static BufferedImage readImage(String path) { - try (InputStream in = readResourceAsStream(path)) { - BufferedImage image = ImageIO.read(in); - if (image != null) return image; - else throw new FileNotFoundException("result of BufferedImage was null"); - } catch (Exception e) { - throw new IllegalStateException("Failed loading BufferedImage from resources", e); - } - } - - public static InputStream readResourceAsStream(String source) { - return readResourceAsStream$byClassLoader(source, JarTool.class.getClassLoader()); // InputStream still can be null - } - - private static InputStream readResourceAsStream$byClassLoader(String source, ClassLoader classLoader) { - InputStream is = classLoader.getResourceAsStream(source); - if (is == null && source.startsWith("/")) is = classLoader.getResourceAsStream(source.substring(1)); - return is; - } - - public static URL readResource(String source) { - return readResource$byClassLoader(source, JarTool.class.getClassLoader()); // URL still can be null - } - - private static URL readResource$byClassLoader(String source, ClassLoader classLoader) { - URL is = classLoader.getResource(source); - if (is == null && source.startsWith("/")) is = classLoader.getResource(source.substring(1)); - return is; - } -} \ No newline at end of file diff --git a/src/main/java/org/watermedia/tools/NetTool.java b/src/main/java/org/watermedia/tools/NetTool.java deleted file mode 100644 index a94eb315..00000000 --- a/src/main/java/org/watermedia/tools/NetTool.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.watermedia.tools; - -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.URI; -import java.net.URL; - -public class NetTool { - public static final String USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0"; - - public static HttpURLConnection connect(URL url, String method) throws IOException { - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setRequestMethod(method); - conn.setRequestProperty("User-Agent", USER_AGENT); - return conn; - } - - public static HttpURLConnection connect(URI uri, String method) throws IOException { - return connect(uri.toURL(), method); - } - - public static HttpURLConnection connect(String url, String method) throws IOException { - return connect(new URL(url), method); - } -} diff --git a/src/main/resources/META-INF/services/org.watermedia.api.WaterMediaAPI b/src/main/resources/META-INF/services/me.srrapero720.watermedia.api.WaterMediaAPI similarity index 71% rename from src/main/resources/META-INF/services/org.watermedia.api.WaterMediaAPI rename to src/main/resources/META-INF/services/me.srrapero720.watermedia.api.WaterMediaAPI index 2e92f622..9220f3c3 100644 --- a/src/main/resources/META-INF/services/org.watermedia.api.WaterMediaAPI +++ b/src/main/resources/META-INF/services/me.srrapero720.watermedia.api.WaterMediaAPI @@ -1,7 +1,7 @@ me.srrapero720.watermedia.api.player.PlayerAPI -me.srrapero720.watermedia.core.cache.CacheCore +me.srrapero720.watermedia.api.cache.CacheAPI me.srrapero720.watermedia.api.image.ImageAPI me.srrapero720.watermedia.core.config.ConfigCore me.srrapero720.watermedia.api.image.ImageAPI -org.watermedia.api.NetworkAPI +me.srrapero720.watermedia.api.network.NetworkAPI me.srrapero720.watermedia.api.rendering.RenderAPI \ No newline at end of file diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 89ce1428..17a701bb 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -20,6 +20,9 @@ "me.srrapero720.watermedia.loaders.McFabricLoader" ] }, + "breaks": { + "fancyvideo_api": "*" + }, "depends": { "java": "$javarange" } diff --git a/src/main/resources/videolan/win-x64/win-x64.7z b/src/main/resources/videolan/win-x64/win-x64.7z deleted file mode 100644 index 99670f61..00000000 Binary files a/src/main/resources/videolan/win-x64/win-x64.7z and /dev/null differ diff --git a/src/test/java/org/watermedia/render/TestApp.java b/src/test/java/me/srrapero720/watermedia/TestApp.java similarity index 96% rename from src/test/java/org/watermedia/render/TestApp.java rename to src/test/java/me/srrapero720/watermedia/TestApp.java index 8c1fd475..4cc92fba 100644 --- a/src/test/java/org/watermedia/render/TestApp.java +++ b/src/test/java/me/srrapero720/watermedia/TestApp.java @@ -1,9 +1,9 @@ -package org.watermedia.render; +package me.srrapero720.watermedia; -import org.watermedia.WaterMedia; import me.srrapero720.watermedia.api.image.ImageAPI; import me.srrapero720.watermedia.api.image.ImageCache; import me.srrapero720.watermedia.api.image.ImageRenderer; +import me.srrapero720.watermedia.loader.ILoader; import org.lwjgl.glfw.GLFWErrorCallback; import org.lwjgl.glfw.GLFWVidMode; import org.lwjgl.opengl.ARBDebugOutput; @@ -31,11 +31,11 @@ public class TestApp implements Executor { private long window; // The media loader - private static final String NAME = "org.watermedia.render.TestApp"; + private static final String NAME = "TestApp"; public void run(String url) { try { - WaterMedia.prepare(WaterMedia.DEFAULT_LOADER).start(); + WaterMedia.prepare(ILoader.DEFAULT).start(); } catch (Exception e) { throw new RuntimeException("Failed to load WATERMeDIA", e); } diff --git a/src/test/java/me/srrapero720/watermedia/tests/bootstrap/BootstrapTest.java b/src/test/java/me/srrapero720/watermedia/tests/bootstrap/BootstrapTest.java new file mode 100644 index 00000000..124af9ac --- /dev/null +++ b/src/test/java/me/srrapero720/watermedia/tests/bootstrap/BootstrapTest.java @@ -0,0 +1,14 @@ +package me.srrapero720.watermedia.tests.bootstrap; + +import me.srrapero720.watermedia.WaterMedia; +import me.srrapero720.watermedia.loader.ILoader; +import org.junit.jupiter.api.BeforeAll; + +public class BootstrapTest { + + @BeforeAll + public static void testBootstrap() throws Exception { + WaterMedia instance = WaterMedia.prepare(ILoader.DEFAULT); + instance.start(); + } +} diff --git a/src/test/java/me/srrapero720/watermedia/tests/compress/Seven7ExtractorTest.java b/src/test/java/me/srrapero720/watermedia/tests/compress/Seven7ExtractorTest.java new file mode 100644 index 00000000..e01ea3cd --- /dev/null +++ b/src/test/java/me/srrapero720/watermedia/tests/compress/Seven7ExtractorTest.java @@ -0,0 +1,17 @@ +package me.srrapero720.watermedia.tests.compress; + +import me.srrapero720.watermedia.tools.IOTool; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.MarkerManager; +import org.junit.jupiter.api.Test; + +import java.io.File; + +public class Seven7ExtractorTest { + private static final Marker IT = MarkerManager.getMarker(Seven7ExtractorTest.class.getSimpleName()); + + @Test + public void testSeven7() throws Exception { + IOTool.un7zip(IT, new File("run/win-x64.7z").toPath()); + } +} diff --git a/src/test/java/org/watermedia/math/MathEasingTest.java b/src/test/java/me/srrapero720/watermedia/tests/easing/MathEasingTest.java similarity index 99% rename from src/test/java/org/watermedia/math/MathEasingTest.java rename to src/test/java/me/srrapero720/watermedia/tests/easing/MathEasingTest.java index 569dafc8..beafebf3 100644 --- a/src/test/java/org/watermedia/math/MathEasingTest.java +++ b/src/test/java/me/srrapero720/watermedia/tests/easing/MathEasingTest.java @@ -1,6 +1,6 @@ -package org.watermedia.math; +package me.srrapero720.watermedia.tests.easing; -import org.watermedia.api.MathAPI; +import me.srrapero720.watermedia.api.math.MathAPI; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/src/test/java/me/srrapero720/watermedia/tests/network/NetworkFixersTest.java b/src/test/java/me/srrapero720/watermedia/tests/network/NetworkFixersTest.java new file mode 100644 index 00000000..6ab18e1b --- /dev/null +++ b/src/test/java/me/srrapero720/watermedia/tests/network/NetworkFixersTest.java @@ -0,0 +1,6 @@ +package me.srrapero720.watermedia.tests.network; + +public class NetworkFixersTest { + + +} diff --git a/src/test/java/org/watermedia/BootstrapTest.java b/src/test/java/org/watermedia/BootstrapTest.java deleted file mode 100644 index 2ce2fe80..00000000 --- a/src/test/java/org/watermedia/BootstrapTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.watermedia; - -import org.junit.jupiter.api.BeforeAll; - - -public class BootstrapTest { - - @BeforeAll - public static void testBootstrap() throws Exception { - WaterMedia instance = WaterMedia.prepare(WaterMedia.DEFAULT_LOADER); - instance.start(); - } -} diff --git a/src/test/java/org/watermedia/network/ImgurPatchTest.java b/src/test/java/org/watermedia/network/ImgurPatchTest.java deleted file mode 100644 index a86317d6..00000000 --- a/src/test/java/org/watermedia/network/ImgurPatchTest.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.watermedia.network; - -import org.junit.jupiter.api.Test; - -public class ImgurPatchTest { - - @Test - public void testPatch() { - - } -} diff --git a/src/test/java/org/watermedia/network/TwitterPatchTest.java b/src/test/java/org/watermedia/network/TwitterPatchTest.java deleted file mode 100644 index 135d001b..00000000 --- a/src/test/java/org/watermedia/network/TwitterPatchTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.watermedia.network; - -import me.srrapero720.watermedia.api.MediaContext; -import org.watermedia.api.network.MRL; -import org.watermedia.core.network.patchs.TwitterPatch; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; -import static org.watermedia.WaterMedia.*; - -public class TwitterPatchTest { - public static final MediaContext CONTEXT = new MediaContext.Simple("twitter_test", "Twitter Patch test"); - private static final String EXAMPLE_URI = "https://x.com/SrRap720/status/1842440861964984539"; - - @Test - public void testTwitterConnection() { - MRL source = MRL.get(CONTEXT, EXAMPLE_URI); - TwitterPatch patch = new TwitterPatch(); - - assertTrue(patch.validate(source)); - try { - patch.patch(source, CONTEXT); - } catch (Exception e) { - throw new RuntimeException("Failed to patch URL", e); - } - - assertTrue(source.patched()); - var sources = source.getSources(); - assertEquals(4, sources.length); - - for (MRL.Source s: sources) { - LOGGER.debug(s.toString()); - } - } -} diff --git a/src/test/java/org/watermedia/tooling/Seven7Test.java b/src/test/java/org/watermedia/tooling/Seven7Test.java deleted file mode 100644 index 133f1417..00000000 --- a/src/test/java/org/watermedia/tooling/Seven7Test.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.watermedia.tooling; - -import org.watermedia.tools.IOTool; -import org.apache.logging.log4j.Marker; -import org.apache.logging.log4j.MarkerManager; -import org.junit.jupiter.api.Test; - -import java.io.File; - -public class Seven7Test { - private static final Marker IT = MarkerManager.getMarker(Seven7Test.class.getSimpleName()); - - @Test - public void testVideoLanExtraction() throws Exception { - File zip = new File("run/win-x64.7z"); - IOTool.un7zip(zip.toPath()); - - // delete test shit - for (File file: zip.getParentFile().listFiles()) { - if (!file.isDirectory()) { - if (!file.equals(zip)) file.delete(); - } else { - deleteFolder(file); - } - } - } - - private static void deleteFolder(File file) { - for (File f: file.listFiles()) { - if (f.isDirectory()) deleteFolder(f); - else f.delete(); - } - file.delete(); - } -}