From f2df8deb0c21a809eed783b643d472c704d012a7 Mon Sep 17 00:00:00 2001
From: 0xb00bface <0xboobface@gmail.com>
Date: Sat, 4 Mar 2023 15:14:59 +0100
Subject: [PATCH] Add playground dialog for post-processing variables/functions
and update the documentation
---
.../java/ctbrec/ui/settings/SettingsTab.java | 9 ++-
.../VariablePlayGroundDialogFactory.java | 76 +++++++++++++++++++
.../resources/html/docs/PostProcessing.md | 49 +++++++-----
common/src/main/java/ctbrec/Settings.java | 2 +-
...AbstractPlaceholderAwarePostProcessor.java | 19 ++---
.../ctbrec/recorder/postprocessing/Move.java | 2 +-
.../recorder/postprocessing/Rename.java | 4 +-
.../ModelVariableExpander.java | 17 +----
.../functions/Capitalize.java | 5 +-
.../variableexpansion/functions/Format.java | 33 ++++----
.../variableexpansion/functions/Lower.java | 5 +-
.../variableexpansion/functions/OrElse.java | 10 ++-
.../variableexpansion/functions/Sanitize.java | 5 +-
.../variableexpansion/functions/Trim.java | 5 +-
.../variableexpansion/functions/Upper.java | 5 +-
...ractPlaceholderAwarePostProcessorTest.java | 6 +-
.../postprocessing/RenameDirectoryTest.java | 17 ++---
.../functions/CapitalizeTest.java | 31 ++++++++
.../functions/FormatTest.java | 55 ++++++++++++++
.../functions/LowerTest.java | 31 ++++++++
.../functions/OrElseTest.java | 29 +++++++
.../functions/SanitizeTest.java | 35 +++++++++
.../variableexpansion/functions/TrimTest.java | 33 ++++++++
.../functions/UpperTest.java | 31 ++++++++
24 files changed, 432 insertions(+), 82 deletions(-)
create mode 100644 client/src/main/java/ctbrec/ui/settings/VariablePlayGroundDialogFactory.java
create mode 100644 common/src/test/java/ctbrec/variableexpansion/functions/CapitalizeTest.java
create mode 100644 common/src/test/java/ctbrec/variableexpansion/functions/FormatTest.java
create mode 100644 common/src/test/java/ctbrec/variableexpansion/functions/LowerTest.java
create mode 100644 common/src/test/java/ctbrec/variableexpansion/functions/OrElseTest.java
create mode 100644 common/src/test/java/ctbrec/variableexpansion/functions/SanitizeTest.java
create mode 100644 common/src/test/java/ctbrec/variableexpansion/functions/TrimTest.java
create mode 100644 common/src/test/java/ctbrec/variableexpansion/functions/UpperTest.java
diff --git a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java
index 1ac57835..39ca8e2d 100644
--- a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java
+++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java
@@ -278,7 +278,8 @@ public class SettingsTab extends Tab implements TabSelectionListener {
Group.of("Post-Processing",
Setting.of("Threads", postProcessingThreads),
Setting.of("Steps", postProcessingStepPanel),
- Setting.of("", createHelpButton("Post-Processing Help", "http://localhost:5689/docs/PostProcessing.md")))),
+ Setting.of("", createHelpButton("Post-Processing Help", "http://localhost:5689/docs/PostProcessing.md")),
+ Setting.of("", createVariablePlayGroundButton()))),
Category.of("Events & Actions", new ActionSettingsPanel(recorder)), Category.of("Ignore List", ignoreList),
Category.of("Sites", siteCategories.toArray(new Category[0])),
Category.of("Proxy",
@@ -377,6 +378,12 @@ public class SettingsTab extends Tab implements TabSelectionListener {
return postProcessingHelpButton;
}
+ private Button createVariablePlayGroundButton() {
+ var postProcessingHelpButton = new Button("Variable Playground");
+ postProcessingHelpButton.setOnAction(e -> new VariablePlayGroundDialogFactory().openDialog(this.getTabPane().getScene(), config, recorder));
+ return postProcessingHelpButton;
+ }
+
private void bindEnabledProperty(Setting s, BooleanExpression bindTo) {
try {
s.getGui().disableProperty().bind(bindTo);
diff --git a/client/src/main/java/ctbrec/ui/settings/VariablePlayGroundDialogFactory.java b/client/src/main/java/ctbrec/ui/settings/VariablePlayGroundDialogFactory.java
new file mode 100644
index 00000000..25a00b49
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/settings/VariablePlayGroundDialogFactory.java
@@ -0,0 +1,76 @@
+package ctbrec.ui.settings;
+
+import ctbrec.Config;
+import ctbrec.Recording;
+import ctbrec.StringUtil;
+import ctbrec.UnknownModel;
+import ctbrec.recorder.Recorder;
+import ctbrec.recorder.postprocessing.PostProcessingContext;
+import ctbrec.sites.chaturbate.Chaturbate;
+import ctbrec.ui.controls.Dialogs;
+import ctbrec.ui.tabs.DownloadPostprocessor;
+import ctbrec.variableexpansion.functions.AntlrSyntacErrorAdapter;
+import javafx.scene.Scene;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextField;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Priority;
+import org.antlr.v4.runtime.RecognitionException;
+import org.antlr.v4.runtime.Recognizer;
+
+import java.nio.file.Paths;
+import java.time.Instant;
+
+public class VariablePlayGroundDialogFactory {
+ public void openDialog(Scene parent, Config config, Recorder recorder) {
+ Chaturbate chaturbate = new Chaturbate();
+ UnknownModel unknownModel = new UnknownModel();
+ unknownModel.setName("Pussy_Galore");
+ unknownModel.setDisplayName("Pussy Galore");
+ unknownModel.setSite(chaturbate);
+ Recording recording = new Recording();
+ recording.setAbsoluteFile(Paths.get("ctbrec", "recs", "pussy_galore", "2023-02-26_14-05-56").toFile());
+ recording.setStartDate(Instant.now());
+ recording.setStatus(Recording.State.POST_PROCESSING);
+ recording.setNote("notes about the recording");
+ recording.setModel(unknownModel);
+ PostProcessingContext ctx = new PostProcessingContext();
+ ctx.setConfig(config);
+ ctx.setRecorder(recorder);
+ ctx.setRecording(recording);
+ DownloadPostprocessor postprocessor = new DownloadPostprocessor();
+
+ GridPane pane = new GridPane();
+ Label result = new Label();
+ Label error = new Label();
+
+ pane.add(new Label("Expression"), 0, 0);
+ TextField input = new TextField();
+ input.setMinWidth(600);
+ pane.add(input, 1, 0);
+ GridPane.setHgrow(input, Priority.ALWAYS);
+ input.setOnKeyTyped(evt -> {
+ String r = postprocessor.fillInPlaceHolders(input.getText(), ctx, new AntlrSyntacErrorAdapter() {
+ @Override
+ public void syntaxError(Recognizer, ?> recognizer, Object o, int line, int pos, String s, RecognitionException e) {
+ error.setText(String.format("Syntax error at %d:%d %s", line, pos, s));
+ }
+ });
+ result.setText(r);
+ if (StringUtil.isNotBlank(r)) {
+ error.setText("");
+ }
+ });
+
+ pane.add(error, 0, 1);
+ GridPane.setColumnSpan(error, 2);
+
+ pane.add(result, 0, 2);
+ GridPane.setColumnSpan(result, 2);
+
+ pane.setHgap(5);
+ pane.vgapProperty().bind(pane.hgapProperty());
+ Dialogs.showCustomInput(parent, "Playground", pane, (obs, oldV, newV) -> {
+ });
+ }
+}
diff --git a/client/src/main/resources/html/docs/PostProcessing.md b/client/src/main/resources/html/docs/PostProcessing.md
index 9cc13eeb..c84f8069 100644
--- a/client/src/main/resources/html/docs/PostProcessing.md
+++ b/client/src/main/resources/html/docs/PostProcessing.md
@@ -1,8 +1,10 @@
#### Post-Processing
+
The post-processing gives you the possibility to execute different actions after a recording has finished. You can use that to convert
the files to another format, create preview images, rename / move the file etc.
##### Available Steps
+
- **create a copy** - Creates a copy of the original recording. All following post-processing steps are executed on the copy, not on the
original recording. This means, that the post-processing can be rerun in case a step failed, because the original recording is still
available.
@@ -21,17 +23,20 @@ the files to another format, create preview images, rename / move the file etc.
- **create contactsheet** - create a contact sheet with preview images of the recording
#### Planned for future releases
+
- **call a webhook** - call a URL once a recording is finished
- **create timeline thumbnails** - create a small thumbnail for every second or every few seconds, which can be used to very fast
scan through a recording
#### How to configure the server to do post-processing
+
There is currently no user interface to configure the post-processing for the server. It has to be added manually to the server config.
I suggest to start the app and configure the post-processing steps in the settings. Afterwards you close the app and copy the
post-processing section from the settings.json to your server.json file. To find out, where these files are on your system, read
[Configuration File](ConfigurationFile.md).
The part you have to copy is
+
```
postProcessors: [
...
@@ -43,16 +48,13 @@ The part you have to copy is
###### Available variables:
+
- **${modelName}** - the name of the recorded model
-- **${modelDisplayName}** - the name of the recorded model, which is shown on the webpage. Might be the same as
+- **${modelDisplayName}** - the name of the recorded model, which is shown on the webpage. Might be the same as
${modelName}
-- **${modelSanitizedName}** - sanitized name of the model. The following characters are replaced by an underscore:
- \\, /, ', " and space
- **${modelGroupName}** - name of the model group, if the model is part of a group
- **${modelGroupId}** - the unique ID of the model group, if the model is part of a group
- **${siteName}** - the name of the cam site, the model streams on
-- **${siteSanitizedName}** - sanitized name of the site. The following characters are replaced by an underscore:
- \\, /, ', " and space
- **${fileSuffix}** - the file extension of the recording. E.g. ts or mp4. In case of a standard server recording,
this will be empty
- **${epochSecond}** - timestamp of the recording in seconds since 1970-01-01 (unixtime)
@@ -60,16 +62,31 @@ The part you have to copy is
\\, /, ', " and space
- **${recordingNotes}** - sanitized recording notes. The following characters are replaced by an underscore:
\\, /, ', " and space. Useful for the download of recordings from the server.
-- **${recordingsDir}** - the base directory of all recordings. Same as Recordings Directory in the Recorder settings
+- **${recordingsDir}** - the base directory of all recordings. Same as Recordings Directory in the Recorder settings
section.
- **${absolutePath}** - the absolute path in the filesystem to the recording file (or the recording directory in case of
a server recording)
-- **${absoluteParentPath}** - the absolute path to the parent directory of the recording in the filesystem (or the
+- **${absoluteParentPath}** - the absolute path to the parent directory of the recording in the filesystem (or the
recording dir in case of a server recording)
-- **${utcDateTime}** and **${localDateTime}** - the timestamp of the recording in the UTC or your local timezone. If no
- pattern is given, the default ```yyyy-MM-dd_HH-mm-ss``` is used. You can also define your own pattern using the following
- symbols. For example ```${localDateTime(yyyyMMdd-HHmmss)}``` would lead to 20200928-173605.
+- **${utcDateTime}** and **${localDateTime}** - the timestamp of the recording in the UTC or your local timezone
+
+#### Functions
+
+- **$trim** - removes all leading and trailing space - `$trim( hello world )` becomes `hello world`
+- **$upper** - converts all of the characters to upper case - `$upper(hello world)` becomes `HELLO WORLD`
+- **$lower** - converts all of the characters to lower case - `$lower(hElLo WORLD)` becomes `hello world`
+- **$capitalize** - capitalizes words changing the first character to upper case - `$capitalize(hElLo WorLD)` becomes `HElLo WorLD`
+- **$sanitize** - removes problematic characters - `$sanitize(hEl'Lo / WO"RLD)` becomes `hEl_Lo___WO_RLD`. The following characters are replaced by an
+ underscore:
+ \\, /, ', " and space
+- **$orElse** - provide an alternative in case a variable is not set - `$orElse(${variable},someValue)` - becomes `${variable}`, if it is set or `someValue`, if
+ `${variable}` is not set
+- **$format** - formats a date, can be used with a pattern or without.
+ Examples:
+ - `$format(${localDateTime})` - becomes something like `2023-02-26_12-23-15`
+ - `$format(${localDateTime},yyyyMMdd-HHmmss)` would lead to `20200928-173605`
+
Symbol
Meaning
Presentation
Examples
@@ -118,16 +135,10 @@ 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)
-###### Fallback values:
-You can define a fallback value for each variable in case there is no value available for the variable. The syntax is
- ${placeholder?foobar}
-Let's for example say you have created some model groups. For models, which are part of a group, you want to use the group name. But for models, which
-are not part of a group you want to use the sanitized name. You can achieve that by using the following expression:
+For more information see: [DateTimeFormatter](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html)
- ${modelGroupName?${modelSanitizedName}}
+#### Full Example
-It can be read like "use the modelGroupName, but if that is not available use modelSanitizedName".
\ No newline at end of file
+`$orElse(${modelGroupName},$sanitize(${modelName}))_$format(${localDateTime})_${recordingNotes}`
diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java
index 6db0dbb0..28083645 100644
--- a/common/src/main/java/ctbrec/Settings.java
+++ b/common/src/main/java/ctbrec/Settings.java
@@ -66,7 +66,7 @@ public class Settings {
public int defaultPriority = 50;
public boolean determineResolution = false;
public List disabledSites = new ArrayList<>();
- public String downloadFilename = "${modelSanitizedName}-$format(${localDateTime})";
+ public String downloadFilename = "$sanitize(${modelName})_$format(${localDateTime})";
public List eventHandlers = new ArrayList<>();
public boolean eventsSuspended = false;
public boolean fastScrollSpeed = true;
diff --git a/common/src/main/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessor.java b/common/src/main/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessor.java
index 8ab1d590..b96425ba 100644
--- a/common/src/main/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessor.java
+++ b/common/src/main/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessor.java
@@ -10,7 +10,9 @@ import ctbrec.variableexpansion.antlr.PostProcessingLexer;
import ctbrec.variableexpansion.antlr.PostProcessingParser;
import ctbrec.variableexpansion.functions.AntlrSyntacErrorAdapter;
import lombok.extern.slf4j.Slf4j;
-import org.antlr.v4.runtime.*;
+import org.antlr.v4.runtime.CharStream;
+import org.antlr.v4.runtime.CharStreams;
+import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTree;
import java.io.IOException;
@@ -28,6 +30,10 @@ import static java.util.Optional.ofNullable;
public abstract class AbstractPlaceholderAwarePostProcessor extends AbstractPostProcessor {
public String fillInPlaceHolders(String input, PostProcessingContext ctx) {
+ return this.fillInPlaceHolders(input, ctx, null);
+ }
+
+ public String fillInPlaceHolders(String input, PostProcessingContext ctx, AntlrSyntacErrorAdapter errorListener) {
Recording rec = ctx.getRecording();
Config config = ctx.getConfig();
@@ -42,21 +48,16 @@ public abstract class AbstractPlaceholderAwarePostProcessor extends AbstractPost
variables.put("utcDateTime", getUtcDateTime(rec));
variables.put("localDateTime", getLocalDateTime(rec));
- return fillInPlaceHolders(input, variables);
+ return fillInPlaceHolders(input, variables, errorListener);
}
- private String fillInPlaceHolders(String input, Map> variables) {
+ private String fillInPlaceHolders(String input, Map> variables, AntlrSyntacErrorAdapter errorListener) {
try (StringReader reader = new StringReader(input)) {
CharStream s = CharStreams.fromReader(reader);
PostProcessingLexer lexer = new PostProcessingLexer(s);
CommonTokenStream tokens = new CommonTokenStream(lexer);
PostProcessingParser parser = new PostProcessingParser(tokens);
- parser.addErrorListener(new AntlrSyntacErrorAdapter() {
- @Override
- public void syntaxError(Recognizer, ?> recognizer, Object o, int line, int pos, String s, RecognitionException e) {
- log.warn("Syntax error at {}:{} {}", line, pos, s);
- }
- });
+ Optional.ofNullable(errorListener).ifPresent(parser::addErrorListener);
ParseTree parseTree = parser.line();
ParserVisitor visitor = new ParserVisitor(variables);
return visitor.visit(parseTree);
diff --git a/common/src/main/java/ctbrec/recorder/postprocessing/Move.java b/common/src/main/java/ctbrec/recorder/postprocessing/Move.java
index c8f6b6c4..78470449 100644
--- a/common/src/main/java/ctbrec/recorder/postprocessing/Move.java
+++ b/common/src/main/java/ctbrec/recorder/postprocessing/Move.java
@@ -16,7 +16,7 @@ public class Move extends AbstractPlaceholderAwarePostProcessor {
private static final Logger LOG = LoggerFactory.getLogger(Move.class);
public static final String PATH_TEMPLATE = "path.template";
- public static final String DEFAULT = "${modelSanitizedName}" + File.separatorChar + "$format(${localDateTime})";
+ public static final String DEFAULT = "$sanitize(${modelName})" + File.separatorChar + "$format(${localDateTime})";
@Override
public String getName() {
diff --git a/common/src/main/java/ctbrec/recorder/postprocessing/Rename.java b/common/src/main/java/ctbrec/recorder/postprocessing/Rename.java
index a9a2bd82..879977ee 100644
--- a/common/src/main/java/ctbrec/recorder/postprocessing/Rename.java
+++ b/common/src/main/java/ctbrec/recorder/postprocessing/Rename.java
@@ -13,8 +13,8 @@ public class Rename extends AbstractPlaceholderAwarePostProcessor {
private static final Logger LOG = LoggerFactory.getLogger(Rename.class);
public static final String FILE_NAME_TEMPLATE = "filename.template";
- public static final String DEFAULT = "${modelSanitizedName}_$format(${localDateTime}).${fileSuffix}";
- public static final String DEFAULT_DIR = "${modelSanitizedName}_$format(${localDateTime})";
+ public static final String DEFAULT = "$sanitize(${modelName})_$format(${localDateTime}).${fileSuffix}";
+ public static final String DEFAULT_DIR = "$sanitize(${modelName})_$format(${localDateTime})";
@Override
public String getName() {
diff --git a/common/src/main/java/ctbrec/variableexpansion/ModelVariableExpander.java b/common/src/main/java/ctbrec/variableexpansion/ModelVariableExpander.java
index 1db6e774..323d059b 100644
--- a/common/src/main/java/ctbrec/variableexpansion/ModelVariableExpander.java
+++ b/common/src/main/java/ctbrec/variableexpansion/ModelVariableExpander.java
@@ -18,10 +18,8 @@ public class ModelVariableExpander extends AbstractVariableExpander {
Optional modelGroup = Optional.ofNullable(recorder).flatMap(r -> r.getModelGroup(model));
placeholderValueSuppliers.put("modelName", ofNullable(model.getName()));
placeholderValueSuppliers.put("modelDisplayName", ofNullable(model.getDisplayName()));
- placeholderValueSuppliers.put("modelSanitizedName",getSanitizedName(model));
- placeholderValueSuppliers.put("modelNotes",getSanitizedModelNotes(config, model));
+ placeholderValueSuppliers.put("modelNotes", getSanitizedModelNotes(config, model));
placeholderValueSuppliers.put("siteName", ofNullable(model).map(Model::getSite).map(Site::getName));
- placeholderValueSuppliers.put("siteSanitizedName",getSanitizedSiteName(model));
placeholderValueSuppliers.put("modelGroupName", modelGroup.map(ModelGroup::getName));
placeholderValueSuppliers.put("modelGroupId", modelGroup.map(ModelGroup::getId).map(UUID::toString));
}
@@ -30,19 +28,6 @@ public class ModelVariableExpander extends AbstractVariableExpander {
return fillInPlaceHolders(input, placeholderValueSuppliers);
}
- private Optional