From 35592e2f48d44080dd6cbf28d91aa12f1474d13b Mon Sep 17 00:00:00 2001 From: 0xb00bface <0xboobface@gmail.com> Date: Sun, 12 Nov 2023 13:48:31 +0100 Subject: [PATCH] Move Help servlets to common module and embed it into the webinterface --- client/pom.xml | 5 - .../src/main/java/ctbrec/docs/DocServer.java | 21 +- client/src/main/resources/html/docs/400.html | 3 - client/src/main/resources/html/docs/404.html | 2 - .../src/main/resources/html/docs/footer.html | 83 -- .../src/main/resources/html/docs/header.html | 107 -- common/pom.xml | 5 + .../ctbrec/servlet}/AbstractDocServlet.java | 96 +- .../java/ctbrec/servlet}/MarkdownServlet.java | 38 +- .../java/ctbrec/servlet}/SearchServlet.java | 28 +- common/src/main/resources/docs/400.html | 3 + common/src/main/resources/docs/404.html | 2 + common/src/main/resources/docs/500.html | 3 + .../src/main/resources}/docs/Avidemux.md | 0 .../main/resources}/docs/ConfigurationFile.md | 70 +- .../src/main/resources}/docs/FFmpeg.md | 12 +- .../src/main/resources}/docs/MKVToolNix.md | 0 .../main/resources}/docs/PostProcessing.md | 2 +- .../resources}/docs/QuestionsAndAnswers.md | 20 +- .../main/resources}/docs/RunningTheServer.md | 0 .../main/resources}/docs/VideoTutorials.md | 0 common/src/main/resources/docs/footer.html | 41 + common/src/main/resources/docs/header.html | 80 ++ .../ctbrec/recorder/server/HttpServer.java | 11 + .../src/main/resources/html/static/ctbrec.svg | 155 +++ .../main/resources/html/static/favicon.svg | 108 ++ .../src/main/resources/html/static/index.html | 1016 +++++++++-------- 27 files changed, 1097 insertions(+), 814 deletions(-) delete mode 100644 client/src/main/resources/html/docs/400.html delete mode 100644 client/src/main/resources/html/docs/404.html delete mode 100644 client/src/main/resources/html/docs/footer.html delete mode 100644 client/src/main/resources/html/docs/header.html rename {client/src/main/java/ctbrec/docs => common/src/main/java/ctbrec/servlet}/AbstractDocServlet.java (51%) rename {client/src/main/java/ctbrec/docs => common/src/main/java/ctbrec/servlet}/MarkdownServlet.java (77%) rename {client/src/main/java/ctbrec/docs => common/src/main/java/ctbrec/servlet}/SearchServlet.java (85%) create mode 100644 common/src/main/resources/docs/400.html create mode 100644 common/src/main/resources/docs/404.html create mode 100644 common/src/main/resources/docs/500.html rename {client/src/main/resources/html => common/src/main/resources}/docs/Avidemux.md (100%) rename {client/src/main/resources/html => common/src/main/resources}/docs/ConfigurationFile.md (66%) rename {client/src/main/resources/html => common/src/main/resources}/docs/FFmpeg.md (77%) rename {client/src/main/resources/html => common/src/main/resources}/docs/MKVToolNix.md (100%) rename {client/src/main/resources/html => common/src/main/resources}/docs/PostProcessing.md (99%) rename {client/src/main/resources/html => common/src/main/resources}/docs/QuestionsAndAnswers.md (91%) rename {client/src/main/resources/html => common/src/main/resources}/docs/RunningTheServer.md (100%) rename {client/src/main/resources/html => common/src/main/resources}/docs/VideoTutorials.md (100%) create mode 100644 common/src/main/resources/docs/footer.html create mode 100644 common/src/main/resources/docs/header.html create mode 100644 server/src/main/resources/html/static/ctbrec.svg create mode 100644 server/src/main/resources/html/static/favicon.svg diff --git a/client/pom.xml b/client/pom.xml index 689d8fd4..1aea5857 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -80,11 +80,6 @@ org.eclipse.jetty jetty-servlet - - com.vladsch.flexmark - flexmark - 0.40.34 - diff --git a/client/src/main/java/ctbrec/docs/DocServer.java b/client/src/main/java/ctbrec/docs/DocServer.java index 6fe48ff1..b1e7cfb6 100644 --- a/client/src/main/java/ctbrec/docs/DocServer.java +++ b/client/src/main/java/ctbrec/docs/DocServer.java @@ -1,29 +1,28 @@ package ctbrec.docs; -import java.net.BindException; - -import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.HttpConfiguration; -import org.eclipse.jetty.server.HttpConnectionFactory; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; +import ctbrec.servlet.AbstractDocServlet; +import ctbrec.servlet.MarkdownServlet; +import ctbrec.servlet.SearchServlet; +import ctbrec.servlet.StaticFileServlet; +import org.eclipse.jetty.server.*; import org.eclipse.jetty.server.handler.HandlerList; import org.eclipse.jetty.servlet.ServletHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import ctbrec.servlet.StaticFileServlet; +import java.net.BindException; public class DocServer { private static final Logger LOG = LoggerFactory.getLogger(DocServer.class); private static volatile boolean started = false; - private DocServer() {} + private DocServer() { + } public static synchronized void start() throws Exception { - if(started) { + if (started) { return; } @@ -39,7 +38,7 @@ public class DocServer { var handler = new ServletHandler(); server.setHandler(handler); var handlers = new HandlerList(); - handlers.setHandlers(new Handler[] { handler }); + handlers.setHandlers(new Handler[]{handler}); server.setHandler(handlers); var markdownServlet = new MarkdownServlet(); diff --git a/client/src/main/resources/html/docs/400.html b/client/src/main/resources/html/docs/400.html deleted file mode 100644 index ea1f9c45..00000000 --- a/client/src/main/resources/html/docs/400.html +++ /dev/null @@ -1,3 +0,0 @@ -

400 Bad Request

-

{message}

-Try something else! \ No newline at end of file diff --git a/client/src/main/resources/html/docs/404.html b/client/src/main/resources/html/docs/404.html deleted file mode 100644 index 6cae96d4..00000000 --- a/client/src/main/resources/html/docs/404.html +++ /dev/null @@ -1,2 +0,0 @@ -

404 File Not Found

-Try something else! \ No newline at end of file diff --git a/client/src/main/resources/html/docs/footer.html b/client/src/main/resources/html/docs/footer.html deleted file mode 100644 index 694625d2..00000000 --- a/client/src/main/resources/html/docs/footer.html +++ /dev/null @@ -1,83 +0,0 @@ -

- - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/main/resources/html/docs/header.html b/client/src/main/resources/html/docs/header.html deleted file mode 100644 index ebd4d59c..00000000 --- a/client/src/main/resources/html/docs/header.html +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - - - - - - - - CTB Recorder - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-

- - \ No newline at end of file diff --git a/common/pom.xml b/common/pom.xml index 6319163b..d5c3fe48 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -79,6 +79,11 @@ antlr4-runtime ${antlr.version} + + com.vladsch.flexmark + flexmark + 0.40.34 + ch.qos.logback logback-classic diff --git a/client/src/main/java/ctbrec/docs/AbstractDocServlet.java b/common/src/main/java/ctbrec/servlet/AbstractDocServlet.java similarity index 51% rename from client/src/main/java/ctbrec/docs/AbstractDocServlet.java rename to common/src/main/java/ctbrec/servlet/AbstractDocServlet.java index d02ae95f..37a62a22 100644 --- a/client/src/main/java/ctbrec/docs/AbstractDocServlet.java +++ b/common/src/main/java/ctbrec/servlet/AbstractDocServlet.java @@ -1,67 +1,71 @@ -package ctbrec.docs; +package ctbrec.servlet; -import static java.nio.charset.StandardCharsets.*; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.net.URLDecoder; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Enumeration; -import java.util.List; -import java.util.Objects; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; +import lombok.extern.slf4j.Slf4j; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.net.URL; +import java.net.URLDecoder; +import java.util.*; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.regex.Matcher; +import java.util.regex.Pattern; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import static java.nio.charset.StandardCharsets.UTF_8; +@Slf4j public abstract class AbstractDocServlet extends HttpServlet { - private static final transient Logger LOG = LoggerFactory.getLogger(AbstractDocServlet.class); + public static final String CLASSPATH_DIR = "/docs"; + private static final Pattern CONTEXT_AWARE_URL = Pattern.compile("@\\{(.*?)\\}"); String loadFile(String resource) throws IOException { - InputStream resourceAsStream = getClass().getResourceAsStream(resource); - if(resourceAsStream == null) { - throw new FileNotFoundException(); + try (InputStream resourceAsStream = getClass().getResourceAsStream(resource)) { + if (resourceAsStream == null) { + throw new FileNotFoundException(); + } + var out = new ByteArrayOutputStream(); + var length = 0; + var buffer = new byte[1024]; + while ((length = resourceAsStream.read(buffer)) >= 0) { + out.write(buffer, 0, length); + } + return out.toString(UTF_8); } - var out = new ByteArrayOutputStream(); - var length = 0; - var buffer = new byte[1024]; - while( (length = resourceAsStream.read(buffer)) >= 0 ) { - out.write(buffer, 0, length); - } - return new String(out.toByteArray(), UTF_8); + } + + protected String getBaseDir() { + return Optional.ofNullable(getServletContext().getContextPath()).orElse("") + CLASSPATH_DIR; } String getHeader() throws IOException { - return loadFile("/html/docs/header.html"); + return renderContextAwareUris(loadFile(CLASSPATH_DIR + "/header.html")); } String getFooter() throws IOException { - return loadFile("/html/docs/footer.html"); + return renderContextAwareUris(loadFile(CLASSPATH_DIR + "/footer.html")); + } + + private String renderContextAwareUris(String s) { + Matcher m = CONTEXT_AWARE_URL.matcher(s); + var contextPath = Optional.ofNullable(getServletContext().getContextPath()).orElse(""); + return m.replaceAll(matchResult -> contextPath + matchResult.group(1)); } List getPages() throws IOException { List pages = new ArrayList<>(); - URL resource = getClass().getResource("/html/docs"); - if(Objects.equals(resource.getProtocol(), "file")) { - LOG.debug("FILE {}", resource); + URL resource = getClass().getResource(CLASSPATH_DIR); + if (Objects.equals(resource.getProtocol(), "file")) { + log.debug("FILE {}", resource); indexDirectory(resource, pages); - } else if(Objects.equals(resource.getProtocol(), "jar")) { - LOG.debug("JAR {}", resource); + } else if (Objects.equals(resource.getProtocol(), "jar")) { + log.debug("JAR {}", resource); indexJar(resource, pages); } pages.add("index.md"); - Collections.sort(pages, (a, b) -> a.compareToIgnoreCase(b)); + pages.sort(String::compareToIgnoreCase); return pages; } @@ -91,23 +95,27 @@ public abstract class AbstractDocServlet extends HttpServlet { } String loadMarkdown(String path) throws IOException { - String resource = "/html" + path; - return loadFile(resource); + var contextPath = getServletContext().getContextPath(); + if (contextPath != null && path.startsWith(contextPath)) { + path = path.substring(contextPath.length()); + } + return loadFile(path); } protected void error(HttpServletResponse resp, int status, String message) { try { resp.setStatus(status); resp.getWriter().println(getHeader()); - String html = loadFile("/html/docs/" + status + ".html"); - if(message == null || message.trim().isEmpty()) { + String html = loadFile(CLASSPATH_DIR + "/" + status + ".html"); + if (message == null || message.trim().isEmpty()) { message = ""; } html = html.replace("{message}", message); + html = renderContextAwareUris(html); resp.getWriter().println(html); resp.getWriter().println(getFooter()); } catch (IOException e) { - LOG.error("Error while sending error response. Man, his is bad!", e); + log.error("Error while sending error response. Man, his is bad!", e); } } } diff --git a/client/src/main/java/ctbrec/docs/MarkdownServlet.java b/common/src/main/java/ctbrec/servlet/MarkdownServlet.java similarity index 77% rename from client/src/main/java/ctbrec/docs/MarkdownServlet.java rename to common/src/main/java/ctbrec/servlet/MarkdownServlet.java index cadb8772..92ec3462 100644 --- a/client/src/main/java/ctbrec/docs/MarkdownServlet.java +++ b/common/src/main/java/ctbrec/servlet/MarkdownServlet.java @@ -1,32 +1,26 @@ -package ctbrec.docs; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.List; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +package ctbrec.servlet; import com.google.common.base.Objects; import com.vladsch.flexmark.html.HtmlRenderer; import com.vladsch.flexmark.parser.Parser; import com.vladsch.flexmark.util.ast.Node; import com.vladsch.flexmark.util.options.MutableDataSet; +import lombok.extern.slf4j.Slf4j; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.List; + +@Slf4j public class MarkdownServlet extends AbstractDocServlet { - - private static final transient Logger LOG = LoggerFactory.getLogger(MarkdownServlet.class); - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { String path = req.getRequestURI(); - LOG.trace("Path: [{}]", path); + log.debug("Path: [{}]", path); try { - if(Objects.equal(path, "/docs/index.md")) { + if (Objects.equal(path, getBaseDir() + "/index.md")) { listPages(resp); } else { String md = loadMarkdown(path); @@ -37,9 +31,11 @@ public class MarkdownServlet extends AbstractDocServlet { resp.getWriter().println(getFooter()); } } catch (FileNotFoundException e) { - error(resp, HttpServletResponse.SC_NOT_FOUND, ""); + log.error("Error loading markdown page", e); + error(resp, HttpServletResponse.SC_NOT_FOUND, e.getMessage()); } catch (Exception e) { - error(resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ""); + log.error("Error loading markdown page", e); + error(resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage()); } } @@ -47,7 +43,7 @@ public class MarkdownServlet extends AbstractDocServlet { List pages = getPages(); var html = new StringBuilder("

"); resp.setStatus(HttpServletResponse.SC_OK); diff --git a/client/src/main/java/ctbrec/docs/SearchServlet.java b/common/src/main/java/ctbrec/servlet/SearchServlet.java similarity index 85% rename from client/src/main/java/ctbrec/docs/SearchServlet.java rename to common/src/main/java/ctbrec/servlet/SearchServlet.java index a4883eeb..03e56878 100644 --- a/client/src/main/java/ctbrec/docs/SearchServlet.java +++ b/common/src/main/java/ctbrec/servlet/SearchServlet.java @@ -1,25 +1,23 @@ -package ctbrec.docs; - -import static javax.servlet.http.HttpServletResponse.*; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.List; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +package ctbrec.servlet; import org.json.JSONArray; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.List; + +import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; + public class SearchServlet extends AbstractDocServlet { private static final Logger LOG = LoggerFactory.getLogger(SearchServlet.class); private static final String Q = "term"; @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { try { if (req.getParameter(Q) == null) { error(resp, HttpServletResponse.SC_BAD_REQUEST, "Parameter \"" + Q + "\" is missing"); @@ -34,7 +32,7 @@ public class SearchServlet extends AbstractDocServlet { String q = req.getParameter(Q).toLowerCase(); String[] tokens = q.split("\\s+"); searchPages(result, pages, tokens); - resp.getWriter().println(result.toString()); + resp.getWriter().println(result); } catch (Exception e) { try { resp.sendError(SC_INTERNAL_SERVER_ERROR, "Internal Server Error"); @@ -45,13 +43,15 @@ public class SearchServlet extends AbstractDocServlet { } private void searchPages(JSONArray result, List pages, String[] tokens) throws IOException { + LOG.debug(pages.toString()); for (String page : pages) { try { - String content = loadMarkdown("/docs/" + page).toLowerCase(); + String content = loadMarkdown(CLASSPATH_DIR + "/" + page).toLowerCase(); var allFound = true; for (String token : tokens) { if (!content.contains(token)) { allFound = false; + break; } } if (allFound) { diff --git a/common/src/main/resources/docs/400.html b/common/src/main/resources/docs/400.html new file mode 100644 index 00000000..2d77ac15 --- /dev/null +++ b/common/src/main/resources/docs/400.html @@ -0,0 +1,3 @@ +

400 Bad Request

+

{message}

+Try something else! diff --git a/common/src/main/resources/docs/404.html b/common/src/main/resources/docs/404.html new file mode 100644 index 00000000..008defdf --- /dev/null +++ b/common/src/main/resources/docs/404.html @@ -0,0 +1,2 @@ +

404 File Not Found

+Try something else! diff --git a/common/src/main/resources/docs/500.html b/common/src/main/resources/docs/500.html new file mode 100644 index 00000000..03f85007 --- /dev/null +++ b/common/src/main/resources/docs/500.html @@ -0,0 +1,3 @@ +

500 Internal Server Error

+

{message}

+Try something else! diff --git a/client/src/main/resources/html/docs/Avidemux.md b/common/src/main/resources/docs/Avidemux.md similarity index 100% rename from client/src/main/resources/html/docs/Avidemux.md rename to common/src/main/resources/docs/Avidemux.md diff --git a/client/src/main/resources/html/docs/ConfigurationFile.md b/common/src/main/resources/docs/ConfigurationFile.md similarity index 66% rename from client/src/main/resources/html/docs/ConfigurationFile.md rename to common/src/main/resources/docs/ConfigurationFile.md index 8d659d49..0530b3a0 100644 --- a/client/src/main/resources/html/docs/ConfigurationFile.md +++ b/common/src/main/resources/docs/ConfigurationFile.md @@ -1,18 +1,23 @@ #### Configuration File + The configuration file stores all your settings and recorded models. ##### Location + ctbrec application: + - Windows: `C:\Users\{your user name}\AppData\Roaming\ctbrec\settings.json` - Linux: `~/.config/ctbrec/settings.json` -- macOS: `/Users/{your user name}/Library/Preferences/ctbrec/settings.json` +- macOS: `/Users/{your user name}/Library/Preferences/ctbrec/settings.json` server: + - Windows: `C:\Users\{your user name}\AppData\Roaming\ctbrec\server.json` - Linux: `~/.config/ctbrec/server.json` - macOS: `/Users/{your user name}/Library/Preferences/ctbrec/server.json` ##### Values + The application and the server share the same configuration file structure. That's why there are values, which don't make sense in the server configuration and vice versa. These values are simply ignored. This is a collection of the most interesting values: @@ -20,20 +25,23 @@ ignored. This is a collection of the most interesting values: - **chooseStreamQuality** (app only) - [`true`,`false`] Opens the stream resolution selection dialog, when you start recording a model. - **concurrentRecordings** - [0 - 2147483647] Limits the number of concurrently running recordings. Once this number is reached, now more recordings are started -until a recording is finished. 0 means unlimited. + until a recording is finished. 0 means unlimited. - **determineResolution** (app only) - [`true`,`false`] Display the stream resolution on the thumbnails. - **hlsdlExecutable** - Path to the hlsdl executable, which is used, if `useHlsdl` is set to true -- **httpPort** - [1 - 65536] The TCP port, the server listens on. In the server config, this will tell the server, which port to use. In the application this will set -the port ctbrec tries to connect to, if it is run in remote mode. +- **httpPort** - [1 - 65536] The TCP port, the server listens on. In the server config, this will tell the server, which port to use. In the application this + will set + the port ctbrec tries to connect to, if it is run in remote mode. - **httpServer** (app only) - The TCP host where the server is running. Has no effect, if ctbrec is run in local reocrding mode. -- **httpTimeout** - [1 - 2147483647] in milliseconds. In the server configuration this sets the idle timeout for connections to the server. In the app this sets the connect and read timeout for any HTTP connection. +- **httpTimeout** - [1 - 2147483647] in milliseconds. In the server configuration this sets the idle timeout for connections to the server. In the app this sets + the connect and read timeout for any HTTP connection. -- **httpUserAgent** - The user agent, which is used in the HTTP header, when ctbrec connects to a camsite. This is used to disguise, that it actually is a recording software :) +- **httpUserAgent** - The user agent, which is used in the HTTP header, when ctbrec connects to a camsite. This is used to disguise, that it actually is a + recording software :) - **httpUserAgentMobile** - Same as *httpUserAgent*, but in same cases we have to pretend to be a mobile phone :) @@ -45,39 +53,55 @@ the port ctbrec tries to connect to, if it is run in remote mode. - **loghlsdlOutput** - [`true`,`false`] The output from hlsdl will be logged in temporary files. Only in effect, if `useHlsdl` is set to true -- **minimumResolution** - [1 - 2147483647]. Sets the minimum video height for a recording. ctbrec tries to find a stream quality, which is higher than or equal to this value. If the only provided stream quality is below this threshold, ctbrec won't record the stream. +- **minimumResolution** - [1 - 2147483647]. Sets the minimum video height for a recording. ctbrec tries to find a stream quality, which is higher than or equal + to this value. If the only provided stream quality is below this threshold, ctbrec won't record the stream. -- **maximumResolution** - [1 - 2147483647]. Sets the maximum video height for a recording. ctbrec tries to find a stream quality, which is lower than or equal to this value. If the only provided stream quality is above this threshold, ctbrec won't record the stream. +- **maximumResolution** - [1 - 2147483647]. Sets the maximum video height for a recording. ctbrec tries to find a stream quality, which is lower than or equal + to this value. If the only provided stream quality is above this threshold, ctbrec won't record the stream. -- **minimumLengthInSeconds** - **Deprecated. Add a post-processing step instead. See [Post-Processing](/docs/PostProcessing.md)** [0 - 2147483647] Automatically delete recordings, which are shorter than this amount of seconds. 0 disables this feature. +- **minimumLengthInSeconds** - **Deprecated. Add a post-processing step instead. See [Post-Processing](PostProcessing.md)** [0 - 2147483647] + Automatically delete recordings, which are shorter than this amount of seconds. 0 disables this feature. -- **minimumSpaceLeftInBytes** - [0 - 9223372036854775807] The space in bytes ctbrec should conserve on the hard drive. 1 GiB = 1024 MiB = 1048576 KiB = 1073741824 bytes +- **minimumSpaceLeftInBytes** - [0 - 9223372036854775807] The space in bytes ctbrec should conserve on the hard drive. 1 GiB = 1024 MiB = 1048576 KiB = + 1073741824 bytes -- **onlineCheckIntervalInSecs** - [1 - 2147483647] How often ctbrec checks, if a model is online. This is not a guaranteed interval: If you record many models, the online check for all models can take longer than this interval. A minute is a reasonable value, but you can go lower, if you don't want to miss a anything. But don't go too low, or you risk to do too many requests in a short amount of time and get banned by some sites. +- **onlineCheckIntervalInSecs** - [1 - 2147483647] How often ctbrec checks, if a model is online. This is not a guaranteed interval: If you record many models, + the online check for all models can take longer than this interval. A minute is a reasonable value, but you can go lower, if you don't want to miss a + anything. But don't go too low, or you risk to do too many requests in a short amount of time and get banned by some sites. -- **onlineCheckSkipsPausedModels** - [`true`,`false`] Skip the online check for paused models. If you have many models in the recording list, this can reduce the delay when a recording starts after a model came online. +- **onlineCheckSkipsPausedModels** - [`true`,`false`] Skip the online check for paused models. If you have many models in the recording list, this can reduce + the delay when a recording starts after a model came online. -- **postProcessing** - **Deprecated. See [Post-Processing](/docs/PostProcessing.md)** Absolute path to a script, which is executed once a recording is finished. +- **postProcessing** - **Deprecated. See [Post-Processing](PostProcessing.md)** Absolute path to a script, which is executed once a recording is + finished. - **recordingsDir** - Where ctbrec saves the recordings. -- **recordingsDirStructure** (server only) - [`FLAT`, `ONE_PER_MODEL`, `ONE_PER_RECORDING`] How recordings are stored in the file system. `FLAT` - all recordings in one directory; `ONE_PER_MODEL` - one directory per model; `ONE_PER_RECORDING` - each recordings ends up in its own directory. Change this only, if you have `recordSingleFile` set to `true` +- **recordingsDirStructure** (server only) - [`FLAT`, `ONE_PER_MODEL`, `ONE_PER_RECORDING`] How recordings are stored in the file system. `FLAT` - all + recordings in one directory; `ONE_PER_MODEL` - one directory per model; `ONE_PER_RECORDING` - each recordings ends up in its own directory. Change this only, + if you have `recordSingleFile` set to `true` -- **recordSingleFile** (server only) - [`true`,`false`] - How recordings are stored in the file system. `true` means, each recording is saved in one large file. `false` means, ctbrec just downloads the stream segments. +- **recordSingleFile** (server only) - [`true`,`false`] - How recordings are stored in the file system. `true` means, each recording is saved in one large + file. `false` means, ctbrec just downloads the stream segments. -- **splitStrategy** - [`DONT`, `TIME`, `SIZE`, `TIME_OR_SIZE`] Defines if and how to split recordings. Also see `splitRecordingsAfterSecs` and `splitRecordingsBiggerThanBytes` +- **splitStrategy** - [`DONT`, `TIME`, `SIZE`, `TIME_OR_SIZE`] Defines if and how to split recordings. Also see `splitRecordingsAfterSecs` + and `splitRecordingsBiggerThanBytes` -- **splitRecordingsAfterSecs** - [0 - 2147483647] in seconds. Split recordings after this amount of seconds. The recordings are split up into several individual recordings, -which have the defined length (roughly). Has to be activated with `splitStrategy`. +- **splitRecordingsAfterSecs** - [0 - 2147483647] in seconds. Split recordings after this amount of seconds. The recordings are split up into several individual + recordings, + which have the defined length (roughly). Has to be activated with `splitStrategy`. -- **splitRecordingsBiggerThanBytes** - [0 - 9223372036854775807] in bytes. Split recordings, if the size on disk exceeds this value. The recordings are split up into several individual recordings, -which have the defined size (roughly). Has to be activated with `splitStrategy`. +- **splitRecordingsBiggerThanBytes** - [0 - 9223372036854775807] in bytes. Split recordings, if the size on disk exceeds this value. The recordings are split up + into several individual recordings, + which have the defined size (roughly). Has to be activated with `splitStrategy`. - **timeoutRecordingStartingAt** - [00:00 - 23:59] - Start of the recording timeout timeframe - No new recordings will be started in this period - **timeoutRecordingEndingAt** - [00:00 - 23:59] - End of the recording timeout timeframe - No new recordings will be started in this period -- **useHlsdl** - [`true`,`false`] Use hlsdl to record the live streams. You also have to set `hlsdlExecutable`, if hlsdl is not globally available on your system. hlsdl won't be used for MV Live, LiveJasmin and Showup. +- **useHlsdl** - [`true`,`false`] Use hlsdl to record the live streams. You also have to set `hlsdlExecutable`, if hlsdl is not globally available on your + system. hlsdl won't be used for MV Live, LiveJasmin and Showup. -- **webinterface** (server only) - [`true`,`false`] Enables the webinterface for the server. You can access it with http://host:port/static/index.html Don't activate this on -a machine, which can be accessed from the internet, because this is totally unprotected at the moment. +- **webinterface** (server only) - [`true`,`false`] Enables the webinterface for the server. You can access it with http://host:port/static/index.html Don't + activate this on + a machine, which can be accessed from the internet, because this is totally unprotected at the moment. diff --git a/client/src/main/resources/html/docs/FFmpeg.md b/common/src/main/resources/docs/FFmpeg.md similarity index 77% rename from client/src/main/resources/html/docs/FFmpeg.md rename to common/src/main/resources/docs/FFmpeg.md index 4758444e..0df620c5 100644 --- a/client/src/main/resources/html/docs/FFmpeg.md +++ b/common/src/main/resources/docs/FFmpeg.md @@ -1,8 +1,10 @@ #### FFmpeg + [FFmpeg](https://ffmpeg.org) is a multimedia framework, which comes with a very powerful command line tool to process multimedia files. It is available for all major platforms. FFmpeg can be used to manually and automatically remux recordings. ##### Installation + * Linux: Use the package manager of your distribution to find and install ffmpeg * Windows: 1. Download the latest stable version from the homepage @@ -13,14 +15,16 @@ It is available for all major platforms. FFmpeg can be used to manually and auto * macOS: See Windows ##### Manual remuxing + * Open a terminal (command prompt) and cd into the directory of the recording * Run FFmpeg: - `ffmpeg -i AwesomeGirl_-2019-04-04_15-46-19_195.ts -c:v copy -c:a copy AwesomeGirl_-2019-04-04_15-46-19_195.mp4` + `ffmpeg -i AwesomeGirl_-2019-04-04_15-46-19_195.ts -c:v copy -c:a copy AwesomeGirl_-2019-04-04_15-46-19_195.mp4` - As you can see, the codecs for video and audio are set to copy, which means, that the recording is not reencoded, but just "copied" to + As you can see, the codecs for video and audio are set to copy, which means, that the recording is not reencoded, but just "copied" to another container format. You could also use mkv or avi for the output file suffix and FFmpeg would create that respective file. Depending on your hardware specs and the length of the recording, this process probably takes a few seconds up to a couple of minutes. - + ##### Automatic remuxing -FFmpeg can also be used to automatically remux recordings. ctbrec provides a [post-processing](/docs/PostProcessing.md) mechanism for this purpose. \ No newline at end of file + +FFmpeg can also be used to automatically remux recordings. ctbrec provides a [post-processing](PostProcessing.md) mechanism for this purpose. diff --git a/client/src/main/resources/html/docs/MKVToolNix.md b/common/src/main/resources/docs/MKVToolNix.md similarity index 100% rename from client/src/main/resources/html/docs/MKVToolNix.md rename to common/src/main/resources/docs/MKVToolNix.md diff --git a/client/src/main/resources/html/docs/PostProcessing.md b/common/src/main/resources/docs/PostProcessing.md similarity index 99% rename from client/src/main/resources/html/docs/PostProcessing.md rename to common/src/main/resources/docs/PostProcessing.md index c84f8069..51885310 100644 --- a/client/src/main/resources/html/docs/PostProcessing.md +++ b/common/src/main/resources/docs/PostProcessing.md @@ -137,7 +137,7 @@ The part you have to copy is -For more information see: [DateTimeFormatter](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html) +For more information see: [DateTimeFormatter](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/time/format/DateTimeFormatter.html) #### Full Example diff --git a/client/src/main/resources/html/docs/QuestionsAndAnswers.md b/common/src/main/resources/docs/QuestionsAndAnswers.md similarity index 91% rename from client/src/main/resources/html/docs/QuestionsAndAnswers.md rename to common/src/main/resources/docs/QuestionsAndAnswers.md index 307cfd54..de5d7f80 100644 --- a/client/src/main/resources/html/docs/QuestionsAndAnswers.md +++ b/common/src/main/resources/docs/QuestionsAndAnswers.md @@ -1,26 +1,33 @@ #### Are ticket / gold / private shows supported? + No. I never intended to record these kind of shows and I never tested it. You might be able to record them, but you have to figure it out yourself. #### How can I convert the recordings to mp4 / mkv? + To convert the files to another format, you have to remux them. You don't have to reencode them, since the files (usually, have not seen an exception yet) already contain H.264 video and AAC audio. You can remux the -files manually with tools like [Avidemux](/docs/Avidemux.md), [MKVToolNix](/docs/MKVToolNix.md) or -[FFmpeg](/docs/FFmpeg.md) or use one of the [post-processing](/docs/PostProcessing.md) scripts. +files manually with tools like [Avidemux](Avidemux.md), [MKVToolNix](MKVToolNix.md) or +[FFmpeg](FFmpeg.md) or use one of the [post-processing](PostProcessing.md) scripts. #### Streams are not getting recorded even though the model is online + - Is "Leave space on device set" and do you have enough space left? -- Is "Maximum resolution" set? In case maximum resolution is set and ctbrec cannot determine the +- Is "Maximum resolution" set? In case maximum resolution is set and ctbrec cannot determine the resolution of a stream, the stream will not be recorded. - Is "Concurrent Recordings" set and you reached the maximum? #### How can I playback the recorded .ts files? + Use one of the following players: + - [mpv](https://mpv.io/installation/) - [VLC](https://www.videolan.org/vlc/) #### How can I playback the server recordings? + Use one of the following players: + - [mpv](https://mpv.io/installation/) - [VLC](https://www.videolan.org/vlc/) @@ -30,17 +37,20 @@ Under **General** select the **Player** of your choice. Then you can start the p **Recordings** tab. #### The login for site XYZ does not work anymore and the credentails work in a browser -Stop CTB Recorder. Then open the settings directory (check [ConfigurationFile](/docs/ConfigurationFile.md) for the location) and + +Stop CTB Recorder. Then open the settings directory (check [ConfigurationFile](ConfigurationFile.md) for the location) and delete the cookies file for that site. Start CTB Recorder again and it should work again. If it does not work, check the log file for errors. The log file is called ctbrec.log and you can find it in the installation directory of CTB Recorder. If that does not work go back to your settings directory, go up to the parent directory and delete ctbrec-minimal-browser, if it exists. #### It takes a long time until a recording starts for a model + You probably have a lot of models in the "Recording" list. CTB Recorder checks the models one after the other. This is done on purpose to not fire too many requests in a short amount of time, because this can cause blocks by the camsites. #### Can I run several instances of CTB Recorder + It is possible to define the configuration directory and configuration file, which is used by ctbrec. This way you can create several instances with different configurations. On Windows, create a file called `ctbrec.l4j.ini` right next to `ctbrec.exe`. Add one of the following settings @@ -58,4 +68,4 @@ On Linux and macOS edit `ctbrec.sh` and add one or both of the above mentioned s `$JAVA -Dctbrec.config=alternate-settings.json -Djdk.gtk.version=3 -cp ctbrec-1.19.1-final.jar ctbrec.ui.Launcher` - \ No newline at end of file + diff --git a/client/src/main/resources/html/docs/RunningTheServer.md b/common/src/main/resources/docs/RunningTheServer.md similarity index 100% rename from client/src/main/resources/html/docs/RunningTheServer.md rename to common/src/main/resources/docs/RunningTheServer.md diff --git a/client/src/main/resources/html/docs/VideoTutorials.md b/common/src/main/resources/docs/VideoTutorials.md similarity index 100% rename from client/src/main/resources/html/docs/VideoTutorials.md rename to common/src/main/resources/docs/VideoTutorials.md diff --git a/common/src/main/resources/docs/footer.html b/common/src/main/resources/docs/footer.html new file mode 100644 index 00000000..33f86352 --- /dev/null +++ b/common/src/main/resources/docs/footer.html @@ -0,0 +1,41 @@ +

+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + diff --git a/common/src/main/resources/docs/header.html b/common/src/main/resources/docs/header.html new file mode 100644 index 00000000..22b8146c --- /dev/null +++ b/common/src/main/resources/docs/header.html @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + CTB Recorder + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+

diff --git a/server/src/main/java/ctbrec/recorder/server/HttpServer.java b/server/src/main/java/ctbrec/recorder/server/HttpServer.java index b1f1140e..7a4452ed 100644 --- a/server/src/main/java/ctbrec/recorder/server/HttpServer.java +++ b/server/src/main/java/ctbrec/recorder/server/HttpServer.java @@ -11,6 +11,9 @@ import ctbrec.image.LocalPortraitStore; import ctbrec.recorder.OnlineMonitor; import ctbrec.recorder.Recorder; import ctbrec.recorder.SimplifiedLocalRecorder; +import ctbrec.servlet.AbstractDocServlet; +import ctbrec.servlet.MarkdownServlet; +import ctbrec.servlet.SearchServlet; import ctbrec.servlet.StaticFileServlet; import ctbrec.sites.Site; import ctbrec.sites.amateurtv.AmateurTv; @@ -228,6 +231,14 @@ public class HttpServer { holder = new ServletHolder(hlsServlet); defaultContext.addServlet(holder, "/hls/*"); + var markdownServlet = new MarkdownServlet(); + holder = new ServletHolder(markdownServlet); + defaultContext.addServlet(holder, "/docs/*"); + + AbstractDocServlet searchServlet = new SearchServlet(); + holder = new ServletHolder(searchServlet); + defaultContext.addServlet(holder, "/search/*"); + LocalPortraitStore portraitStore = new LocalPortraitStore(config); ImageServlet imageServlet = new ImageServlet(portraitStore, config); holder = new ServletHolder(imageServlet); diff --git a/server/src/main/resources/html/static/ctbrec.svg b/server/src/main/resources/html/static/ctbrec.svg new file mode 100644 index 00000000..9519861f --- /dev/null +++ b/server/src/main/resources/html/static/ctbrec.svg @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/src/main/resources/html/static/favicon.svg b/server/src/main/resources/html/static/favicon.svg new file mode 100644 index 00000000..0e473f62 --- /dev/null +++ b/server/src/main/resources/html/static/favicon.svg @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/server/src/main/resources/html/static/index.html b/server/src/main/resources/html/static/index.html index 51e2262c..830f0c95 100644 --- a/server/src/main/resources/html/static/index.html +++ b/server/src/main/resources/html/static/index.html @@ -3,525 +3,559 @@ - - - - - + + + + + -CTB Recorder ${project.version} + CTB Recorder ${project.version} - - + + - - - - + + + + - - - + + + - + - - + + - - + + - + - + - -

-
-
-
-
-
-
- - -
-
-
-
-
-

-
- - - - - - - - - - - - - - - - - - - -
ModelOnlineRecording
- - -
-
-
-
-
-
-
-
-
-
- Space left: - - -
+ +
+
+
+
+
+
+ + +
+
+
+
+
+

+
+ + + + + + + + + + + + + + + + + + + +
ModelOnlineRecording
+ + + + +
+
+
+
+
+
+
+
+
+
+ Space left: + + + - - -
-
-
-
-

-
- - - - - - - - - - - - - - - - - - - - - - - - - -
ModelDateStatusProgressSize
- - - - - -
-
-
-
-
-
-
-
-
-
-
-

-
- - - - - - - - - - - - - -
ParameterValue
-
-
-
-
-
- -
-
-
-
-
-
+ + +
+
+
+
+

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
ModelDateStatusSize
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+
+ + + + + + + + + + + + + +
ParameterValue
+
+
+
+
+
+ +
+
+
+
+
+ - - - + + + - - - - - - + + + + + + - - - + + + - - + + - - - - - + - - - - - - - - + - function addModelKeyPressed(e) { - let charCode = (typeof e.which === "number") ? e.which : e.keyCode; - let val = $('#addModelByUrl').val(); + + + + + + + + + let ctbrec = { + add: function(input, onsuccess) { + try { + let model = { + type: null, + name: '', + url: input + }; + + if (console) console.log(model); + let action = input.startsWith('http') ? 'startByUrl' : 'startByName'; + let msg = '{"action": "' + action + '", "model": ' + JSON.stringify(model) + '}'; + $.ajax({ + type: 'POST', + url: '../rec', + dataType: 'json', + async: true, + timeout: 60000, + headers: {'CTBREC-HMAC': CryptoJS.HmacSHA256(msg, hmac)}, + data: msg + }) + .done(function(data) { + if (data.status === 'success') { + onsuccess.call(data); + $.notify('Model added', 'info'); + } else { + $.notify('Adding model failed', 'error'); + } + }) + .fail(function(jqXHR, textStatus, errorThrown) { + if (console) console.log(textStatus, errorThrown); + $.notify('Adding model failed', 'error'); + }); + } catch (e) { + if (console) console.log('Unexpected error', e); + } + }, + + resume: function(model) { + try { + let action = '{"action": "resume", "model": ' + JSON.stringify(model) + '}'; + $.ajax({ + type: 'POST', + url: '../rec', + dataType: 'json', + async: true, + timeout: 60000, + headers: {'CTBREC-HMAC': CryptoJS.HmacSHA256(action, hmac)}, + data: action + }) + .done(function(data) { + if (data.status === 'success') { + $.notify('Recording of ' + model.name + ' resumed', 'info'); + } else { + $.notify('Resuming recording of model ' + model.name + ' failed', 'error'); + } + }) + .fail(function(jqXHR, textStatus, errorThrown) { + if (console) console.log(textStatus, errorThrown); + $.notify('Resuming recording of model ' + model.name + ' failed', 'error'); + }); + } catch (e) { + if (console) console.log('Unexpected error', e); + } + }, + + suspend: function(model) { + try { + let action = '{"action": "suspend", "model": ' + JSON.stringify(model) + '}'; + $.ajax({ + type: 'POST', + url: '../rec', + dataType: 'json', + async: true, + timeout: 60000, + headers: {'CTBREC-HMAC': CryptoJS.HmacSHA256(action, hmac)}, + data: action + }) + .done(function(data) { + if (data.status === 'success') { + $.notify('Recording of ' + model.name + ' suspended', 'info'); + } else { + $.notify('Suspending recording of model ' + model.name + ' failed', 'error'); + } + }) + .fail(function(jqXHR, textStatus, errorThrown) { + if (console) console.log(textStatus, errorThrown); + $.notify('Suspending recording of model ' + model.name + ' failed', 'error'); + }); + } catch (e) { + if (console) console.log('Unexpected error', e); + } + }, + + stop: function(model) { + try { + let action = '{"action": "stop", "model": ' + JSON.stringify(model) + '}'; + $.ajax({ + type: 'POST', + url: '../rec', + dataType: 'json', + async: true, + timeout: 60000, + headers: {'CTBREC-HMAC': CryptoJS.HmacSHA256(action, hmac)}, + data: action + }) + .done(function(data) { + if (data.status === 'success') { + $.notify('Removed ' + model.name, 'info'); + observableModelsArray.remove(model); + } else { + $.notify('Removing model ' + model.name + ' failed', 'error'); + } + }) + .fail(function(jqXHR, textStatus, errorThrown) { + if (console) console.log(textStatus, errorThrown); + $.notify('Removing model ' + model.name + ' failed', 'error'); + }); + } catch (e) { + if (console) console.log('Unexpected error', e); + } + }, + + rerunProcessing: function(recording) { + let name = recording.model.name + ' ' + recording.ko_date(); + try { + let action = '{"action": "rerunPostProcessing", "recording": ' + JSON.stringify(recording) + '}'; + $.ajax({ + type: 'POST', + url: '../rec', + dataType: 'json', + async: true, + timeout: 60000, + headers: {'CTBREC-HMAC': CryptoJS.HmacSHA256(action, hmac)}, + data: action + }); + } catch (e) { + if (console) console.log('Unexpected error', e); + } + }, + + deleteRecording: function(recording) { + let name = recording.model.name + ' ' + recording.ko_date(); + try { + let action = '{"action": "delete", "recording": ' + JSON.stringify(recording) + '}'; + $.ajax({ + type: 'POST', + url: '../rec', + dataType: 'json', + async: true, + timeout: 60000, + headers: {'CTBREC-HMAC': CryptoJS.HmacSHA256(action, hmac)}, + data: action + }) + .done(function(data) { + if (data.status === 'success') { + $.notify('Removed recording ' + name, 'info'); + observableRecordingsArray.remove(recording); + } else { + $.notify('Removing recording ' + name + ' failed', 'error'); + } + }) + .fail(function(jqXHR, textStatus, errorThrown) { + if (console) console.log(textStatus, errorThrown); + $.notify('Removing recording ' + name + ' failed', 'error'); + }); + } catch (e) { + if (console) console.log('Unexpected error', e); + } + } + }; + + $(document).ready(function() { + if (localStorage !== undefined && localStorage.hmac !== undefined) { + if (console) console.log('using hmac from local storage'); + hmac = localStorage.hmac; + } else { + if (console) console.log('hmac not found in local storage. requesting hmac from server'); + $.ajax({ + type: 'GET', + url: '../secured/hmac', + dataType: 'json', + async: true, + timeout: 60000 + }) + .done(function(data) { + hmac = data.hmac; + if (localStorage !== undefined) { + if (console) console.log('saving hmac to local storage'); + localStorage.setItem("hmac", hmac); + } + }) + .fail(function(jqXHR, textStatus, errorThrown) { + if (console) console.log(textStatus, errorThrown); + $.notify('Could not get HMAC', 'error'); + hmac = ''; + }); + } + + function tab(id) { + $(id).css('display: block'); + } + + var navMain = $("#mainNav"); + navMain.on("click", "a", null, function() { + navMain.collapse('hide'); + $('#navbarResponsive').collapse('hide'); + }); + + ko.applyBindings({ + models: observableModelsArray, + recordings: observableRecordingsArray, + settings: observableSettingsArray, + space, + throughput + }); + + updateOnlineModels(); + updateRecordings(); + + $('a[data-toggle="tab"]').on('shown.bs.tab', function(e) { + let selectedTab = e.target.attributes['id'].value; + if (selectedTab === 'configuration-tab') { + loadConfig(); + } + }); + loadConfig(); + + $('#dark-mode-toggle').click(toggleDarkMode); + + if (cookieJar.get('darkMode') === 'true') { + toggleDarkMode(); + } + }); + + + function toggleDarkMode() { + darkMode = !darkMode; + var newCss = document.createElement("link"); + newCss.setAttribute("rel", "stylesheet"); + newCss.setAttribute("type", "text/css"); + newCss.setAttribute("href", darkMode ? "freelancer-dark.css" : "freelancer.css"); + console.log(darkMode, newCss); + $('#mainTheme').remove(); + $('head').append(newCss); + $('#dark-mode-toggle').removeClass('fa-moon'); + $('#dark-mode-toggle').removeClass('fa-sun'); + $('#dark-mode-toggle').addClass(darkMode ? 'fa-sun' : 'fa-moon'); + cookieJar.set("darkMode", darkMode); + } + + - - -