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

View File

@ -4,7 +4,6 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
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.videobase.library.Library;
import me.mrletsplay.videobase.rest.LibraryAPI;
import me.mrletsplay.videobase.util.ThumbnailCreator;
public class VideoBase {
@ -48,6 +48,7 @@ public class VideoBase {
}
config = JSONConverter.decodeObject(new JSONObject(Files.readString(configPath, StandardCharsets.UTF_8)), Config.class);
config.validate();
} catch (IOException e) {
LOGGER.error("Failed to load config", e);
System.exit(1);
@ -82,8 +83,8 @@ public class VideoBase {
}
private static void loadLibrary() {
Objects.requireNonNull(config.getLibraryPath(), "libraryPath");
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) {
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;
@ -138,21 +142,31 @@ public class Library {
.filter(p -> path.equals(p.getParent()))
.sorted(Comparator.comparing(p -> p.getFileName().toString()))
.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(libraryMetadata != null && libraryMetadata.getExcludePaths().contains(subPath.getFileName().toString())) {
VideoBase.LOGGER.debug("Ignoring folder at " + subPath + ": Listed in excludedPaths");
continue;
}
if(Files.isDirectory(subPath)) {
load(rootPath, subPath, videos, defaultMetadata);
continue;
}
String fileName = subPath.getFileName().toString();
if(fileName.equals(METADATA_NAME) || fileName.endsWith(VIDEO_METADATA_SUFFIX)) {
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.FIELD_TITLE, fileNameNoExt
)));

View File

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

View File

@ -189,9 +189,10 @@ public class LibraryAPI implements EndpointCollection {
try {
long length = Files.size(videoPath);
String contentType = Files.probeContentType(videoPath);
InputStream in = Files.newInputStream(videoPath);
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) {
ctx.respond(HttpStatusCodes.INTERNAL_SERVER_ERROR_500, new TextResponse("Failed to load video"));
return;

View File

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