From 04b17abdee2953063696f1e82967b1fe2d36c2fe Mon Sep 17 00:00:00 2001 From: MrLetsplay Date: Wed, 12 Feb 2025 22:40:35 +0100 Subject: [PATCH] Save metadata to library, Add thumbnail caching --- .gitignore | 2 + .../java/me/mrletsplay/videobase/Config.java | 14 +++++++ .../me/mrletsplay/videobase/VideoBase.java | 39 ++++++++++++++++--- .../mrletsplay/videobase/library/Library.java | 16 ++++++-- .../mrletsplay/videobase/rest/LibraryAPI.java | 2 +- .../videobase/util/ThumbnailCreator.java | 33 +++++++++++++--- 6 files changed, 91 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index fe582da..c5eb281 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ /target/ /dependency-reduced-pom.xml /library +/cache +/config.json diff --git a/src/main/java/me/mrletsplay/videobase/Config.java b/src/main/java/me/mrletsplay/videobase/Config.java index ff69e3c..8b7358c 100644 --- a/src/main/java/me/mrletsplay/videobase/Config.java +++ b/src/main/java/me/mrletsplay/videobase/Config.java @@ -1,5 +1,6 @@ package me.mrletsplay.videobase; +import me.mrletsplay.mrcore.json.converter.JSONConstructor; import me.mrletsplay.mrcore.json.converter.JSONConvertible; import me.mrletsplay.mrcore.json.converter.JSONValue; @@ -8,11 +9,24 @@ public class Config implements JSONConvertible { @JSONValue private String libraryPath; + @JSONValue + private String cachePath; + + @JSONConstructor private Config() {} + public String getLibraryPath() { + return libraryPath; + } + + public String getCachePath() { + return cachePath; + } + public static Config createDefault() { Config config = new Config(); config.libraryPath = "library"; + config.cachePath = "cache"; return config; } diff --git a/src/main/java/me/mrletsplay/videobase/VideoBase.java b/src/main/java/me/mrletsplay/videobase/VideoBase.java index 9753933..fac54b6 100644 --- a/src/main/java/me/mrletsplay/videobase/VideoBase.java +++ b/src/main/java/me/mrletsplay/videobase/VideoBase.java @@ -1,5 +1,8 @@ package me.mrletsplay.videobase; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -7,25 +10,47 @@ import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.slf4j.helpers.NOPLogger; +import me.mrletsplay.mrcore.json.JSONObject; +import me.mrletsplay.mrcore.json.converter.JSONConverter; +import me.mrletsplay.mrcore.json.converter.SerializationOption; import me.mrletsplay.simplehttpserver.http.cors.CorsConfiguration; import me.mrletsplay.simplehttpserver.http.server.HttpServer; import me.mrletsplay.videobase.library.Library; import me.mrletsplay.videobase.rest.LibraryAPI; -import me.mrletsplay.videobase.util.Hash; public class VideoBase { public static final Logger LOGGER = LoggerFactory.getLogger(VideoBase.class.getName()); private static ScheduledExecutorService executor; + private static Config config; private static Library library; public static void main(String[] args) { executor = Executors.newScheduledThreadPool(0); - // Verify hash is working - Hash.hash("test"); + Path configPath = Path.of("config.json"); + if(args.length >= 1) { + configPath = Path.of(args[0]); + } + + configPath = configPath.toAbsolutePath(); + + try { + if(!Files.exists(configPath)) { + Files.createDirectories(configPath.getParent()); + Files.writeString(configPath, Config.createDefault().toJSON(SerializationOption.DONT_INCLUDE_CLASS).toFancyString(), StandardCharsets.UTF_8); + LOGGER.info("Default configuration created, please edit it and restart"); + System.exit(0); + } + + config = JSONConverter.decodeObject(new JSONObject(Files.readString(configPath, StandardCharsets.UTF_8)), Config.class); + } catch (IOException e) { + LOGGER.error("Failed to load config", e); + System.exit(1); + } loadLibrary(); executor.scheduleWithFixedDelay(VideoBase::loadLibrary, 10, 10, TimeUnit.MINUTES); @@ -35,7 +60,7 @@ public class VideoBase { .port(6969) .poolSize(20) .ioWorkers(3) -// .logger(NOPLogger.NOP_LOGGER) + .logger(NOPLogger.NOP_LOGGER) .defaultCorsConfiguration(CorsConfiguration.createAllowAll()) .create()); @@ -44,12 +69,16 @@ public class VideoBase { server.start(); } + public static Config getConfig() { + return config; + } + public static Library getLibrary() { return library; } private static void loadLibrary() { - library = Library.load(Path.of("/mnt/wd4tb/Files/ytdl/Aliensrock/")); + library = Library.load(Path.of(config.getLibraryPath())); System.out.println(library.getVideos()); } diff --git a/src/main/java/me/mrletsplay/videobase/library/Library.java b/src/main/java/me/mrletsplay/videobase/library/Library.java index 8c2cd49..1dee67d 100644 --- a/src/main/java/me/mrletsplay/videobase/library/Library.java +++ b/src/main/java/me/mrletsplay/videobase/library/Library.java @@ -1,6 +1,7 @@ package me.mrletsplay.videobase.library; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -13,6 +14,7 @@ import java.util.stream.Collectors; import me.mrletsplay.mrcore.json.JSONObject; import me.mrletsplay.mrcore.json.JSONParseException; import me.mrletsplay.mrcore.json.converter.JSONConverter; +import me.mrletsplay.mrcore.json.converter.SerializationOption; import me.mrletsplay.videobase.VideoBase; import me.mrletsplay.videobase.util.Hash; @@ -75,10 +77,18 @@ public class Library { .collect(Collectors.toList()); } - public void updateMetadata(Video video, VideoMetadata metadata) { + public boolean updateMetadata(Video video, VideoMetadata metadata) { VideoMetadata oldMetadata = video.getMetadata(); - video.setMetadata(VideoMetadata.inherit(oldMetadata, metadata)); - // TODO: save metadata + VideoMetadata newMetadata = VideoMetadata.inherit(oldMetadata, metadata); + + Path videoPath = video.getPath().toAbsolutePath(); + try { + Files.writeString(videoPath.getParent().resolve(videoPath.getFileName().toString() + VIDEO_METADATA_SUFFIX), newMetadata.toJSON(SerializationOption.DONT_INCLUDE_CLASS).toFancyString(), StandardCharsets.UTF_8); + video.setMetadata(newMetadata); + return true; + } catch (IOException e) { + return false; + } } public boolean updateMetadata(String videoId, VideoMetadata metadata) { diff --git a/src/main/java/me/mrletsplay/videobase/rest/LibraryAPI.java b/src/main/java/me/mrletsplay/videobase/rest/LibraryAPI.java index b69ee35..39745f8 100644 --- a/src/main/java/me/mrletsplay/videobase/rest/LibraryAPI.java +++ b/src/main/java/me/mrletsplay/videobase/rest/LibraryAPI.java @@ -138,7 +138,7 @@ public class LibraryAPI implements EndpointCollection { return; } - BufferedImage thumbnail = ThumbnailCreator.createThumbnail(video.getPath()); + BufferedImage thumbnail = ThumbnailCreator.createThumbnail(video); if(thumbnail == null) { ctx.respond(HttpStatusCodes.NOT_FOUND_404, new TextResponse("No thumbnail found")); return; diff --git a/src/main/java/me/mrletsplay/videobase/util/ThumbnailCreator.java b/src/main/java/me/mrletsplay/videobase/util/ThumbnailCreator.java index 20acaa1..d52f289 100644 --- a/src/main/java/me/mrletsplay/videobase/util/ThumbnailCreator.java +++ b/src/main/java/me/mrletsplay/videobase/util/ThumbnailCreator.java @@ -4,18 +4,36 @@ import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; import java.nio.file.Path; import java.util.concurrent.TimeUnit; import javax.imageio.ImageIO; import me.mrletsplay.videobase.VideoBase; +import me.mrletsplay.videobase.library.Video; public class ThumbnailCreator { - public static BufferedImage createThumbnail(Path videoPath) { + public static BufferedImage createThumbnail(Video video) { + Path cacheDir = Path.of(VideoBase.getConfig().getCachePath()).resolve("thumbnails").toAbsolutePath(); + Path thumbnailFile = cacheDir.resolve(video.getId() + ".png"); + try { + Files.createDirectories(cacheDir); + if(Files.exists(thumbnailFile)) { + try(InputStream in = Files.newInputStream(thumbnailFile)) { + return ImageIO.read(in); + } + } + } catch (IOException e) { + VideoBase.LOGGER.warn("Failed to load thumbnail", e); + return null; + } + ProcessBuilder ffmpegBuilder = new ProcessBuilder("ffmpeg", - "-i", videoPath.toAbsolutePath().toString(), + "-i", video.getPath().toAbsolutePath().toString(), "-vf", "thumbnail", "-vf", "scale=960:540:force_original_aspect_ratio=decrease,pad=960:540:-1:-1:color=black", "-frames:v", "1", @@ -26,11 +44,9 @@ public class ThumbnailCreator { try { Process ffmpeg = ffmpegBuilder.start(); - long start = System.currentTimeMillis(); ByteArrayOutputStream bOut = new ByteArrayOutputStream(); - byte[] buf = new byte[1024]; int len; while(System.currentTimeMillis() - start < 1000 @@ -51,9 +67,14 @@ public class ThumbnailCreator { return null; } - return ImageIO.read(new ByteArrayInputStream(bOut.toByteArray())); + BufferedImage thumbnail = ImageIO.read(new ByteArrayInputStream(bOut.toByteArray())); + try(OutputStream out = Files.newOutputStream(thumbnailFile)) { + ImageIO.write(thumbnail, "PNG", out); + } + + return thumbnail; } catch (InterruptedException | IOException e) { - e.printStackTrace(); + VideoBase.LOGGER.warn("Failed to create thumbnail", e); return null; }