diff --git a/client/pom.xml b/client/pom.xml
index d2e7e34a..78a12194 100644
--- a/client/pom.xml
+++ b/client/pom.xml
@@ -8,7 +8,7 @@
ctbrec
master
- 4.5.2
+ 4.5.3
../master
diff --git a/client/src/main/java/ctbrec/ui/TimeTextFieldTest.java b/client/src/main/java/ctbrec/ui/TimeTextFieldTest.java
new file mode 100644
index 00000000..ed8c405b
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/TimeTextFieldTest.java
@@ -0,0 +1,258 @@
+package ctbrec.ui;
+import java.util.regex.Pattern;
+
+import javafx.application.Application;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.IntegerBinding;
+import javafx.beans.property.ReadOnlyIntegerProperty;
+import javafx.beans.property.ReadOnlyIntegerWrapper;
+import javafx.geometry.Insets;
+import javafx.scene.Scene;
+import javafx.scene.control.IndexRange;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextField;
+import javafx.scene.layout.VBox;
+import javafx.stage.Stage;
+
+public class TimeTextFieldTest extends Application {
+
+ @Override
+ public void start(Stage primaryStage) {
+ VBox root = new VBox(5);
+ root.setPadding(new Insets(5));
+ Label hrLabel = new Label();
+ Label minLabel = new Label();
+ Label secLabel = new Label();
+ TimeTextField timeTextField = new TimeTextField();
+ hrLabel.textProperty().bind(Bindings.format("Hours: %d", timeTextField.hoursProperty()));
+ minLabel.textProperty().bind(Bindings.format("Minutes: %d", timeTextField.minutesProperty()));
+ secLabel.textProperty().bind(Bindings.format("Seconds: %d", timeTextField.secondsProperty()));
+
+ root.getChildren().addAll(timeTextField, hrLabel, minLabel, secLabel);
+ Scene scene = new Scene(root);
+ primaryStage.setScene(scene);
+ primaryStage.show();
+ }
+
+ public static void main(String[] args) {
+ launch(args);
+
+ }
+
+ public static class TimeTextField extends TextField {
+
+ enum Unit {
+ HOURS, MINUTES, SECONDS
+ };
+
+ private final Pattern timePattern;
+ private final ReadOnlyIntegerWrapper hours;
+ private final ReadOnlyIntegerWrapper minutes;
+ private final ReadOnlyIntegerWrapper seconds;
+
+ public TimeTextField() {
+ this("00:00:00");
+ }
+
+ public TimeTextField(String time) {
+ super(time);
+ timePattern = Pattern.compile("\\d\\d:\\d\\d:\\d\\d");
+ if (!validate(time)) {
+ throw new IllegalArgumentException("Invalid time: " + time);
+ }
+ hours = new ReadOnlyIntegerWrapper(this, "hours");
+ minutes = new ReadOnlyIntegerWrapper(this, "minutes");
+ seconds = new ReadOnlyIntegerWrapper(this, "seconds");
+ hours.bind(new TimeTextField.TimeUnitBinding(Unit.HOURS));
+ minutes.bind(new TimeTextField.TimeUnitBinding(Unit.MINUTES));
+ seconds.bind(new TimeTextField.TimeUnitBinding(Unit.SECONDS));
+ }
+
+ public ReadOnlyIntegerProperty hoursProperty() {
+ return hours.getReadOnlyProperty();
+ }
+
+ public int getHours() {
+ return hours.get();
+ }
+
+ public ReadOnlyIntegerProperty minutesProperty() {
+ return minutes.getReadOnlyProperty();
+ }
+
+ public int getMinutes() {
+ return minutes.get();
+ }
+
+ public ReadOnlyIntegerProperty secondsProperty() {
+ return seconds.getReadOnlyProperty();
+ }
+
+ public int getSeconds() {
+ return seconds.get();
+ }
+
+ @Override
+ public void appendText(String text) {
+ // Ignore this. Our text is always 8 characters long, we cannot append anything
+ }
+
+ @Override
+ public boolean deleteNextChar() {
+
+ boolean success = false;
+
+ // If there's a selection, delete it:
+ final IndexRange selection = getSelection();
+ if (selection.getLength() > 0) {
+ int selectionEnd = selection.getEnd();
+ this.deleteText(selection);
+ this.positionCaret(selectionEnd);
+ success = true;
+ } else {
+ // If the caret preceeds a digit, replace that digit with a zero and move the caret forward. Else just move the caret forward.
+ int caret = this.getCaretPosition();
+ if (caret % 3 != 2) { // not preceeding a colon
+ String currentText = this.getText();
+ setText(currentText.substring(0, caret) + "0" + currentText.substring(caret + 1));
+ success = true;
+ }
+ this.positionCaret(Math.min(caret + 1, this.getText().length()));
+ }
+ return success;
+ }
+
+ @Override
+ public boolean deletePreviousChar() {
+
+ boolean success = false;
+
+ // If there's a selection, delete it:
+ final IndexRange selection = getSelection();
+ if (selection.getLength() > 0) {
+ int selectionStart = selection.getStart();
+ this.deleteText(selection);
+ this.positionCaret(selectionStart);
+ success = true;
+ } else {
+ // If the caret is after a digit, replace that digit with a zero and move the caret backward. Else just move the caret back.
+ int caret = this.getCaretPosition();
+ if (caret % 3 != 0) { // not following a colon
+ String currentText = this.getText();
+ setText(currentText.substring(0, caret - 1) + "0" + currentText.substring(caret));
+ success = true;
+ }
+ this.positionCaret(Math.max(caret - 1, 0));
+ }
+ return success;
+ }
+
+ @Override
+ public void deleteText(IndexRange range) {
+ this.deleteText(range.getStart(), range.getEnd());
+ }
+
+ @Override
+ public void deleteText(int begin, int end) {
+ // Replace all digits in the given range with zero:
+ StringBuilder builder = new StringBuilder(this.getText());
+ for (int c = begin; c < end; c++) {
+ if (c % 3 != 2) { // Not at a colon:
+ builder.replace(c, c + 1, "0");
+ }
+ }
+ this.setText(builder.toString());
+ }
+
+ @Override
+ public void insertText(int index, String text) {
+ // Handle an insert by replacing the range from index to index+text.length() with text, if that results in a valid string:
+ StringBuilder builder = new StringBuilder(this.getText());
+ builder.replace(index, index + text.length(), text);
+ final String testText = builder.toString();
+ if (validate(testText)) {
+ this.setText(testText);
+ }
+ this.positionCaret(index + text.length());
+ }
+
+ @Override
+ public void replaceSelection(String replacement) {
+ final IndexRange selection = this.getSelection();
+ if (selection.getLength() == 0) {
+ this.insertText(selection.getStart(), replacement);
+ } else {
+ this.replaceText(selection.getStart(), selection.getEnd(), replacement);
+ }
+ }
+
+ @Override
+ public void replaceText(IndexRange range, String text) {
+ this.replaceText(range.getStart(), range.getEnd(), text);
+ }
+
+ @Override
+ public void replaceText(int begin, int end, String text) {
+ if (begin == end) {
+ this.insertText(begin, text);
+ } else {
+ // only handle this if text.length() is equal to the number of characters being replaced, and if the replacement results in a valid string:
+ if (text.length() == end - begin) {
+ StringBuilder builder = new StringBuilder(this.getText());
+ builder.replace(begin, end, text);
+ String testText = builder.toString();
+ if (validate(testText)) {
+ this.setText(testText);
+ }
+ this.positionCaret(end);
+ }
+ }
+ }
+
+ private boolean validate(String time) {
+ if (!timePattern.matcher(time).matches()) {
+ return false;
+ }
+ String[] tokens = time.split(":");
+ assert tokens.length == 3;
+ try {
+ int hours = Integer.parseInt(tokens[0]);
+ int mins = Integer.parseInt(tokens[1]);
+ int secs = Integer.parseInt(tokens[2]);
+ if (hours < 0 || hours > 23) {
+ return false;
+ }
+ if (mins < 0 || mins > 59) {
+ return false;
+ }
+ if (secs < 0 || secs > 59) {
+ return false;
+ }
+ return true;
+ } catch (NumberFormatException nfe) {
+ // regex matching should assure we never reach this catch block
+ assert false;
+ return false;
+ }
+ }
+
+ private final class TimeUnitBinding extends IntegerBinding {
+
+ final Unit unit;
+
+ TimeUnitBinding(Unit unit) {
+ this.bind(textProperty());
+ this.unit = unit;
+ }
+
+ @Override
+ protected int computeValue() {
+ // Crazy enum magic
+ String token = getText().split(":")[unit.ordinal()];
+ return Integer.parseInt(token);
+ }
+
+ }
+
+ }
+}
diff --git a/client/src/main/java/ctbrec/ui/controls/TimePicker.java b/client/src/main/java/ctbrec/ui/controls/TimePicker.java
new file mode 100644
index 00000000..223cb418
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/controls/TimePicker.java
@@ -0,0 +1,67 @@
+package ctbrec.ui.controls;
+
+import java.time.LocalTime;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoUnit;
+import java.util.Optional;
+
+import javafx.scene.control.Spinner;
+import javafx.scene.control.SpinnerValueFactory;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.input.ScrollEvent;
+
+public class TimePicker extends Spinner {
+
+ public TimePicker() {
+ this(LocalTime.now().truncatedTo(ChronoUnit.MINUTES));
+ }
+
+ public TimePicker(LocalTime value) {
+ setValueFactory(new TimePickerValueFactory());
+ getValueFactory().setValue(value);
+ setEditable(true);
+ getEditor().setOnKeyReleased(this::updateValueFromInput);
+ getEditor().focusedProperty().addListener((obs, oldV, focused) -> {
+ if (Boolean.FALSE.equals(focused)) {
+ getEditor().setText(getValue().toString());
+ }
+ });
+ setOnScroll(this::onScroll);
+ }
+
+ private void onScroll(ScrollEvent evt) {
+ int d = (int) evt.getDeltaY();
+ int units = (int) (Math.abs(d) / evt.getMultiplierY());
+ if (d > 0) {
+ getValueFactory().increment(units);
+ } else {
+ getValueFactory().decrement(units);
+ }
+ evt.consume();
+ }
+
+ private void updateValueFromInput(KeyEvent evt) {
+ String input = getEditor().getText();
+ try {
+ LocalTime newValue = LocalTime.parse(input);
+ getValueFactory().setValue(newValue);
+ } catch (DateTimeParseException e) {
+ // input is invalid, we do nothing
+ }
+ }
+
+ private class TimePickerValueFactory extends SpinnerValueFactory {
+ @Override
+ public void decrement(int steps) {
+ LocalTime time = Optional.ofNullable(getValue()).orElse(LocalTime.now().truncatedTo(ChronoUnit.MINUTES));
+ setValue(time.minusMinutes(steps));
+
+ }
+
+ @Override
+ public void increment(int steps) {
+ LocalTime time = Optional.ofNullable(getValue()).orElse(LocalTime.now().truncatedTo(ChronoUnit.MINUTES));
+ setValue(time.plusMinutes(steps));
+ }
+ }
+}
diff --git a/client/src/main/java/ctbrec/ui/settings/CtbrecPreferencesStorage.java b/client/src/main/java/ctbrec/ui/settings/CtbrecPreferencesStorage.java
index 30e2d795..72117c9c 100644
--- a/client/src/main/java/ctbrec/ui/settings/CtbrecPreferencesStorage.java
+++ b/client/src/main/java/ctbrec/ui/settings/CtbrecPreferencesStorage.java
@@ -1,6 +1,7 @@
package ctbrec.ui.settings;
import java.io.IOException;
+import java.time.LocalTime;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -13,9 +14,11 @@ import ctbrec.Settings;
import ctbrec.StringUtil;
import ctbrec.ui.controls.DirectorySelectionBox;
import ctbrec.ui.controls.ProgramSelectionBox;
+import ctbrec.ui.controls.TimePicker;
import ctbrec.ui.controls.range.DiscreteRange;
import ctbrec.ui.controls.range.RangeSlider;
import ctbrec.ui.settings.api.ExclusiveSelectionProperty;
+import ctbrec.ui.settings.api.LocalTimeProperty;
import ctbrec.ui.settings.api.Preferences;
import ctbrec.ui.settings.api.PreferencesStorage;
import ctbrec.ui.settings.api.Setting;
@@ -82,6 +85,8 @@ public class CtbrecPreferencesStorage implements PreferencesStorage {
return createDirectorySelector(setting);
} else if (prop instanceof SimpleFileProperty) {
return createFileSelector(setting);
+ } else if (prop instanceof LocalTimeProperty) {
+ return createTimeSelector(setting);
} else if (prop instanceof IntegerProperty) {
return createIntegerProperty(setting);
} else if (prop instanceof LongProperty) {
@@ -212,6 +217,23 @@ public class CtbrecPreferencesStorage implements PreferencesStorage {
return directorySelector;
}
+ private Node createTimeSelector(Setting setting) {
+ LocalTime time = (LocalTime) setting.getProperty().getValue();
+ var timePicker = new TimePicker(time);
+ timePicker.valueProperty().addListener((obs, o, n) -> saveValue(() -> {
+ var field = Settings.class.getField(setting.getKey());
+ LocalTime oldValue = (LocalTime) field.get(settings);
+ if (!Objects.equals(n, oldValue)) {
+ field.set(settings, n); // NOSONAR
+ if (setting.doesNeedRestart()) {
+ runRestartRequiredCallback();
+ }
+ config.save();
+ }
+ }));
+ return timePicker;
+ }
+
private Node createStringProperty(Setting setting) {
var ctrl = new TextField();
ctrl.textProperty().addListener((obs, oldV, newV) -> saveValue(() -> {
diff --git a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java
index 5208e05a..ae637028 100644
--- a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java
+++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java
@@ -31,6 +31,7 @@ import ctbrec.ui.settings.api.Category;
import ctbrec.ui.settings.api.ExclusiveSelectionProperty;
import ctbrec.ui.settings.api.GigabytesConverter;
import ctbrec.ui.settings.api.Group;
+import ctbrec.ui.settings.api.LocalTimeProperty;
import ctbrec.ui.settings.api.Preferences;
import ctbrec.ui.settings.api.Setting;
import ctbrec.ui.settings.api.SimpleDirectoryProperty;
@@ -139,6 +140,8 @@ public class SettingsTab extends Tab implements TabSelectionListener {
private SimpleBooleanProperty minimizeToTray;
private SimpleBooleanProperty showGridLinesInTables;
private SimpleIntegerProperty defaultPriority;
+ private LocalTimeProperty timeoutRecordingStartingAt;
+ private LocalTimeProperty timeoutRecordingEndingAt;
public SettingsTab(List sites, Recorder recorder) {
this.sites = sites;
@@ -205,6 +208,8 @@ public class SettingsTab extends Tab implements TabSelectionListener {
minimizeToTray = new SimpleBooleanProperty(null, "minimizeToTray", settings.minimizeToTray);
showGridLinesInTables = new SimpleBooleanProperty(null, "showGridLinesInTables", settings.showGridLinesInTables);
defaultPriority = new SimpleIntegerProperty(null, "defaultPriority", settings.defaultPriority);
+ timeoutRecordingStartingAt = new LocalTimeProperty(null, "timeoutRecordingStartingAt", settings.timeoutRecordingStartingAt);
+ timeoutRecordingEndingAt = new LocalTimeProperty(null, "timeoutRecordingEndingAt", settings.timeoutRecordingEndingAt);
}
private void createGui() {
@@ -243,7 +248,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
Setting.of("Show grid lines in tables", showGridLinesInTables, "Show grid lines in tables").needsRestart(),
Setting.of("Fast scroll speed", fastScrollSpeed, "Makes the thumbnail overviews scroll faster with the mouse wheel").needsRestart())),
Category.of("Recorder",
- Group.of("Settings", Setting.of("Recordings Directory", recordingsDir), Setting.of("Directory Structure", directoryStructure),
+ Group.of("Recorder", Setting.of("Recordings Directory", recordingsDir), Setting.of("Directory Structure", directoryStructure),
Setting.of("Split recordings after", splitAfter).converter(SplitAfterOption.converter()).onChange(this::splitValuesChanged),
Setting.of("Split recordings bigger than", splitBiggerThan).converter(SplitBiggerThanOption.converter())
.onChange(this::splitValuesChanged),
@@ -256,6 +261,10 @@ public class SettingsTab extends Tab implements TabSelectionListener {
Setting.of("File Extension", fileExtension, "File extension to use for recordings"),
Setting.of("Check online state every (seconds)", onlineCheckIntervalInSecs, "Check every x seconds, if a model came online"),
Setting.of("Skip online check for paused models", onlineCheckSkipsPausedModels, "Skip online check for paused models")),
+ Group.of("Timeout",
+ Setting.of("Don't record from", timeoutRecordingStartingAt),
+ Setting.of("Until", timeoutRecordingEndingAt)
+ ),
Group.of("Location", Setting.of("Record Location", recordLocal).needsRestart(), Setting.of("Server", server), Setting.of("Port", port),
Setting.of("Path", path, "Leave empty, if you didn't change the servletContext in the server config"),
Setting.of("Download Filename", downloadFilename, "File name pattern for downloads"), Setting.of("", variablesHelpButton),
diff --git a/client/src/main/java/ctbrec/ui/settings/api/LocalTimeProperty.java b/client/src/main/java/ctbrec/ui/settings/api/LocalTimeProperty.java
new file mode 100644
index 00000000..ebd64b5d
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/settings/api/LocalTimeProperty.java
@@ -0,0 +1,13 @@
+package ctbrec.ui.settings.api;
+
+import java.time.LocalTime;
+
+import javafx.beans.property.SimpleObjectProperty;
+
+public class LocalTimeProperty extends SimpleObjectProperty {
+
+ public LocalTimeProperty(Object bean, String name, LocalTime initialValue) {
+ super(bean, name, initialValue);
+ }
+
+}
diff --git a/common/pom.xml b/common/pom.xml
index 4e951853..d4314aee 100644
--- a/common/pom.xml
+++ b/common/pom.xml
@@ -8,7 +8,7 @@
ctbrec
master
- 4.5.2
+ 4.5.3
../master
diff --git a/common/src/main/java/ctbrec/Config.java b/common/src/main/java/ctbrec/Config.java
index fa0f6ebe..73deda30 100644
--- a/common/src/main/java/ctbrec/Config.java
+++ b/common/src/main/java/ctbrec/Config.java
@@ -10,6 +10,7 @@ import java.nio.file.StandardCopyOption;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDateTime;
+import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
@@ -30,6 +31,7 @@ import com.squareup.moshi.Moshi;
import ctbrec.Settings.SplitStrategy;
import ctbrec.io.FileJsonAdapter;
+import ctbrec.io.LocalTimeJsonAdapter;
import ctbrec.io.ModelJsonAdapter;
import ctbrec.io.PostProcessorJsonAdapter;
import ctbrec.io.UuidJSonAdapter;
@@ -78,6 +80,7 @@ public class Config {
.add(PostProcessor.class, new PostProcessorJsonAdapter())
.add(File.class, new FileJsonAdapter())
.add(UUID.class, new UuidJSonAdapter())
+ .add(LocalTime.class, new LocalTimeJsonAdapter())
.build();
JsonAdapter adapter = moshi.adapter(Settings.class).lenient();
File configFile = new File(configDir, filename);
@@ -239,6 +242,7 @@ public class Config {
.add(PostProcessor.class, new PostProcessorJsonAdapter())
.add(File.class, new FileJsonAdapter())
.add(UUID.class, new UuidJSonAdapter())
+ .add(LocalTime.class, new LocalTimeJsonAdapter())
.build();
JsonAdapter adapter = moshi.adapter(Settings.class).indent(" ");
String json = adapter.toJson(settings);
diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java
index 2a0e0bb1..ef4c05c9 100644
--- a/common/src/main/java/ctbrec/Settings.java
+++ b/common/src/main/java/ctbrec/Settings.java
@@ -1,6 +1,7 @@
package ctbrec;
import java.io.File;
+import java.time.LocalTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
@@ -177,6 +178,8 @@ public class Settings {
public String stripchatPassword = "";
public boolean stripchatUseXhamster = false;
public List tabOrder = new ArrayList<>();
+ public LocalTime timeoutRecordingStartingAt = LocalTime.of(0, 0);
+ public LocalTime timeoutRecordingEndingAt = LocalTime.of(0, 0);
public boolean totalModelCountInTitle = false;
public boolean transportLayerSecurity = true;
public int thumbWidth = 180;
diff --git a/common/src/main/java/ctbrec/io/LocalTimeJsonAdapter.java b/common/src/main/java/ctbrec/io/LocalTimeJsonAdapter.java
new file mode 100644
index 00000000..4d32e6ac
--- /dev/null
+++ b/common/src/main/java/ctbrec/io/LocalTimeJsonAdapter.java
@@ -0,0 +1,21 @@
+package ctbrec.io;
+
+import java.io.IOException;
+import java.time.LocalTime;
+
+import com.squareup.moshi.JsonAdapter;
+import com.squareup.moshi.JsonReader;
+import com.squareup.moshi.JsonWriter;
+
+public class LocalTimeJsonAdapter extends JsonAdapter {
+ @Override
+ public LocalTime fromJson(JsonReader reader) throws IOException {
+ String timeString = reader.nextString();
+ return LocalTime.parse(timeString);
+ }
+
+ @Override
+ public void toJson(JsonWriter writer, LocalTime time) throws IOException {
+ writer.value(time.toString());
+ }
+}
diff --git a/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java b/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java
index beca0eef..b1a03096 100644
--- a/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java
+++ b/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java
@@ -101,7 +101,7 @@ public class NextGenLocalRecorder implements Recorder {
recording = true;
registerEventBusListener();
- preconditions = new RecordingPreconditions(this);
+ preconditions = new RecordingPreconditions(this, config);
LOG.debug("Recorder initialized");
LOG.info("Models to record: {}", models);
@@ -286,7 +286,7 @@ public class NextGenLocalRecorder implements Recorder {
private CompletableFuture startRecordingProcess(Model model) {
return CompletableFuture.runAsync(() -> {
try {
- preconditions.check(model, config);
+ preconditions.check(model);
LOG.info("Starting recording for model {}", model.getName());
Download download = createDownload(model);
recorderLock.lock();
diff --git a/common/src/main/java/ctbrec/recorder/RecordingPreconditions.java b/common/src/main/java/ctbrec/recorder/RecordingPreconditions.java
index 932947e6..8f50e22f 100644
--- a/common/src/main/java/ctbrec/recorder/RecordingPreconditions.java
+++ b/common/src/main/java/ctbrec/recorder/RecordingPreconditions.java
@@ -5,6 +5,7 @@ import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
+import java.time.LocalTime;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
@@ -28,14 +29,15 @@ public class RecordingPreconditions {
private long lastPreconditionMessage = 0;
- RecordingPreconditions(NextGenLocalRecorder recorder) {
+ RecordingPreconditions(NextGenLocalRecorder recorder, Config config) {
this.recorder = recorder;
+ this.config = config;
}
- void check(Model model, Config config) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
- this.config = config;
+ void check(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
LOG.debug("Checking preconditions for model {}", model);
ensureRecorderIsActive();
+ ensureNotInTimeoutPeriod();
ensureModelIsNotSuspended(model);
ensureModelIsNotMarkedForLaterRecording(model);
ensureRecordUntilIsInFuture(model);
@@ -47,6 +49,21 @@ public class RecordingPreconditions {
ensureDownloadSlotAvailable(model);
}
+ private void ensureNotInTimeoutPeriod() {
+ LocalTime start = config.getSettings().timeoutRecordingStartingAt;
+ LocalTime end = config.getSettings().timeoutRecordingEndingAt;
+ if (start.equals(end)) {
+ return;
+ }
+
+ LocalTime now = LocalTime.now();
+ if (start.isBefore(end) && now.isAfter(start) && now.isBefore(end)) {
+ throw new PreconditionNotMetException("Current time is in recording timeout " + start + " - " + end);
+ } else if(start.isAfter(end) && !(now.isAfter(end) && now.isBefore(start))) { // NOSONAR
+ throw new PreconditionNotMetException("Current time is in recording timeout " + start + " - " + end);
+ }
+ }
+
private void ensureModelIsOnline(Model model) {
try {
if (!model.isOnline(IGNORE_CACHE)) {
diff --git a/common/src/test/java/ctbrec/recorder/RecordingPreconditionsTest.java b/common/src/test/java/ctbrec/recorder/RecordingPreconditionsTest.java
index 021b2a5b..645be818 100644
--- a/common/src/test/java/ctbrec/recorder/RecordingPreconditionsTest.java
+++ b/common/src/test/java/ctbrec/recorder/RecordingPreconditionsTest.java
@@ -8,6 +8,8 @@ import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
+import java.time.LocalTime;
+import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
@@ -45,8 +47,8 @@ class RecordingPreconditionsTest {
NextGenLocalRecorder recorder = mock(NextGenLocalRecorder.class);
Model model = mock(Model.class);
- RecordingPreconditions preconditions = new RecordingPreconditions(recorder);
- PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(model, config));
+ RecordingPreconditions preconditions = new RecordingPreconditions(recorder, config);
+ PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(model));
assertEquals("Recorder is not in recording mode", ex.getMessage());
}
@@ -59,8 +61,8 @@ class RecordingPreconditionsTest {
when(model.isSuspended()).thenReturn(true);
when(model.toString()).thenReturn("Mockita Boobilicious");
- RecordingPreconditions preconditions = new RecordingPreconditions(recorder);
- PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(model, config));
+ RecordingPreconditions preconditions = new RecordingPreconditions(recorder, config);
+ PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(model));
assertEquals("Recording for model Mockita Boobilicious is suspended", ex.getMessage());
}
@@ -73,8 +75,8 @@ class RecordingPreconditionsTest {
when(model.isMarkedForLaterRecording()).thenReturn(true);
when(model.toString()).thenReturn("Mockita Boobilicious");
- RecordingPreconditions preconditions = new RecordingPreconditions(recorder);
- PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(model, config));
+ RecordingPreconditions preconditions = new RecordingPreconditions(recorder, config);
+ PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(model));
assertEquals("Model Mockita Boobilicious is marked for later recording", ex.getMessage());
}
@@ -87,8 +89,8 @@ class RecordingPreconditionsTest {
when(model.getRecordUntil()).thenReturn(Instant.now().minus(1, HOURS));
when(model.toString()).thenReturn("Mockita Boobilicious");
- RecordingPreconditions preconditions = new RecordingPreconditions(recorder);
- PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(model, config));
+ RecordingPreconditions preconditions = new RecordingPreconditions(recorder, config);
+ PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(model));
assertTrue(ex.getMessage().contains("Recording expired at "));
}
@@ -104,8 +106,8 @@ class RecordingPreconditionsTest {
when(model.getRecordUntil()).thenReturn(Instant.MAX);
when(model.toString()).thenReturn("Mockita Boobilicious");
- RecordingPreconditions preconditions = new RecordingPreconditions(recorder);
- PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(model, config));
+ RecordingPreconditions preconditions = new RecordingPreconditions(recorder, config);
+ PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(model));
assertEquals("A recording for model Mockita Boobilicious is already running", ex.getMessage());
}
@@ -121,8 +123,8 @@ class RecordingPreconditionsTest {
when(model.toString()).thenReturn("Mockita Boobilicious");
when(model.isOnline(true)).thenReturn(true);
- RecordingPreconditions preconditions = new RecordingPreconditions(recorder);
- PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(model, config));
+ RecordingPreconditions preconditions = new RecordingPreconditions(recorder, config);
+ PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(model));
assertEquals("Model Mockita Boobilicious has been removed. Restarting of recording cancelled.", ex.getMessage());
modelsToRecord.add(model);
@@ -130,7 +132,7 @@ class RecordingPreconditionsTest {
when(recorder.isRecording()).thenReturn(true);
when(recorder.getModels()).thenReturn(modelsToRecord);
when(recorder.enoughSpaceForRecording()).thenReturn(true);
- assertDoesNotThrow(() -> preconditions.check(model, config));
+ assertDoesNotThrow(() -> preconditions.check(model));
}
@Test
@@ -147,8 +149,8 @@ class RecordingPreconditionsTest {
when(recorder.getModels()).thenReturn(modelsToRecord);
when(recorder.enoughSpaceForRecording()).thenReturn(false);
- RecordingPreconditions preconditions = new RecordingPreconditions(recorder);
- PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(model, config));
+ RecordingPreconditions preconditions = new RecordingPreconditions(recorder, config);
+ PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(model));
assertEquals("Not enough disk space for recording", ex.getMessage());
}
@@ -185,8 +187,8 @@ class RecordingPreconditionsTest {
when(recorder.getRecordingProcesses()).thenReturn(recordingProcesses);
- RecordingPreconditions preconditions = new RecordingPreconditions(recorder);
- PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(mockita, config));
+ RecordingPreconditions preconditions = new RecordingPreconditions(recorder, config);
+ PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(mockita));
assertEquals("The Other One from the same group is already recorded", ex.getMessage());
}
@@ -206,8 +208,8 @@ class RecordingPreconditionsTest {
when(recorder.getModels()).thenReturn(modelsToRecord);
when(recorder.enoughSpaceForRecording()).thenReturn(true);
- RecordingPreconditions preconditions = new RecordingPreconditions(recorder);
- PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(mockita, config));
+ RecordingPreconditions preconditions = new RecordingPreconditions(recorder, config);
+ PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(mockita));
assertEquals("Mockita Boobilicious's room is not public", ex.getMessage());
}
@@ -226,15 +228,15 @@ class RecordingPreconditionsTest {
when(recorder.getModels()).thenReturn(modelsToRecord);
when(recorder.enoughSpaceForRecording()).thenReturn(true);
- RecordingPreconditions preconditions = new RecordingPreconditions(recorder);
- PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(mockita, config));
+ RecordingPreconditions preconditions = new RecordingPreconditions(recorder, config);
+ PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(mockita));
assertEquals("Mockita Boobilicious's room is not public", ex.getMessage());
reset(mockita);
when(mockita.isOnline(true)).thenThrow(new InterruptedException());
when(mockita.getRecordUntil()).thenReturn(Instant.MAX);
when(mockita.getName()).thenReturn("Mockita Boobilicious");
- ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(mockita, config));
+ ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(mockita));
assertEquals("Mockita Boobilicious's room is not public", ex.getMessage());
}
@@ -264,20 +266,20 @@ class RecordingPreconditionsTest {
recordingProcesses.put(theOtherOne, new Recording());
when(recorder.getRecordingProcesses()).thenReturn(recordingProcesses);
- RecordingPreconditions preconditions = new RecordingPreconditions(recorder);
- PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(mockita, config));
+ RecordingPreconditions preconditions = new RecordingPreconditions(recorder, config);
+ PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(mockita));
assertEquals("Other models have higher prio, not starting recording for Mockita Boobilicious", ex.getMessage());
settings.concurrentRecordings = -1;
- ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(mockita, config));
+ ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(mockita));
assertEquals("Other models have higher prio, not starting recording for Mockita Boobilicious", ex.getMessage());
settings.concurrentRecordings = 0;
- assertDoesNotThrow(() -> preconditions.check(mockita, config));
+ assertDoesNotThrow(() -> preconditions.check(mockita));
settings.concurrentRecordings = 1;
recordingProcesses.clear();
- assertDoesNotThrow(() -> preconditions.check(mockita, config));
+ assertDoesNotThrow(() -> preconditions.check(mockita));
}
@Test
@@ -316,11 +318,65 @@ class RecordingPreconditionsTest {
when(lowestPrio.getPriority()).thenReturn(1);
recordingProcesses.put(theOtherOne, mockRecordingProcess(lowestPrio));
- RecordingPreconditions preconditions = new RecordingPreconditions(recorder);
- assertDoesNotThrow(() -> preconditions.check(mockita, config));
+ RecordingPreconditions preconditions = new RecordingPreconditions(recorder, config);
+ assertDoesNotThrow(() -> preconditions.check(mockita));
verify(recorder).stopRecordingProcess(lowestPrio);
}
+ @Test
+ void testNotInTimeoutPeriod() throws InvalidKeyException, NoSuchAlgorithmException, IOException, ExecutionException, InterruptedException {
+ Model mockita = mock(Model.class);
+ when(mockita.isOnline(true)).thenReturn(true);
+ when(mockita.getRecordUntil()).thenReturn(Instant.MAX);
+ when(mockita.getName()).thenReturn("Mockita Boobilicious");
+ when(mockita.getPriority()).thenReturn(100);
+ NextGenLocalRecorder recorder = mock(NextGenLocalRecorder.class);
+ List modelsToRecord = new LinkedList<>();
+ settings.models = modelsToRecord;
+ settings.timeoutRecordingStartingAt = LocalTime.now().minusHours(1).truncatedTo(ChronoUnit.MINUTES);
+ settings.timeoutRecordingEndingAt = LocalTime.now().plusHours(1).truncatedTo(ChronoUnit.MINUTES);
+ modelsToRecord.add(mockita);
+ when(recorder.isRecording()).thenReturn(true);
+ when(recorder.getModels()).thenReturn(modelsToRecord);
+ when(recorder.enoughSpaceForRecording()).thenReturn(true);
+ Map recordingProcesses = new HashMap<>();
+ when(recorder.getRecordingProcesses()).thenReturn(recordingProcesses);
+
+ RecordingPreconditions preconditions = new RecordingPreconditions(recorder, config);
+ PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(mockita));
+ assertTrue(ex.getMessage().startsWith("Current time is in recording timeout"));
+
+ settings.timeoutRecordingStartingAt = LocalTime.now().minusHours(1).truncatedTo(ChronoUnit.MINUTES);
+ settings.timeoutRecordingEndingAt = LocalTime.now().minusHours(2).truncatedTo(ChronoUnit.MINUTES);
+ ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(mockita));
+ assertTrue(ex.getMessage().startsWith("Current time is in recording timeout"));
+
+ settings.timeoutRecordingStartingAt = LocalTime.now().minusHours(1).truncatedTo(ChronoUnit.MINUTES);
+ settings.timeoutRecordingEndingAt = LocalTime.now().minusHours(2).truncatedTo(ChronoUnit.MINUTES);
+ ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(mockita));
+ assertTrue(ex.getMessage().startsWith("Current time is in recording timeout"));
+
+ settings.timeoutRecordingStartingAt = LocalTime.now().plusHours(2).truncatedTo(ChronoUnit.MINUTES);
+ settings.timeoutRecordingEndingAt = LocalTime.now().plusHours(1).truncatedTo(ChronoUnit.MINUTES);
+ ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(mockita));
+ assertTrue(ex.getMessage().startsWith("Current time is in recording timeout"));
+
+ settings.timeoutRecordingStartingAt = LocalTime.of(0, 0);
+ settings.timeoutRecordingEndingAt = LocalTime.of(0, 0);
+ assertDoesNotThrow(() -> preconditions.check(mockita));
+
+ settings.timeoutRecordingStartingAt = LocalTime.now().plusHours(1).truncatedTo(ChronoUnit.MINUTES);
+ settings.timeoutRecordingEndingAt = LocalTime.now().plusHours(2).truncatedTo(ChronoUnit.MINUTES);
+ assertDoesNotThrow(() -> preconditions.check(mockita));
+ settings.timeoutRecordingStartingAt = LocalTime.now().minusHours(2).truncatedTo(ChronoUnit.MINUTES);
+ settings.timeoutRecordingEndingAt = LocalTime.now().minusHours(1).truncatedTo(ChronoUnit.MINUTES);
+ assertDoesNotThrow(() -> preconditions.check(mockita));
+
+ settings.timeoutRecordingStartingAt = LocalTime.now().plusHours(1).truncatedTo(ChronoUnit.MINUTES);
+ settings.timeoutRecordingEndingAt = LocalTime.now().minusHours(1).truncatedTo(ChronoUnit.MINUTES);
+ assertDoesNotThrow(() -> preconditions.check(mockita));
+ }
+
private Recording mockRecordingProcess(Model model) {
Download download = mock(Download.class);
when(download.getModel()).thenReturn(model);
diff --git a/master/pom.xml b/master/pom.xml
index 227d9aa0..93131f80 100644
--- a/master/pom.xml
+++ b/master/pom.xml
@@ -6,7 +6,7 @@
ctbrec
master
pom
- 4.5.2
+ 4.5.3
../common
diff --git a/server/pom.xml b/server/pom.xml
index 680f0e36..61bfef53 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -8,7 +8,7 @@
ctbrec
master
- 4.5.2
+ 4.5.3
../master