Add GUI and remote support for temporary recordings

This commit is contained in:
0xb00bface 2020-08-08 17:51:03 +02:00
parent 729319dfd2
commit e55daa0772
6 changed files with 159 additions and 37 deletions

View File

@ -16,6 +16,7 @@ import javafx.scene.control.Dialog;
import javafx.scene.control.TextArea; import javafx.scene.control.TextArea;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.Region;
import javafx.stage.Modality; import javafx.stage.Modality;
import javafx.stage.Stage; import javafx.stage.Stage;
@ -87,6 +88,23 @@ public class Dialogs {
return dialog.showAndWait(); return dialog.showAndWait();
} }
public static Boolean showCustomInput(Scene parent, String title, Region region) {
Dialog<?> dialog = new Dialog<>();
dialog.setTitle(title);
dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
dialog.initModality(Modality.APPLICATION_MODAL);
dialog.setResizable(true);
InputStream icon = Dialogs.class.getResourceAsStream("/icon.png");
Stage stage = (Stage) dialog.getDialogPane().getScene().getWindow();
stage.getIcons().add(new Image(icon));
if (parent != null) {
stage.getScene().getStylesheets().addAll(parent.getStylesheets());
}
dialog.getDialogPane().setContent(region);
dialog.showAndWait();
return dialog.getResult() == ButtonType.OK;
}
public static boolean showConfirmDialog(String title, String message, String header, Scene parent) { public static boolean showConfirmDialog(String title, String message, String header, Scene parent) {
AutosizeAlert confirm = new AutosizeAlert(AlertType.CONFIRMATION, message, parent, YES, NO); AutosizeAlert confirm = new AutosizeAlert(AlertType.CONFIRMATION, message, parent, YES, NO);
confirm.setTitle(title); confirm.setTitle(title);

View File

@ -1,9 +1,13 @@
package ctbrec.ui.tabs; package ctbrec.ui.tabs;
import static ctbrec.SubsequentAction.*;
import java.io.IOException; import java.io.IOException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
@ -25,6 +29,7 @@ import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.StringUtil; import ctbrec.StringUtil;
import ctbrec.SubsequentAction;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.sites.Site; import ctbrec.sites.Site;
import ctbrec.ui.AutosizeAlert; import ctbrec.ui.AutosizeAlert;
@ -59,8 +64,10 @@ import javafx.geometry.Pos;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu; import javafx.scene.control.ContextMenu;
import javafx.scene.control.DatePicker;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.MenuItem; import javafx.scene.control.MenuItem;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ScrollPane; import javafx.scene.control.ScrollPane;
import javafx.scene.control.SelectionMode; import javafx.scene.control.SelectionMode;
import javafx.scene.control.Tab; import javafx.scene.control.Tab;
@ -71,6 +78,7 @@ import javafx.scene.control.TableColumn.SortType;
import javafx.scene.control.TableRow; import javafx.scene.control.TableRow;
import javafx.scene.control.TableView; import javafx.scene.control.TableView;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.Tooltip; import javafx.scene.control.Tooltip;
import javafx.scene.control.cell.CheckBoxTableCell; import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.control.cell.PropertyValueFactory;
@ -84,6 +92,7 @@ import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent; import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane; import javafx.scene.layout.FlowPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.util.Callback; import javafx.util.Callback;
@ -612,6 +621,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
pauseRecording.setOnAction(e -> pauseRecording(selectedModels)); pauseRecording.setOnAction(e -> pauseRecording(selectedModels));
MenuItem resumeRecording = new MenuItem("Resume Recording"); MenuItem resumeRecording = new MenuItem("Resume Recording");
resumeRecording.setOnAction(e -> resumeRecording(selectedModels)); resumeRecording.setOnAction(e -> resumeRecording(selectedModels));
MenuItem stopRecordingAt = new MenuItem("Stop Recording at Date");
stopRecordingAt.setOnAction(e -> setStopDate(selectedModels.get(0)));
MenuItem openInBrowser = new MenuItem("Open in Browser"); MenuItem openInBrowser = new MenuItem("Open in Browser");
openInBrowser.setOnAction(e -> DesktopIntegration.open(selectedModels.get(0).getUrl())); openInBrowser.setOnAction(e -> DesktopIntegration.open(selectedModels.get(0).getUrl()));
MenuItem openInPlayer = new MenuItem("Open in Player"); MenuItem openInPlayer = new MenuItem("Open in Player");
@ -630,6 +641,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
ContextMenu menu = new ContextMenu(stop); ContextMenu menu = new ContextMenu(stop);
if (selectedModels.size() == 1) { if (selectedModels.size() == 1) {
menu.getItems().add(selectedModels.get(0).isSuspended() ? resumeRecording : pauseRecording); menu.getItems().add(selectedModels.get(0).isSuspended() ? resumeRecording : pauseRecording);
menu.getItems().add(stopRecordingAt);
} else { } else {
menu.getItems().addAll(resumeRecording, pauseRecording); menu.getItems().addAll(resumeRecording, pauseRecording);
} }
@ -646,6 +658,46 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
return menu; return menu;
} }
private void setStopDate(JavaFxModel model) {
DatePicker datePicker = new DatePicker();
GridPane grid = new GridPane();
grid.setHgap(10);
grid.setVgap(10);
grid.setPadding(new Insets(20, 150, 10, 10));
grid.add(new Label("Stop at"), 0, 0);
grid.add(datePicker, 1, 0);
grid.add(new Label("And then"), 0, 1);
ToggleGroup toggleGroup = new ToggleGroup();
RadioButton pauseButton = new RadioButton("pause recording");
pauseButton.setSelected(model.getRecordUntilSubsequentAction() == PAUSE);
pauseButton.setToggleGroup(toggleGroup);
RadioButton removeButton = new RadioButton("remove model");
removeButton.setSelected(model.getRecordUntilSubsequentAction() == REMOVE);
removeButton.setToggleGroup(toggleGroup);
HBox row = new HBox();
row.getChildren().addAll(pauseButton, removeButton);
HBox.setMargin(pauseButton, new Insets(5));
HBox.setMargin(removeButton, new Insets(5));
grid.add(row, 1, 1);
if (model.getRecordUntil().toEpochMilli() != Long.MAX_VALUE) {
LocalDate localDate = LocalDate.ofInstant(model.getRecordUntil(), ZoneId.systemDefault());
datePicker.setValue(localDate);
}
boolean userClickedOk = Dialogs.showCustomInput(getTabPane().getScene(), "Stop Recording at", grid);
if (userClickedOk) {
SubsequentAction action = pauseButton.isSelected() ? PAUSE : REMOVE;
LOG.info("Stop at {} and {}", datePicker.getValue(), action);
Instant stopAt = Instant.from(datePicker.getValue().atStartOfDay().atZone(ZoneId.systemDefault()));
model.setRecordUntil(stopAt);
model.setRecordUntilSubsequentAction(action);
try {
recorder.stopRecordingAt(model.getDelegate());
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
Dialogs.showError(getTabPane().getScene(), "Error", "Couln't set stop date", e);
}
}
}
private void ignore(ObservableList<JavaFxModel> selectedModels) { private void ignore(ObservableList<JavaFxModel> selectedModels) {
for (JavaFxModel fxModel : selectedModels) { for (JavaFxModel fxModel : selectedModels) {
Model modelToIgnore = fxModel.getDelegate(); Model modelToIgnore = fxModel.getDelegate();

View File

@ -1,5 +1,6 @@
package ctbrec.recorder; package ctbrec.recorder;
import static ctbrec.SubsequentAction.*;
import static ctbrec.event.Event.Type.*; import static ctbrec.event.Event.Type.*;
import java.io.File; import java.io.File;
@ -10,6 +11,7 @@ import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.time.ZoneId;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
@ -20,6 +22,7 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService; import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@ -42,7 +45,6 @@ import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.Recording.State; import ctbrec.Recording.State;
import ctbrec.SubsequentAction;
import ctbrec.event.Event; import ctbrec.event.Event;
import ctbrec.event.EventBusHolder; import ctbrec.event.EventBusHolder;
import ctbrec.event.ModelIsOnlineEvent; import ctbrec.event.ModelIsOnlineEvent;
@ -215,41 +217,14 @@ public class NextGenLocalRecorder implements Recorder {
recorderLock.lock(); recorderLock.lock();
try { try {
checkRecordingPreconditions(model); checkRecordingPreconditions(model);
LOG.info("Starting recording for model {}", model.getName()); LOG.info("Starting recording for model {}", model.getName());
Download download = model.createDownload(); Download download = createDownload(model);
download.init(config, model, Instant.now()); Recording rec = createRecording(download);
Objects.requireNonNull(download.getStartTime(), completionService.submit(createDownloadJob(rec));
"At this point the download should have set a startTime. Make sure to set a startTime in " + download.getClass() + ".init()");
LOG.debug("Downloading with {}", download.getClass().getSimpleName());
Recording rec = createRecording(model, download);
completionService.submit(() -> {
try {
setRecordingStatus(rec, State.RECORDING);
model.setLastRecorded(rec.getStartDate());
recordingManager.saveRecording(rec);
download.start();
} catch (Exception e) {
LOG.error("Download for {} failed. Download state: {}", model.getName(), rec.getStatus(), e);
}
boolean deleted = deleteIfEmpty(rec);
setRecordingStatus(rec, deleted ? State.DELETED : State.WAITING);
if (!deleted) {
// only save the status, if the recording has not been deleted, otherwise we recreate the metadata file
recordingManager.saveRecording(rec);
}
return rec;
});
} catch (RecordUntilExpiredException e) { } catch (RecordUntilExpiredException e) {
LOG.info("Precondition not met to record {}: {}", model, e.getLocalizedMessage()); LOG.info("Precondition not met to record {}: {}", model, e.getLocalizedMessage());
executeRecordUntilSubsequentAction(model); executeRecordUntilSubsequentAction(model);
} catch (PreconditionNotMetException e) { } catch (PreconditionNotMetException e) {
// long now = System.currentTimeMillis();
// if ((now - lastSpaceMessage) > TimeUnit.MINUTES.toMillis(1)) {
// LOG.info("Not enough space for recording, not starting recording for {}", model);
// lastSpaceMessage = now;
// }
LOG.info("Precondition not met to record {}: {}", model, e.getLocalizedMessage()); LOG.info("Precondition not met to record {}: {}", model, e.getLocalizedMessage());
return; return;
} finally { } finally {
@ -257,20 +232,53 @@ public class NextGenLocalRecorder implements Recorder {
} }
} }
private void executeRecordUntilSubsequentAction(Model model) throws IOException { private Download createDownload(Model model) {
if (model.getRecordUntilSubsequentAction() == SubsequentAction.PAUSE) { Download download = model.createDownload();
model.setSuspended(true); download.init(config, model, Instant.now());
} else if (model.getRecordUntilSubsequentAction() == SubsequentAction.REMOVE) { Objects.requireNonNull(download.getStartTime(),
"At this point the download should have set a startTime. Make sure to set a startTime in " + download.getClass() + ".init()");
LOG.debug("Downloading with {}", download.getClass().getSimpleName());
return download;
}
private Callable<Recording> createDownloadJob(Recording rec) {
return () -> {
try { try {
LOG.info("Recording timeframe expired for model {} - {}", model, model.getRecordUntil()); setRecordingStatus(rec, State.RECORDING);
rec.getModel().setLastRecorded(rec.getStartDate());
recordingManager.saveRecording(rec);
rec.getDownload().start();
} catch (Exception e) {
LOG.error("Download for {} failed. Download state: {}", rec.getModel().getName(), rec.getStatus(), e);
}
boolean deleted = deleteIfEmpty(rec);
setRecordingStatus(rec, deleted ? State.DELETED : State.WAITING);
if (!deleted) {
// only save the status, if the recording has not been deleted, otherwise we recreate the metadata file
recordingManager.saveRecording(rec);
}
return rec;
};
}
private void executeRecordUntilSubsequentAction(Model model) throws IOException {
if (model.getRecordUntilSubsequentAction() == PAUSE) {
model.setSuspended(true);
} else if (model.getRecordUntilSubsequentAction() == REMOVE) {
try {
LOG.info("Removing {} because the recording timeframe ended at {}", model, model.getRecordUntil().atZone(ZoneId.systemDefault()));
stopRecording(model); stopRecording(model);
} catch (InvalidKeyException | NoSuchAlgorithmException e1) { } catch (InvalidKeyException | NoSuchAlgorithmException e1) {
LOG.error("Error while stopping recording", e1); LOG.error("Error while stopping recording", e1);
} }
} }
// reset values, so that model can be recorded again
model.setRecordUntil(null);
model.setRecordUntilSubsequentAction(PAUSE);
} }
private Recording createRecording(Model model, Download download) throws IOException { private Recording createRecording(Download download) throws IOException {
Model model = download.getModel();
Recording rec = new Recording(); Recording rec = new Recording();
rec.setDownload(download); rec.setDownload(download);
rec.setPath(download.getPath(model).replaceAll("\\\\", "/")); rec.setPath(download.getPath(model).replaceAll("\\\\", "/"));
@ -749,4 +757,31 @@ public class NextGenLocalRecorder implements Recorder {
rec.setNote(note); rec.setNote(note);
recordingManager.saveRecording(rec); recordingManager.saveRecording(rec);
} }
@Override
public void stopRecordingAt(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
recorderLock.lock();
try {
int index = models.indexOf(model);
if (index >= 0) {
Model m = models.get(index);
m.setRecordUntil(model.getRecordUntil());
m.setRecordUntilSubsequentAction(model.getRecordUntilSubsequentAction());
config.save();
} else {
throw new NoSuchElementException("Model " + model.getName() + " [" + model.getUrl() + "] not found in list of recorded models");
}
if (recordingProcesses.containsKey(model)) {
Recording rec = recordingProcesses.get(model);
rec.getDownload().stop();
}
} finally {
recorderLock.unlock();
}
if (Instant.now().isAfter(model.getRecordUntil())) {
executeRecordUntilSubsequentAction(model);
}
}
} }

View File

@ -14,6 +14,7 @@ public interface Recorder {
public void startRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException; public void startRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException;
public void stopRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException; public void stopRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException;
public void stopRecordingAt(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException;
public void switchStreamSource(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException; public void switchStreamSource(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException;

View File

@ -86,6 +86,11 @@ public class RemoteRecorder implements Recorder {
sendRequest("stop", model); sendRequest("stop", model);
} }
@Override
public void stopRecordingAt(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
sendRequest("stopAt", model);
}
private void sendRequest(String action, Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException { private void sendRequest(String action, Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
String payload = modelRequestAdapter.toJson(new ModelRequest(action, model)); String payload = modelRequestAdapter.toJson(new ModelRequest(action, model));
LOG.debug("Sending request to recording server: {}", payload); LOG.debug("Sending request to recording server: {}", payload);

View File

@ -96,6 +96,17 @@ public class RecorderServlet extends AbstractCtbrecServlet {
response = "{\"status\": \"success\", \"msg\": \"Stopping recording\"}"; response = "{\"status\": \"success\", \"msg\": \"Stopping recording\"}";
resp.getWriter().write(response); resp.getWriter().write(response);
break; break;
case "stopAt":
new Thread(() -> {
try {
recorder.stopRecordingAt(request.model);
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
LOG.error("Couldn't stop recording for model {}", request.model, e);
}
}).start();
response = "{\"status\": \"success\", \"msg\": \"Stopping recording\"}";
resp.getWriter().write(response);
break;
case "list": case "list":
resp.getWriter().write("{\"status\": \"success\", \"msg\": \"List of models\", \"models\": ["); resp.getWriter().write("{\"status\": \"success\", \"msg\": \"List of models\", \"models\": [");
JsonAdapter<Model> modelAdapter = new ModelJsonAdapter(); JsonAdapter<Model> modelAdapter = new ModelJsonAdapter();