Make all config options except libraryPath optional, Add in-memory thumbnail cache, Add excludePaths & .nomedia to exclude paths
All checks were successful
Build and push container / Build-Docker-Container (push) Successful in 4m44s

This commit is contained in:
MrLetsplay 2025-02-18 20:37:11 +01:00
parent a14ecb8050
commit 2187519abc
Signed by: mr
SSH Key Fingerprint: SHA256:92jBH80vpXyaZHjaIl47pjRq+Yt7XGTArqQg1V7hSqg
6 changed files with 92 additions and 21 deletions

View File

@ -1,11 +1,18 @@
package me.mrletsplay.videobase; package me.mrletsplay.videobase;
import java.util.List;
import java.util.Objects;
import me.mrletsplay.mrcore.json.JSONType;
import me.mrletsplay.mrcore.json.converter.JSONConstructor; import me.mrletsplay.mrcore.json.converter.JSONConstructor;
import me.mrletsplay.mrcore.json.converter.JSONConvertible; import me.mrletsplay.mrcore.json.converter.JSONConvertible;
import me.mrletsplay.mrcore.json.converter.JSONListType;
import me.mrletsplay.mrcore.json.converter.JSONValue; import me.mrletsplay.mrcore.json.converter.JSONValue;
public class Config implements JSONConvertible { public class Config implements JSONConvertible {
public static final List<String> DEFAULT_FILE_TYPES = List.of("mp4", "mpeg", "mkv", "flv", "avi", "webm");
@JSONValue @JSONValue
private String libraryPath; private String libraryPath;
@ -15,6 +22,10 @@ public class Config implements JSONConvertible {
@JSONValue @JSONValue
private String cachePath; private String cachePath;
@JSONValue
@JSONListType(JSONType.STRING)
private List<String> includeFileTypes;
@JSONConstructor @JSONConstructor
private Config() {} private Config() {}
@ -30,11 +41,20 @@ public class Config implements JSONConvertible {
return cachePath; return cachePath;
} }
public List<String> getIncludeFileTypes() {
return includeFileTypes == null ? DEFAULT_FILE_TYPES : includeFileTypes;
}
public void validate() {
Objects.requireNonNull(libraryPath, "libraryPath");
}
public static Config createDefault() { public static Config createDefault() {
Config config = new Config(); Config config = new Config();
config.libraryPath = "library"; config.libraryPath = "library";
config.readOnly = false; config.readOnly = false;
config.cachePath = "cache"; config.cachePath = "cache";
config.includeFileTypes = DEFAULT_FILE_TYPES;
return config; return config;
} }

View File

@ -4,7 +4,6 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Objects;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -20,6 +19,7 @@ import me.mrletsplay.simplehttpserver.http.cors.CorsConfiguration;
import me.mrletsplay.simplehttpserver.http.server.HttpServer; import me.mrletsplay.simplehttpserver.http.server.HttpServer;
import me.mrletsplay.videobase.library.Library; import me.mrletsplay.videobase.library.Library;
import me.mrletsplay.videobase.rest.LibraryAPI; import me.mrletsplay.videobase.rest.LibraryAPI;
import me.mrletsplay.videobase.util.ThumbnailCreator;
public class VideoBase { public class VideoBase {
@ -48,6 +48,7 @@ public class VideoBase {
} }
config = JSONConverter.decodeObject(new JSONObject(Files.readString(configPath, StandardCharsets.UTF_8)), Config.class); config = JSONConverter.decodeObject(new JSONObject(Files.readString(configPath, StandardCharsets.UTF_8)), Config.class);
config.validate();
} catch (IOException e) { } catch (IOException e) {
LOGGER.error("Failed to load config", e); LOGGER.error("Failed to load config", e);
System.exit(1); System.exit(1);
@ -82,8 +83,8 @@ public class VideoBase {
} }
private static void loadLibrary() { private static void loadLibrary() {
Objects.requireNonNull(config.getLibraryPath(), "libraryPath");
library = Library.load(Path.of(config.getLibraryPath()), config.isReadOnly()); library = Library.load(Path.of(config.getLibraryPath()), config.isReadOnly());
ThumbnailCreator.clearCache();
} }
} }

View File

@ -113,6 +113,10 @@ public class Library {
private static void load(Path rootPath, Path path, List<Video> videos, VideoMetadata parentDefaultMetadata) { private static void load(Path rootPath, Path path, List<Video> videos, VideoMetadata parentDefaultMetadata) {
if(!Files.isDirectory(path)) return; if(!Files.isDirectory(path)) return;
if(Files.isRegularFile(path.resolve(".nomedia"))) {
VideoBase.LOGGER.debug("Ignoring folder at " + path + ": .nomedia file exists");
return;
}
LibraryMetadata libraryMetadata = null; LibraryMetadata libraryMetadata = null;
@ -138,21 +142,31 @@ public class Library {
.filter(p -> path.equals(p.getParent())) .filter(p -> path.equals(p.getParent()))
.sorted(Comparator.comparing(p -> p.getFileName().toString())) .sorted(Comparator.comparing(p -> p.getFileName().toString()))
.collect(Collectors.toList())) { .collect(Collectors.toList())) {
String fileName = subPath.getFileName().toString();
String fileNameNoExt = fileName;
if(fileNameNoExt.contains(".")) {
fileNameNoExt = fileNameNoExt.substring(0, fileNameNoExt.lastIndexOf('.'));
}
if(Files.isDirectory(subPath)) { if(Files.isDirectory(subPath)) {
if(libraryMetadata != null && libraryMetadata.getExcludePaths().contains(subPath.getFileName().toString())) {
VideoBase.LOGGER.debug("Ignoring folder at " + subPath + ": Listed in excludedPaths");
continue;
}
load(rootPath, subPath, videos, defaultMetadata); load(rootPath, subPath, videos, defaultMetadata);
continue; continue;
} }
String fileName = subPath.getFileName().toString();
if(fileName.equals(METADATA_NAME) || fileName.endsWith(VIDEO_METADATA_SUFFIX)) { if(fileName.equals(METADATA_NAME) || fileName.endsWith(VIDEO_METADATA_SUFFIX)) {
continue; continue;
} }
if(!VideoBase.getConfig().getIncludeFileTypes().stream().anyMatch(ext -> fileName.endsWith("." + ext))) {
VideoBase.LOGGER.debug("Ignoring file at " + subPath + ": No valid file extension");
continue;
}
String fileNameNoExt = fileName;
if(fileNameNoExt.contains(".")) {
fileNameNoExt = fileNameNoExt.substring(0, fileNameNoExt.lastIndexOf('.'));
}
VideoMetadata inferredVideoMetadata = new VideoMetadata(new JSONObject(Map.of( VideoMetadata inferredVideoMetadata = new VideoMetadata(new JSONObject(Map.of(
VideoMetadata.FIELD_TITLE, fileNameNoExt VideoMetadata.FIELD_TITLE, fileNameNoExt
))); )));

View File

@ -1,12 +1,15 @@
package me.mrletsplay.videobase.library; package me.mrletsplay.videobase.library;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import me.mrletsplay.mrcore.json.JSONObject; import me.mrletsplay.mrcore.json.JSONObject;
import me.mrletsplay.mrcore.json.JSONType;
import me.mrletsplay.mrcore.json.converter.JSONConstructor; import me.mrletsplay.mrcore.json.converter.JSONConstructor;
import me.mrletsplay.mrcore.json.converter.JSONConverter; import me.mrletsplay.mrcore.json.converter.JSONConverter;
import me.mrletsplay.mrcore.json.converter.JSONConvertible; import me.mrletsplay.mrcore.json.converter.JSONConvertible;
import me.mrletsplay.mrcore.json.converter.JSONListType;
import me.mrletsplay.mrcore.json.converter.JSONValue; import me.mrletsplay.mrcore.json.converter.JSONValue;
public class LibraryMetadata implements JSONConvertible { public class LibraryMetadata implements JSONConvertible {
@ -14,6 +17,10 @@ public class LibraryMetadata implements JSONConvertible {
@JSONValue("default") @JSONValue("default")
private VideoMetadata defaultMetadata; private VideoMetadata defaultMetadata;
@JSONValue
@JSONListType(JSONType.STRING)
private List<String> excludePaths;
private Map<String, VideoMetadata> overrides; private Map<String, VideoMetadata> overrides;
@JSONConstructor @JSONConstructor
@ -25,6 +32,10 @@ public class LibraryMetadata implements JSONConvertible {
return defaultMetadata; return defaultMetadata;
} }
public List<String> getExcludePaths() {
return excludePaths;
}
public Map<String, VideoMetadata> getOverrides() { public Map<String, VideoMetadata> getOverrides() {
return overrides; return overrides;
} }

View File

@ -189,9 +189,10 @@ public class LibraryAPI implements EndpointCollection {
try { try {
long length = Files.size(videoPath); long length = Files.size(videoPath);
String contentType = Files.probeContentType(videoPath);
InputStream in = Files.newInputStream(videoPath); InputStream in = Files.newInputStream(videoPath);
ctx.getServerHeader().setCompressionEnabled(false); ctx.getServerHeader().setCompressionEnabled(false);
ctx.getServerHeader().setContent(MimeType.of("video/mpeg"), in, length); ctx.getServerHeader().setContent(MimeType.of(contentType == null ? "video/mpeg" : contentType), in, length);
} catch (IOException e) { } catch (IOException e) {
ctx.respond(HttpStatusCodes.INTERNAL_SERVER_ERROR_500, new TextResponse("Failed to load video")); ctx.respond(HttpStatusCodes.INTERNAL_SERVER_ERROR_500, new TextResponse("Failed to load video"));
return; return;

View File

@ -8,6 +8,8 @@ import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
@ -17,19 +19,37 @@ import me.mrletsplay.videobase.library.Video;
public class ThumbnailCreator { public class ThumbnailCreator {
private static Map<String, BufferedImage> cachedThumbnails = new HashMap<>();
public static void clearCache() {
cachedThumbnails.clear();
}
public static BufferedImage createThumbnail(Video video) { public static BufferedImage createThumbnail(Video video) {
Path cacheDir = Path.of(VideoBase.getConfig().getCachePath()).resolve("thumbnails").toAbsolutePath(); if(cachedThumbnails.containsKey(video.getId())) {
Path thumbnailFile = cacheDir.resolve(video.getId() + ".png"); return cachedThumbnails.get(video.getId());
try { }
Files.createDirectories(cacheDir);
if(Files.exists(thumbnailFile)) { String cachePath = VideoBase.getConfig().getCachePath();
try(InputStream in = Files.newInputStream(thumbnailFile)) { Path thumbnailFile = null;
return ImageIO.read(in);
if(cachePath != null) {
thumbnailFile = Path.of(cachePath).resolve("thumbnails").resolve(video.getId() + ".png").toAbsolutePath();
try {
if(Files.exists(thumbnailFile)) {
try(InputStream in = Files.newInputStream(thumbnailFile)) {
BufferedImage thumbnail = ImageIO.read(in);
if(thumbnail == null) return null;
cachedThumbnails.put(video.getId(), thumbnail);
return thumbnail;
}
} }
} catch (IOException e) {
VideoBase.LOGGER.warn("Failed to load thumbnail", e);
return null;
} }
} catch (IOException e) {
VideoBase.LOGGER.warn("Failed to load thumbnail", e);
return null;
} }
ProcessBuilder ffmpegBuilder = new ProcessBuilder("ffmpeg", ProcessBuilder ffmpegBuilder = new ProcessBuilder("ffmpeg",
@ -70,10 +90,14 @@ public class ThumbnailCreator {
BufferedImage thumbnail = ImageIO.read(new ByteArrayInputStream(bOut.toByteArray())); BufferedImage thumbnail = ImageIO.read(new ByteArrayInputStream(bOut.toByteArray()));
if(thumbnail == null) return null; if(thumbnail == null) return null;
try(OutputStream out = Files.newOutputStream(thumbnailFile)) { if(thumbnailFile != null) {
ImageIO.write(thumbnail, "PNG", out); Files.createDirectories(thumbnailFile.getParent());
try(OutputStream out = Files.newOutputStream(thumbnailFile)) {
ImageIO.write(thumbnail, "PNG", out);
}
} }
cachedThumbnails.put(video.getId(), thumbnail);
return thumbnail; return thumbnail;
} catch (InterruptedException | IOException e) { } catch (InterruptedException | IOException e) {
VideoBase.LOGGER.warn("Failed to create thumbnail", e); VideoBase.LOGGER.warn("Failed to create thumbnail", e);