initial commit

This commit is contained in:
MrLetsplay 2025-02-09 23:51:04 +01:00
commit d826999380
Signed by: mr
SSH Key Fingerprint: SHA256:92jBH80vpXyaZHjaIl47pjRq+Yt7XGTArqQg1V7hSqg
15 changed files with 653 additions and 0 deletions

40
.classpath Normal file
View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/bin/
/target/
/dependency-reduced-pom.xml
/library

23
.project Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>VideoBaseV2</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,12 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.targetPlatform=11
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=11
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
org.eclipse.jdt.core.compiler.release=enabled
org.eclipse.jdt.core.compiler.source=11

View File

@ -0,0 +1,4 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

9
README.md Normal file
View File

@ -0,0 +1,9 @@
# VideoBase
A simple file-based video library
(not to be confused with the identically named project located [here](https://github.com/MrLetsplay2003/VideoBase))
## Planned features
- [ ] REST API supporting fetching videos and metadata, authentication and editing metadata (no video upload)
- [ ] Frontend based on the aforementioned REST API
- [ ] API for easy integration into other projects

40
pom.xml Normal file
View File

@ -0,0 +1,40 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>me.mrletsplay</groupId>
<artifactId>VideoBaseV2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<build>
<sourceDirectory>src/main/java</sourceDirectory>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>11</release>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>Graphite-Official</id>
<url>https://maven.graphite-official.com/releases</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>me.mrletsplay</groupId>
<artifactId>MrCore</artifactId>
<version>4.6.1</version>
</dependency>
<dependency>
<groupId>me.mrletsplay</groupId>
<artifactId>SimpleHTTPServer</artifactId>
<version>2.1-SNAPSHOT</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,19 @@
package me.mrletsplay.videobase;
import me.mrletsplay.mrcore.json.converter.JSONConvertible;
import me.mrletsplay.mrcore.json.converter.JSONValue;
public class Config implements JSONConvertible {
@JSONValue
private String libraryPath;
private Config() {}
public static Config createDefault() {
Config config = new Config();
config.libraryPath = "library";
return config;
}
}

View File

@ -0,0 +1,52 @@
package me.mrletsplay.videobase;
import java.nio.file.Path;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
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 = Logger.getLogger(VideoBase.class.getName());
private static ScheduledExecutorService executor;
private static Library library;
public static void main(String[] args) {
executor = Executors.newScheduledThreadPool(0);
// Verify hash is working
Hash.hash("test");
loadLibrary();
executor.scheduleWithFixedDelay(VideoBase::loadLibrary, 10, 10, TimeUnit.MINUTES);
HttpServer server = new HttpServer(HttpServer.newConfigurationBuilder()
.hostBindAll()
.port(6969)
.poolSize(20)
.ioWorkers(3)
.create());
new LibraryAPI().register(server.getDocumentProvider());
server.start();
}
public static Library getLibrary() {
return library;
}
private static void loadLibrary() {
library = Library.load(Path.of("/mnt/wd4tb/Files/ytdl/Aliensrock/"));
System.out.println(library.getVideos());
}
}

View File

@ -0,0 +1,149 @@
package me.mrletsplay.videobase.library;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.logging.Level;
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.videobase.VideoBase;
import me.mrletsplay.videobase.util.Hash;
public class Library {
public static final String
METADATA_NAME = "meta.json",
VIDEO_METADATA_SUFFIX = ".meta.json";
private Path path;
private List<Video> videos;
public Library(Path path, List<Video> videos) {
Objects.requireNonNull(path, "path");
Objects.requireNonNull(videos, "videos");
this.path = path;
this.videos = videos;
}
public Path getPath() {
return path;
}
public List<Video> getVideos() {
return videos;
}
public Video findVideoById(String id) {
return videos.stream()
.filter(v -> v.getId().equals(id))
.findFirst().orElse(null);
}
/**
* Finds the videos for the given series and/or author sorted by their index
* @param series The series or {@code null} to not filter by series
* @param author The author or {@code null} to not filter by author
* @return The videos matching the given criteria
*/
public List<Video> findVideosBy(String series, String author) { // TODO: Take VideoMetadata to allow any parameter from meta
return videos.stream()
.filter(v -> (series == null || series.equals(v.getMetadata().getSeries()))
&& (author == null || author.equals(v.getMetadata().getAuthor())))
.sorted(Comparator.comparing(v -> v.getMetadata().getIndex()))
.collect(Collectors.toList());
}
public List<String> getSeries() {
return videos.stream()
.map(v -> v.getMetadata().getSeries())
.distinct()
.collect(Collectors.toList());
}
public List<String> getAuthors() {
return videos.stream()
.map(v -> v.getMetadata().getAuthor())
.distinct()
.collect(Collectors.toList());
}
public static Library load(Path path) {
List<Video> videos = new ArrayList<>();
load(path, path, videos, VideoMetadata.DEFAULT_METADATA);
return new Library(path, videos);
}
private static void load(Path rootPath, Path path, List<Video> videos, VideoMetadata parentDefaultMetadata) {
if(!Files.isDirectory(path)) return;
LibraryMetadata libraryMeta = null;
VideoMetadata defaultMetadata = parentDefaultMetadata;
Path metaPath = path.resolve("meta.json");
if(Files.isRegularFile(metaPath)) {
try {
JSONObject obj = new JSONObject(Files.readString(metaPath));
libraryMeta = JSONConverter.decodeObject(obj, LibraryMetadata.class);
defaultMetadata = VideoMetadata.inherit(defaultMetadata, libraryMeta.getDefault());
}catch(IOException | JSONParseException | ClassCastException e) {
VideoBase.LOGGER.log(Level.WARNING, "Failed to parse metadata at " + path, e);
}
}
try {
for(Path subPath : Files.walk(path, 1)
.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)) {
load(rootPath, subPath, videos, defaultMetadata);
continue;
}
if(fileName.equals(METADATA_NAME) || fileName.endsWith(VIDEO_METADATA_SUFFIX)) {
continue;
}
VideoMetadata inferredMetadata = new VideoMetadata(new JSONObject(Map.of(
VideoMetadata.FIELD_TITLE, fileNameNoExt,
VideoMetadata.FIELD_SERIES, path.getFileName().toString()
)));
VideoMetadata videoMeta = VideoMetadata.inherit(defaultMetadata, inferredMetadata);
Path videoMetaPath = path.resolve(subPath.getFileName().toString() + VIDEO_METADATA_SUFFIX);
if(Files.isRegularFile(videoMetaPath)) {
try {
JSONObject obj = new JSONObject(Files.readString(videoMetaPath));
videoMeta = VideoMetadata.inherit(videoMeta, JSONConverter.decodeObject(obj, VideoMetadata.class));
}catch(IOException | JSONParseException | ClassCastException e) {
VideoBase.LOGGER.log(Level.WARNING, "Failed to parse metadata at " + path, e);
}
}else if(libraryMeta != null && libraryMeta.getOverrides().containsKey(fileName)) {
videoMeta = VideoMetadata.inherit(videoMeta, libraryMeta.getOverrides().get(fileName));
}
String id = Hash.hash(rootPath.relativize(subPath).toString());
videos.add(new Video(subPath, id, videoMeta == null ? VideoMetadata.DEFAULT_METADATA : videoMeta));
}
}catch(IOException e) {
VideoBase.LOGGER.log(Level.WARNING, "Failed to load folder at " + path, e);
}
}
}

View File

@ -0,0 +1,51 @@
package me.mrletsplay.videobase.library;
import java.util.HashMap;
import java.util.Map;
import me.mrletsplay.mrcore.json.JSONObject;
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.JSONValue;
public class LibraryMetadata implements JSONConvertible {
@JSONValue("default")
private VideoMetadata defaultMetadata;
private Map<String, VideoMetadata> overrides;
@JSONConstructor
public LibraryMetadata() {
this.overrides = new HashMap<>();
}
public VideoMetadata getDefault() {
return defaultMetadata;
}
public Map<String, VideoMetadata> getOverrides() {
return overrides;
}
@Override
public void preDeserialize(JSONObject object) {
JSONObject ov = object.optJSONObject("overrides").orElse(null);
if(ov != null) {
for(String key : ov.keys()) {
overrides.put(key, JSONConverter.decodeObject(ov.getJSONObject(key), VideoMetadata.class));
}
}
}
@Override
public void preSerialize(JSONObject object) {
JSONObject ov = new JSONObject();
for(String key : overrides.keySet()) {
ov.put(key, overrides.get(key).toJSON());
}
object.put("overrides", ov);
}
}

View File

@ -0,0 +1,34 @@
package me.mrletsplay.videobase.library;
import java.nio.file.Path;
public class Video {
private Path path;
private String id;
private VideoMetadata metadata;
public Video(Path path, String id, VideoMetadata metadata) {
this.path = path;
this.id = id;
this.metadata = metadata;
}
public Path getPath() {
return path;
}
public String getId() {
return id;
}
public VideoMetadata getMetadata() {
return metadata;
}
@Override
public String toString() {
return "{Video: " + id + ", " + path + ", " + metadata + "}";
}
}

View File

@ -0,0 +1,88 @@
package me.mrletsplay.videobase.library;
import java.util.Map;
import java.util.Objects;
import me.mrletsplay.mrcore.json.JSONObject;
import me.mrletsplay.mrcore.json.converter.JSONConstructor;
import me.mrletsplay.mrcore.json.converter.JSONConvertible;
public class VideoMetadata implements JSONConvertible {
public static final String
FIELD_SERIES = "series",
FIELD_TITLE = "title",
FIELD_AUTHOR = "author",
FIELD_INDEX = "index";
public static final String
DEFAULT_SERIES = "Unnamed Series",
DEFAULT_TITLE = "Unnamed Video",
DEFAULT_AUTHOR = "Unnamed Author";
public static final int
DEFAULT_INDEX = 0;
public static final VideoMetadata DEFAULT_METADATA = new VideoMetadata(new JSONObject(Map.of(
FIELD_SERIES, DEFAULT_SERIES,
FIELD_TITLE, DEFAULT_TITLE,
FIELD_AUTHOR, DEFAULT_AUTHOR,
FIELD_INDEX, DEFAULT_INDEX
)));
private JSONObject metadata;
@JSONConstructor
private VideoMetadata() {}
public VideoMetadata(JSONObject metadata) {
Objects.requireNonNull(metadata, "metadata");
this.metadata = metadata;
}
public String getSeries() {
return metadata.optString(FIELD_SERIES).orElse(null);
}
public String getTitle() {
return metadata.optString(FIELD_TITLE).orElse(null);
}
public String getAuthor() {
return metadata.optString(FIELD_AUTHOR).orElse(null);
}
public Integer getIndex() {
return metadata.optInt(FIELD_INDEX).orElse(null);
}
@Override
public void preDeserialize(JSONObject object) {
this.metadata = object;
}
@Override
public void preSerialize(JSONObject object) {
for(String key : metadata.keys()) {
object.set(key, metadata.get(key));
}
}
@Override
public String toString() {
return metadata.toString();
}
public static VideoMetadata inherit(VideoMetadata parent, VideoMetadata child) {
if(parent == null) return child;
if(child == null) return parent;
JSONObject meta = new JSONObject(parent == null ? null : parent.metadata);
for(String key : child.metadata.keys()) {
meta.set(key, child.metadata.get(key));
}
return new VideoMetadata(meta);
}
}

View File

@ -0,0 +1,104 @@
package me.mrletsplay.videobase.rest;
import java.util.List;
import me.mrletsplay.mrcore.json.JSONArray;
import me.mrletsplay.mrcore.json.JSONObject;
import me.mrletsplay.mrcore.json.converter.SerializationOption;
import me.mrletsplay.simplehttpserver.http.HttpRequestMethod;
import me.mrletsplay.simplehttpserver.http.HttpStatusCodes;
import me.mrletsplay.simplehttpserver.http.endpoint.Endpoint;
import me.mrletsplay.simplehttpserver.http.endpoint.EndpointCollection;
import me.mrletsplay.simplehttpserver.http.endpoint.RequestParameter;
import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext;
import me.mrletsplay.simplehttpserver.http.response.JsonResponse;
import me.mrletsplay.simplehttpserver.http.response.TextResponse;
import me.mrletsplay.videobase.VideoBase;
import me.mrletsplay.videobase.library.Library;
import me.mrletsplay.videobase.library.Video;
public class LibraryAPI implements EndpointCollection {
@Endpoint(method = HttpRequestMethod.GET, path = "")
public void getLibrary(HttpRequestContext ctx) {
String groupBy = ctx.getRequestedPath().getQuery().getFirst("groupBy", "none").toLowerCase();
Library library = VideoBase.getLibrary();
JSONObject libraryObj = new JSONObject();
JSONObject videosObj = new JSONObject();
List<String> groups;
switch(groupBy) {
case "series": {
groups = library.getSeries();
break;
}
case "author": {
groups = library.getAuthors();
break;
}
case "none":
default:
groups = List.of("all");
break;
}
for(String group : groups) {
List<Video> videos;
switch(groupBy) {
case "series": {
videos = library.findVideosBy(group, null);
break;
}
case "author": {
videos = library.findVideosBy(null, group);
break;
}
case "none":
default:
videos = library.getVideos();
break;
}
JSONArray videosArr = new JSONArray();
for(Video video : videos) {
videosArr.add(videoToJSON(video));
}
videosObj.put(group, videosArr);
}
if(!"none".equals("groupBy")) {
}
libraryObj.put("videos", videosObj);
ctx.respond(HttpStatusCodes.OK_200, new JsonResponse(libraryObj));
}
@Endpoint(method = HttpRequestMethod.GET, path = "/video/{video}", pathPattern = true)
public void getVideo(HttpRequestContext ctx, @RequestParameter("video") String videoId) {
Video video = VideoBase.getLibrary().findVideoById(videoId);
if(video == null) {
ctx.respond(HttpStatusCodes.NOT_FOUND_404, new TextResponse("Not found"));
return;
}
ctx.respond(HttpStatusCodes.OK_200, new JsonResponse(videoToJSON(video)));
}
private JSONObject videoToJSON(Video video) {
JSONObject v = new JSONObject();
v.put("id", video.getId());
v.put("metadata", video.getMetadata().toJSON(SerializationOption.DONT_INCLUDE_CLASS));
return v;
}
@Override
public String getBasePath() {
return "/api/library";
}
}

View File

@ -0,0 +1,24 @@
package me.mrletsplay.videobase.util;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Hash {
private static final MessageDigest SHA_3;
static {
try {
SHA_3 = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Failed to initialize hash", e);
}
}
public static String hash(String input) {
return new BigInteger(1, SHA_3.digest(input.getBytes(StandardCharsets.UTF_8))).toString(36);
}
}