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
All checks were successful
Build and push container / Build-Docker-Container (push) Successful in 4m44s
This commit is contained in:
parent
a14ecb8050
commit
2187519abc
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
)));
|
)));
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user