Merge branch 'model-groups' into dev
|
@ -63,12 +63,12 @@ import ctbrec.ui.settings.SettingsTab;
|
|||
import ctbrec.ui.tabs.DonateTabFx;
|
||||
import ctbrec.ui.tabs.HelpTab;
|
||||
import ctbrec.ui.tabs.RecentlyWatchedTab;
|
||||
import ctbrec.ui.tabs.RecordedTab;
|
||||
import ctbrec.ui.tabs.RecordingsTab;
|
||||
import ctbrec.ui.tabs.SiteTab;
|
||||
import ctbrec.ui.tabs.TabSelectionListener;
|
||||
import ctbrec.ui.tabs.UpdateTab;
|
||||
import ctbrec.ui.tabs.logging.LoggingTab;
|
||||
import ctbrec.ui.tabs.recorded.RecordedTab;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.HostServices;
|
||||
import javafx.application.Platform;
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package ctbrec.ui;
|
||||
|
||||
public enum Icon {
|
||||
|
||||
GROUP_16(Icon.class.getResource("/16/users.png").toExternalForm()),
|
||||
CHECK_16(Icon.class.getResource("/16/check-small.png").toExternalForm()),
|
||||
CLOCK_16(Icon.class.getResource("/16/clock.png").toExternalForm());
|
||||
|
||||
private String url;
|
||||
|
||||
private Icon(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String url() {
|
||||
return url;
|
||||
}
|
||||
}
|
|
@ -22,15 +22,13 @@ import javafx.beans.property.BooleanProperty;
|
|||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
|
||||
/**
|
||||
* Just a wrapper for Model, which augments it with JavaFX value binding properties, so that UI widgets get updated proeprly
|
||||
*/
|
||||
public class JavaFxModel implements Model {
|
||||
private transient StringProperty onlineProperty = new SimpleStringProperty();
|
||||
private transient StringProperty recordingProperty = new SimpleStringProperty();
|
||||
private transient BooleanProperty onlineProperty = new SimpleBooleanProperty();
|
||||
private transient BooleanProperty recordingProperty = new SimpleBooleanProperty();
|
||||
private transient BooleanProperty pausedProperty = new SimpleBooleanProperty();
|
||||
private transient SimpleIntegerProperty priorityProperty = new SimpleIntegerProperty();
|
||||
private transient SimpleObjectProperty<Instant> lastSeenProperty = new SimpleObjectProperty<>();
|
||||
|
@ -105,14 +103,22 @@ public class JavaFxModel implements Model {
|
|||
return delegate.toString();
|
||||
}
|
||||
|
||||
public StringProperty getOnlineProperty() {
|
||||
public BooleanProperty getOnlineProperty() {
|
||||
return onlineProperty;
|
||||
}
|
||||
|
||||
public StringProperty getRecordingProperty() {
|
||||
public void setOnlineProperty(boolean online) {
|
||||
this.onlineProperty.set(online);
|
||||
}
|
||||
|
||||
public BooleanProperty getRecordingProperty() {
|
||||
return recordingProperty;
|
||||
}
|
||||
|
||||
public void setRecordingProperty(boolean recording) {
|
||||
this.recordingProperty.setValue(recording);
|
||||
}
|
||||
|
||||
public BooleanProperty getPausedProperty() {
|
||||
return pausedProperty;
|
||||
}
|
||||
|
@ -329,6 +335,4 @@ public class JavaFxModel implements Model {
|
|||
public void setMarkedForLaterRecording(boolean marked) {
|
||||
delegate.setMarkedForLaterRecording(marked);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,185 @@
|
|||
package ctbrec.ui.action;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.ModelGroup;
|
||||
import ctbrec.StringUtil;
|
||||
import ctbrec.recorder.Recorder;
|
||||
import ctbrec.ui.controls.Dialogs;
|
||||
import ctbrec.ui.controls.autocomplete.ObservableListSuggester;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
public class AddToGroupAction {
|
||||
|
||||
private Node source;
|
||||
private Model model;
|
||||
private Recorder recorder;
|
||||
|
||||
public AddToGroupAction(Node source, Recorder recorder, Model model) {
|
||||
this.source = source;
|
||||
this.recorder = recorder;
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public void execute() {
|
||||
source.setCursor(Cursor.WAIT);
|
||||
try {
|
||||
var dialog = new AddModelGroupDialog();
|
||||
boolean ok = Dialogs.showCustomInput(source.getScene(), "Add model to group", dialog.getMainPane());
|
||||
dialog.requestFocus();
|
||||
if (ok) {
|
||||
String text = dialog.getText();
|
||||
if (StringUtil.isBlank(text)) {
|
||||
return;
|
||||
}
|
||||
Set<ModelGroup> modelGroups = recorder.getModelGroups();
|
||||
Optional<ModelGroup> existingGroup = modelGroups.stream().filter(mg -> mg.getName().equalsIgnoreCase(text)).findFirst();
|
||||
if (existingGroup.isPresent()) {
|
||||
existingGroup.get().add(model);
|
||||
recorder.saveModelGroup(existingGroup.get());
|
||||
} else {
|
||||
var group = new ModelGroup();
|
||||
group.setId(UUID.randomUUID());
|
||||
group.setName(text);
|
||||
group.add(model);
|
||||
modelGroups.add(group);
|
||||
recorder.saveModelGroup(group);
|
||||
}
|
||||
}
|
||||
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException e) {
|
||||
Dialogs.showError(source.getScene(), "Add model to group", "Saving model group failed", e);
|
||||
} finally {
|
||||
source.setCursor(Cursor.DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
private static class AddModelGroupDialog {
|
||||
private ComboBox<ModelGroupListItem> comboBox;
|
||||
private TextField editor;
|
||||
private ObservableListSuggester suggester;
|
||||
|
||||
public String getText() {
|
||||
return comboBox.getEditor().getText();
|
||||
}
|
||||
|
||||
public void requestFocus() {
|
||||
comboBox.requestFocus();
|
||||
editor.requestFocus();
|
||||
}
|
||||
|
||||
Region getMainPane() {
|
||||
var dialogPane = new GridPane();
|
||||
Set<ModelGroup> modelGroups = Config.getInstance().getSettings().modelGroups;
|
||||
List<ModelGroupListItem> comboBoxItems = modelGroups.stream().map(ModelGroupListItem::new).sorted().collect(Collectors.toList());
|
||||
ObservableList<ModelGroupListItem> comboBoxModel = FXCollections.observableArrayList(comboBoxItems);
|
||||
suggester = new ObservableListSuggester(comboBoxModel);
|
||||
comboBox = new ComboBox<>(comboBoxModel);
|
||||
comboBox.setEditable(true);
|
||||
editor = comboBox.getEditor();
|
||||
comboBox.getEditor().addEventHandler(KeyEvent.KEY_RELEASED, evt -> {
|
||||
if (evt.getCode().isLetterKey() || evt.getCode().isDigitKey()) {
|
||||
autocomplete(false);
|
||||
} else if (evt.getCode() == KeyCode.ENTER) {
|
||||
if (editor.getSelection().getLength() > 0) {
|
||||
editor.selectRange(0, 0);
|
||||
editor.insertText(editor.lengthProperty().get(), ":");
|
||||
editor.positionCaret(editor.lengthProperty().get());
|
||||
evt.consume();
|
||||
}
|
||||
} else if (evt.getCode() == KeyCode.SPACE && evt.isControlDown()) {
|
||||
autocomplete(true);
|
||||
}
|
||||
});
|
||||
comboBox.setPlaceholder(new Label(" type in a name to a add a new group "));
|
||||
dialogPane.add(new Label("Model group "), 0, 0);
|
||||
dialogPane.add(comboBox, 1, 0);
|
||||
return dialogPane;
|
||||
}
|
||||
|
||||
private void autocomplete(boolean fulltextSearch) {
|
||||
String oldtext = getOldText();
|
||||
if(oldtext.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<String> match;
|
||||
if (fulltextSearch) {
|
||||
match = suggester.fulltext(oldtext);
|
||||
} else {
|
||||
match = suggester.startsWith(oldtext);
|
||||
}
|
||||
|
||||
if (match.isPresent()) {
|
||||
editor.setText(match.get());
|
||||
int pos = oldtext.length();
|
||||
editor.positionCaret(pos);
|
||||
editor.selectRange(pos, match.get().length());
|
||||
}
|
||||
}
|
||||
|
||||
private String getOldText() {
|
||||
if(editor.getSelection().getLength() > 0) {
|
||||
return editor.getText().substring(0, editor.getSelection().getStart());
|
||||
} else {
|
||||
return editor.getText();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class ModelGroupListItem implements Comparable<ModelGroupListItem> {
|
||||
private ModelGroup modelGroup;
|
||||
|
||||
public ModelGroupListItem(ModelGroup modelGroup) {
|
||||
this.modelGroup = modelGroup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.modelGroup.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return java.util.Objects.hash(modelGroup);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
ModelGroupListItem other = (ModelGroupListItem) obj;
|
||||
return java.util.Objects.equals(modelGroup, other.modelGroup);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(ModelGroupListItem o) {
|
||||
return this.modelGroup.getName().compareTo(o.modelGroup.getName());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
package ctbrec.ui.action;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import ctbrec.Model;
|
||||
import ctbrec.ModelGroup;
|
||||
import ctbrec.recorder.Recorder;
|
||||
import ctbrec.ui.controls.Dialogs;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
public class EditGroupAction {
|
||||
|
||||
private static final String DIALOG_TITLE = "Edit model group";
|
||||
|
||||
private Node source;
|
||||
private Model model;
|
||||
private Recorder recorder;
|
||||
|
||||
public EditGroupAction(Node source, Recorder recorder, Model model) {
|
||||
this.source = source;
|
||||
this.recorder = recorder;
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public void execute() {
|
||||
source.setCursor(Cursor.WAIT);
|
||||
try {
|
||||
var dialog = new EditModelGroupDialog(model);
|
||||
boolean ok = Dialogs.showCustomInput(source.getScene(), DIALOG_TITLE, dialog);
|
||||
if (ok) {
|
||||
var group = dialog.getModelGroup();
|
||||
group.setName(dialog.getGroupName());
|
||||
group.getModelUrls().clear();
|
||||
group.getModelUrls().addAll(dialog.getUrls());
|
||||
recorder.saveModelGroup(group);
|
||||
if (dialog.getUrls().isEmpty()) {
|
||||
boolean delete = Dialogs.showConfirmDialog(DIALOG_TITLE, "Do you want to delete the group?", "Group is empty", source.getScene());
|
||||
if (delete) {
|
||||
recorder.deleteModelGroup(group);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Dialogs.showError(source.getScene(), DIALOG_TITLE, "Editing model group failed", e);
|
||||
} finally {
|
||||
source.setCursor(Cursor.DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
private class EditModelGroupDialog extends GridPane {
|
||||
private TextField groupName;
|
||||
private ListView<String> urlListView;
|
||||
private ObservableList<String> urlList;
|
||||
private ModelGroup modelGroup;
|
||||
private List<String> urls;
|
||||
|
||||
public EditModelGroupDialog(Model model) {
|
||||
Optional<ModelGroup> optionalModelGroup;
|
||||
try {
|
||||
optionalModelGroup = recorder.getModelGroup(model);
|
||||
if (optionalModelGroup.isPresent()) {
|
||||
modelGroup = optionalModelGroup.get();
|
||||
urls = new ArrayList<>(modelGroup.getModelUrls());
|
||||
createGui(modelGroup);
|
||||
} else {
|
||||
Dialogs.showError(getScene(), DIALOG_TITLE, "No group found for model", null);
|
||||
}
|
||||
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
|
||||
Dialogs.showError(getScene(), DIALOG_TITLE, "Couldn't edit model group", e);
|
||||
}
|
||||
}
|
||||
|
||||
public ModelGroup getModelGroup() {
|
||||
return modelGroup;
|
||||
}
|
||||
|
||||
public List<String> getUrls() {
|
||||
return urls;
|
||||
}
|
||||
|
||||
public String getGroupName() {
|
||||
return groupName.getText();
|
||||
}
|
||||
|
||||
void createGui(ModelGroup modelGroup) {
|
||||
setHgap(5);
|
||||
vgapProperty().bind(hgapProperty());
|
||||
|
||||
groupName = new TextField(modelGroup.getName());
|
||||
Button up = createUpButton();
|
||||
Button down = createDownButton();
|
||||
Button remove = createRemoveButton();
|
||||
var buttons = new VBox(3, up, down, remove);
|
||||
urlList = FXCollections.observableList(modelGroup.getModelUrls());
|
||||
urlList.addListener((ListChangeListener<String>) change -> {
|
||||
urls = new ArrayList<>(urlList);
|
||||
});
|
||||
urlListView = new ListView<>(urlList);
|
||||
GridPane.setHgrow(urlListView, Priority.ALWAYS);
|
||||
|
||||
var row = 0;
|
||||
add(groupName, 0, row++);
|
||||
add(urlListView, 0, row);
|
||||
add(buttons, 1, row);
|
||||
|
||||
urlListView.getSelectionModel().selectedIndexProperty().addListener((obs, oldV, newV) -> {
|
||||
var idx = newV.intValue();
|
||||
boolean noSelection = idx == -1;
|
||||
up.setDisable(noSelection || idx == 0);
|
||||
down.setDisable(noSelection || idx == urlList.size() - 1);
|
||||
remove.setDisable(noSelection);
|
||||
});
|
||||
}
|
||||
|
||||
private Button createUpButton() {
|
||||
var button = createButton("\u25B4", "Move step up");
|
||||
button.setOnAction(evt -> {
|
||||
int idx = urlListView.getSelectionModel().getSelectedIndex();
|
||||
String selectedItem = urlListView.getSelectionModel().getSelectedItem();
|
||||
urlList.remove(idx);
|
||||
urlList.add(idx - 1, selectedItem);
|
||||
urlListView.getSelectionModel().select(idx - 1);
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
private Button createDownButton() {
|
||||
var button = createButton("\u25BE", "Move step down");
|
||||
button.setOnAction(evt -> {
|
||||
int idx = urlListView.getSelectionModel().getSelectedIndex();
|
||||
String selectedItem = urlListView.getSelectionModel().getSelectedItem();
|
||||
urlList.remove(idx);
|
||||
urlList.add(idx + 1, selectedItem);
|
||||
urlListView.getSelectionModel().select(idx + 1);
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
private Button createRemoveButton() {
|
||||
var button = createButton("-", "Remove selected step");
|
||||
button.setOnAction(evt -> {
|
||||
String selectedItem = urlListView.getSelectionModel().getSelectedItem();
|
||||
if (selectedItem != null) {
|
||||
urlList.remove(selectedItem);
|
||||
}
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
private Button createButton(String text, String tooltip) {
|
||||
var b = new Button(text);
|
||||
b.setTooltip(new Tooltip(tooltip));
|
||||
b.setDisable(true);
|
||||
b.setPrefSize(32, 32);
|
||||
return b;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,16 +4,26 @@ import static javafx.scene.control.ButtonType.*;
|
|||
|
||||
import java.io.InputStream;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.ModelGroup;
|
||||
import ctbrec.StringUtil;
|
||||
import ctbrec.ui.AutosizeAlert;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Dialog;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextArea;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.GridPane;
|
||||
|
@ -46,7 +56,7 @@ public class Dialogs {
|
|||
}
|
||||
alert.setContentText(content);
|
||||
if (parent != null) {
|
||||
Stage stage = (Stage) alert.getDialogPane().getScene().getWindow();
|
||||
var stage = (Stage) alert.getDialogPane().getScene().getWindow();
|
||||
stage.getScene().getStylesheets().addAll(parent.getStylesheets());
|
||||
}
|
||||
alert.showAndWait();
|
||||
|
@ -63,22 +73,22 @@ public class Dialogs {
|
|||
Dialog<String> dialog = new Dialog<>();
|
||||
dialog.setTitle(title);
|
||||
dialog.setHeaderText(header);
|
||||
dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
dialog.getDialogPane().getButtonTypes().addAll(OK, CANCEL);
|
||||
dialog.initModality(Modality.APPLICATION_MODAL);
|
||||
dialog.setResizable(true);
|
||||
InputStream icon = Dialogs.class.getResourceAsStream("/icon.png");
|
||||
Stage stage = (Stage) dialog.getDialogPane().getScene().getWindow();
|
||||
var stage = (Stage) dialog.getDialogPane().getScene().getWindow();
|
||||
stage.getIcons().add(new Image(icon));
|
||||
if (parent != null) {
|
||||
stage.getScene().getStylesheets().addAll(parent.getStylesheets());
|
||||
}
|
||||
|
||||
GridPane grid = new GridPane();
|
||||
var grid = new GridPane();
|
||||
grid.setHgap(10);
|
||||
grid.setVgap(10);
|
||||
grid.setPadding(new Insets(20, 150, 10, 10));
|
||||
|
||||
TextArea notes = new TextArea(text);
|
||||
var notes = new TextArea(text);
|
||||
notes.setPrefRowCount(3);
|
||||
grid.add(notes, 0, 0);
|
||||
dialog.getDialogPane().setContent(grid);
|
||||
|
@ -86,7 +96,7 @@ public class Dialogs {
|
|||
Platform.runLater(notes::requestFocus);
|
||||
|
||||
dialog.setResultConverter(dialogButton -> {
|
||||
if (dialogButton == ButtonType.OK) {
|
||||
if (dialogButton == OK) {
|
||||
return notes.getText();
|
||||
}
|
||||
return null;
|
||||
|
@ -98,31 +108,31 @@ public class Dialogs {
|
|||
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.getDialogPane().getButtonTypes().addAll(OK, CANCEL);
|
||||
dialog.initModality(Modality.APPLICATION_MODAL);
|
||||
dialog.setResizable(true);
|
||||
InputStream icon = Dialogs.class.getResourceAsStream("/icon.png");
|
||||
Stage stage = (Stage) dialog.getDialogPane().getScene().getWindow();
|
||||
var 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;
|
||||
return dialog.getResult() == OK;
|
||||
}
|
||||
|
||||
public static boolean showConfirmDialog(String title, String message, String header, Scene parent) {
|
||||
AutosizeAlert confirm = new AutosizeAlert(AlertType.CONFIRMATION, message, parent, YES, NO);
|
||||
var confirm = new AutosizeAlert(AlertType.CONFIRMATION, message, parent, YES, NO);
|
||||
confirm.setTitle(title);
|
||||
confirm.setHeaderText(header);
|
||||
confirm.showAndWait();
|
||||
return confirm.getResult() == ButtonType.YES;
|
||||
return confirm.getResult() == YES;
|
||||
}
|
||||
|
||||
public static ButtonType showShutdownDialog(Scene parent) {
|
||||
String message = "There are recordings in progress";
|
||||
AutosizeAlert confirm = new AutosizeAlert(AlertType.CONFIRMATION, "", parent, YES, FINISH, NO);
|
||||
var message = "There are recordings in progress";
|
||||
var confirm = new AutosizeAlert(AlertType.CONFIRMATION, "", parent, YES, FINISH, NO);
|
||||
confirm.setTitle("Shutdown");
|
||||
confirm.setHeaderText(message);
|
||||
((Button) confirm.getDialogPane().lookupButton(ButtonType.YES)).setText("Shutdown Now");
|
||||
|
@ -134,4 +144,36 @@ public class Dialogs {
|
|||
confirm.showAndWait();
|
||||
return confirm.getResult();
|
||||
}
|
||||
|
||||
public static Optional<ModelGroup> showModelGroupSelectionDialog(Scene parent, Model model) {
|
||||
var dialogPane = new GridPane();
|
||||
Set<ModelGroup> modelGroups = Config.getInstance().getSettings().modelGroups;
|
||||
ObservableList<ModelGroup> comboBoxModel = FXCollections.observableArrayList(modelGroups);
|
||||
ComboBox<ModelGroup> comboBox = new ComboBox<>(comboBoxModel);
|
||||
comboBox.setEditable(true);
|
||||
comboBox.setPlaceholder(new Label(" type in a name to a add a new group "));
|
||||
dialogPane.add(new Label("Model group"), 0, 0);
|
||||
dialogPane.add(comboBox, 1, 0);
|
||||
boolean ok = showCustomInput(parent, "Add model to group", dialogPane);
|
||||
if (ok) {
|
||||
String text = comboBox.getEditor().getText();
|
||||
if (StringUtil.isBlank(text)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Optional<ModelGroup> existingGroup = modelGroups.stream().filter(mg -> mg.getName().equalsIgnoreCase(text)).findFirst();
|
||||
if (existingGroup.isPresent()) {
|
||||
existingGroup.get().add(model);
|
||||
return existingGroup;
|
||||
} else {
|
||||
var group = new ModelGroup();
|
||||
group.setId(UUID.randomUUID());
|
||||
group.setName(text);
|
||||
group.add(model);
|
||||
modelGroups.add(group);
|
||||
return Optional.of(group);
|
||||
}
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,8 @@ package ctbrec.ui.tabs;
|
|||
|
||||
import java.io.IOException;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.recorder.RecordingManager;
|
||||
import ctbrec.recorder.postprocessing.AbstractPlaceholderAwarePostProcessor;
|
||||
import ctbrec.recorder.postprocessing.PostProcessingContext;
|
||||
|
||||
public class DownloadPostprocessor extends AbstractPlaceholderAwarePostProcessor {
|
||||
|
||||
|
@ -15,7 +13,7 @@ public class DownloadPostprocessor extends AbstractPlaceholderAwarePostProcessor
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException {
|
||||
public boolean postprocess(PostProcessingContext ctx) throws IOException, InterruptedException {
|
||||
// nothing really to do in here, we just inherit from AbstractPlaceholderAwarePostProcessor to use fillInPlaceHolders
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@ import ctbrec.io.UrlUtil;
|
|||
import ctbrec.recorder.ProgressListener;
|
||||
import ctbrec.recorder.Recorder;
|
||||
import ctbrec.recorder.RecordingPinnedException;
|
||||
import ctbrec.recorder.postprocessing.PostProcessingContext;
|
||||
import ctbrec.ui.AutosizeAlert;
|
||||
import ctbrec.ui.CamrecApplication;
|
||||
import ctbrec.ui.DesktopIntegration;
|
||||
|
@ -688,7 +689,10 @@ public class RecordingsTab extends Tab implements TabSelectionListener, Shutdown
|
|||
} else {
|
||||
String downloadFilename = config.getSettings().downloadFilename;
|
||||
String fileSuffix = config.getSettings().ffmpegFileSuffix;
|
||||
return new DownloadPostprocessor().fillInPlaceHolders(downloadFilename, recording, config) + '.' + fileSuffix;
|
||||
PostProcessingContext ctx = new PostProcessingContext();
|
||||
ctx.setRecording(recording);
|
||||
ctx.setConfig(config);
|
||||
return new DownloadPostprocessor().fillInPlaceHolders(downloadFilename, ctx) + '.' + fileSuffix;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@ import static ctbrec.ui.controls.Dialogs.*;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
@ -29,6 +31,7 @@ import org.slf4j.LoggerFactory;
|
|||
import ctbrec.Config;
|
||||
import ctbrec.GlobalThreadPool;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.ModelGroup;
|
||||
import ctbrec.event.EventBusHolder;
|
||||
import ctbrec.recorder.Recorder;
|
||||
import ctbrec.sites.Site;
|
||||
|
@ -39,10 +42,13 @@ import ctbrec.ui.DesktopIntegration;
|
|||
import ctbrec.ui.SiteUiFactory;
|
||||
import ctbrec.ui.TipDialog;
|
||||
import ctbrec.ui.TokenLabel;
|
||||
import ctbrec.ui.action.AddToGroupAction;
|
||||
import ctbrec.ui.action.EditGroupAction;
|
||||
import ctbrec.ui.action.IgnoreModelsAction;
|
||||
import ctbrec.ui.action.OpenRecordingsDir;
|
||||
import ctbrec.ui.action.SetStopDateAction;
|
||||
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
|
||||
import ctbrec.ui.controls.Dialogs;
|
||||
import ctbrec.ui.controls.FasterVerticalScrollPaneSkin;
|
||||
import ctbrec.ui.controls.SearchBox;
|
||||
import ctbrec.ui.controls.SearchPopover;
|
||||
|
@ -467,52 +473,57 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
|
||||
private ContextMenu createContextMenu(ThumbCell cell) {
|
||||
Model model = cell.getModel();
|
||||
var model = cell.getModel();
|
||||
boolean modelIsTrackedByRecorder = recorder.isTracked(model);
|
||||
|
||||
MenuItem openInPlayer = new MenuItem("Open in Player");
|
||||
var openInPlayer = new MenuItem("Open in Player");
|
||||
openInPlayer.setOnAction(e -> startPlayer(getSelectedThumbCells(cell)));
|
||||
|
||||
MenuItem start = new MenuItem("Start Recording");
|
||||
var start = new MenuItem("Start Recording");
|
||||
start.setOnAction(e -> startStopAction(getSelectedThumbCells(cell), true));
|
||||
|
||||
MenuItem stop = new MenuItem("Stop Recording");
|
||||
var stop = new MenuItem("Stop Recording");
|
||||
stop.setOnAction(e -> startStopAction(getSelectedThumbCells(cell), false));
|
||||
MenuItem startStop = recorder.isTracked(model) ? stop : start;
|
||||
var startStop = recorder.isTracked(model) ? stop : start;
|
||||
|
||||
MenuItem recordUntil = new MenuItem("Start Recording Until");
|
||||
var recordUntil = new MenuItem("Start Recording Until");
|
||||
recordUntil.setOnAction(e -> startRecordingWithTimeLimit(getSelectedThumbCells(cell)));
|
||||
MenuItem addPaused = new MenuItem("Add in paused state");
|
||||
var addPaused = new MenuItem("Add in paused state");
|
||||
addPaused.setOnAction(e -> addPaused(getSelectedThumbCells(cell)));
|
||||
MenuItem recordLater = new MenuItem("Record Later");
|
||||
var recordLater = new MenuItem("Record Later");
|
||||
recordLater.setOnAction(e -> recordLater(getSelectedThumbCells(cell), true));
|
||||
MenuItem removeRecordLater = new MenuItem("Forget Model");
|
||||
var removeRecordLater = new MenuItem("Forget Model");
|
||||
removeRecordLater.setOnAction(e -> recordLater(getSelectedThumbCells(cell), false));
|
||||
MenuItem addRemoveBookmark = recorder.isMarkedForLaterRecording(model) ? removeRecordLater : recordLater;
|
||||
var addRemoveBookmark = recorder.isMarkedForLaterRecording(model) ? removeRecordLater : recordLater;
|
||||
|
||||
MenuItem pause = new MenuItem("Pause Recording");
|
||||
var pause = new MenuItem("Pause Recording");
|
||||
pause.setOnAction(e -> pauseResumeAction(getSelectedThumbCells(cell), true));
|
||||
MenuItem resume = new MenuItem("Resume Recording");
|
||||
var resume = new MenuItem("Resume Recording");
|
||||
resume.setOnAction(e -> pauseResumeAction(getSelectedThumbCells(cell), false));
|
||||
MenuItem pauseResume = recorder.isSuspended(model) ? resume : pause;
|
||||
var pauseResume = recorder.isSuspended(model) ? resume : pause;
|
||||
|
||||
MenuItem follow = new MenuItem("Follow");
|
||||
var follow = new MenuItem("Follow");
|
||||
follow.setOnAction(e -> follow(getSelectedThumbCells(cell), true));
|
||||
MenuItem unfollow = new MenuItem("Unfollow");
|
||||
var unfollow = new MenuItem("Unfollow");
|
||||
unfollow.setOnAction(e -> follow(getSelectedThumbCells(cell), false));
|
||||
|
||||
MenuItem ignore = new MenuItem("Ignore");
|
||||
var addToGroup = new MenuItem("Add to group");
|
||||
addToGroup.setOnAction(e -> addToGroup(model));
|
||||
var editGroup = new MenuItem("Edit group");
|
||||
editGroup.setOnAction(e -> editGroup(model));
|
||||
|
||||
var ignore = new MenuItem("Ignore");
|
||||
ignore.setOnAction(e -> ignore(getSelectedThumbCells(cell)));
|
||||
|
||||
MenuItem refresh = new MenuItem("Refresh Overview");
|
||||
var refresh = new MenuItem("Refresh Overview");
|
||||
refresh.setOnAction(e -> refresh());
|
||||
|
||||
MenuItem openRecDir = new MenuItem("Open recording directory");
|
||||
var openRecDir = new MenuItem("Open recording directory");
|
||||
openRecDir.setOnAction(e -> new OpenRecordingsDir(cell, model).execute());
|
||||
|
||||
MenuItem copyUrl = createCopyUrlMenuItem(cell);
|
||||
MenuItem openInBrowser = createOpenInBrowser(cell);
|
||||
MenuItem sendTip = createTipMenuItem(cell);
|
||||
var copyUrl = createCopyUrlMenuItem(cell);
|
||||
var openInBrowser = createOpenInBrowser(cell);
|
||||
var sendTip = createTipMenuItem(cell);
|
||||
|
||||
configureItemsForSelection(cell, openInPlayer, copyUrl, sendTip);
|
||||
|
||||
|
@ -528,16 +539,18 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
contextMenu.getItems().add(new SeparatorMenuItem());
|
||||
if (site.supportsFollow()) {
|
||||
MenuItem followOrUnFollow = (this instanceof FollowedTab) ? unfollow : follow;
|
||||
var followOrUnFollow = (this instanceof FollowedTab) ? unfollow : follow;
|
||||
followOrUnFollow.setDisable(!site.credentialsAvailable());
|
||||
contextMenu.getItems().add(followOrUnFollow);
|
||||
}
|
||||
if (site.supportsTips()) {
|
||||
contextMenu.getItems().add(sendTip);
|
||||
}
|
||||
Optional<ModelGroup> modelGroup = getModelGroup(model);
|
||||
contextMenu.getItems().add(modelGroup.isEmpty() ? addToGroup : editGroup);
|
||||
contextMenu.getItems().addAll(copyUrl, openInBrowser, ignore, refresh, openRecDir);
|
||||
if (model instanceof MyFreeCamsModel && Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
|
||||
MenuItem debug = new MenuItem("debug");
|
||||
var debug = new MenuItem("debug");
|
||||
debug.setOnAction(e -> MyFreeCamsClient.getInstance().getSessionState(model));
|
||||
contextMenu.getItems().add(debug);
|
||||
}
|
||||
|
@ -545,6 +558,23 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
return contextMenu;
|
||||
}
|
||||
|
||||
private Optional<ModelGroup> getModelGroup(Model model) {
|
||||
try {
|
||||
return recorder.getModelGroup(model);
|
||||
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
|
||||
Dialogs.showError(grid.getScene(), "Error", "Couldn't get model group for model " + model, e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private void editGroup(Model model) {
|
||||
new EditGroupAction(this.getContent(), recorder, model).execute();
|
||||
}
|
||||
|
||||
private void addToGroup(Model model) {
|
||||
new AddToGroupAction(this.getContent(), recorder, model).execute();
|
||||
}
|
||||
|
||||
private void recordLater(List<ThumbCell> list, boolean recordLater) {
|
||||
for (ThumbCell cell : list) {
|
||||
cell.recordLater(recordLater);
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
package ctbrec.ui.tabs;
|
||||
|
||||
import static ctbrec.ui.controls.Dialogs.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.Model;
|
||||
import ctbrec.sites.Site;
|
||||
import ctbrec.ui.SiteUiFactory;
|
||||
|
@ -7,13 +15,6 @@ import ctbrec.ui.controls.SearchPopover;
|
|||
import ctbrec.ui.controls.SearchPopoverTreeList;
|
||||
import javafx.application.Platform;
|
||||
import javafx.concurrent.Task;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import static ctbrec.ui.controls.Dialogs.showError;
|
||||
|
||||
public class ThumbOverviewTabSearchTask extends Task<List<Model>> {
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
package ctbrec.ui.tabs.recorded;
|
||||
|
||||
import ctbrec.ui.JavaFxModel;
|
||||
import ctbrec.ui.action.PlayAction;
|
||||
import javafx.scene.control.TableCell;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
|
||||
public class ClickableTableCell<T> extends TableCell<JavaFxModel, T> {
|
||||
|
||||
public ClickableTableCell() {
|
||||
addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
|
||||
JavaFxModel selectedModel = getTableView().getSelectionModel().getSelectedItem();
|
||||
if(selectedModel != null) {
|
||||
new PlayAction(getTableView(), selectedModel).execute();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package ctbrec.ui.tabs.recorded;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import ctbrec.ui.Icon;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
|
||||
public class IconTableCell<T> extends ClickableTableCell<T> {
|
||||
|
||||
protected String tooltip;
|
||||
protected HBox iconRow;
|
||||
private Map<Icon, ImageView> icons;
|
||||
|
||||
public IconTableCell(Map<Icon, ImageView> icons) {
|
||||
this.icons = Objects.requireNonNullElse(icons, new HashMap<>());
|
||||
iconRow = new HBox(3);
|
||||
}
|
||||
|
||||
protected void show(Icon iconName) {
|
||||
var imageView = icons.get(iconName);
|
||||
if (imageView != null) {
|
||||
iconRow.getChildren().remove(imageView);
|
||||
iconRow.getChildren().add(imageView);
|
||||
}
|
||||
}
|
||||
|
||||
protected void hide(Icon iconName) {
|
||||
var imageView = icons.get(iconName);
|
||||
if (imageView != null) {
|
||||
iconRow.getChildren().remove(imageView);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(T value, boolean empty) {
|
||||
if (tooltip != null) {
|
||||
setTooltip(new Tooltip(tooltip));
|
||||
} else {
|
||||
setTooltip(null);
|
||||
}
|
||||
setGraphic(iconRow);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package ctbrec.ui.tabs.recorded;
|
||||
|
||||
import static ctbrec.ui.Icon.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import ctbrec.Model;
|
||||
import ctbrec.ModelGroup;
|
||||
import ctbrec.recorder.Recorder;
|
||||
import ctbrec.ui.controls.Dialogs;
|
||||
import javafx.scene.image.ImageView;
|
||||
|
||||
public class ModelNameTableCell extends IconTableCell<String> {
|
||||
|
||||
private Recorder recorder;
|
||||
|
||||
public ModelNameTableCell(Recorder recorder) {
|
||||
super(Map.of(GROUP_16, new ImageView(GROUP_16.url())));
|
||||
this.recorder = recorder;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(String item, boolean empty) {
|
||||
setText(null);
|
||||
tooltip = null;
|
||||
hide(GROUP_16);
|
||||
|
||||
if (item != null && !empty) {
|
||||
setText(item);
|
||||
Model m = getTableView().getItems().get(getTableRow().getIndex());
|
||||
Optional<ModelGroup> optionalGroup = getModelGroup(m);
|
||||
if (optionalGroup.isPresent()) {
|
||||
ModelGroup group = optionalGroup.get();
|
||||
setText(group.getName() + " (aka " + item + ')');
|
||||
show(GROUP_16);
|
||||
tooltip = group.getModelUrls().size() + " models:\n";
|
||||
tooltip += group.getModelUrls().stream().collect(Collectors.joining("\n"));
|
||||
}
|
||||
}
|
||||
super.updateItem(item, empty);
|
||||
}
|
||||
|
||||
private Optional<ModelGroup> getModelGroup(Model model) {
|
||||
try {
|
||||
return recorder.getModelGroup(model);
|
||||
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
|
||||
Dialogs.showError(getScene(), "Error", "Couldn't get model group for model " + model, e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package ctbrec.ui.tabs.recorded;
|
||||
|
||||
import static ctbrec.ui.Icon.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
|
||||
import javafx.scene.image.ImageView;
|
||||
|
||||
public class OnlineTableCell extends IconTableCell<Boolean> {
|
||||
|
||||
public OnlineTableCell() {
|
||||
super(Map.of(CHECK_16, new ImageView(CHECK_16.url())));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(Boolean value, boolean empty) {
|
||||
if (!empty) {
|
||||
if (Objects.equal(value, Boolean.TRUE)) {
|
||||
show(CHECK_16);
|
||||
tooltip = "Online";
|
||||
} else {
|
||||
hide(CHECK_16);
|
||||
tooltip = null;
|
||||
}
|
||||
} else {
|
||||
tooltip = null;
|
||||
}
|
||||
super.updateItem(value, empty);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package ctbrec.ui.tabs;
|
||||
package ctbrec.ui.tabs.recorded;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
|
@ -37,6 +37,7 @@ import ctbrec.ui.controls.Dialogs;
|
|||
import ctbrec.ui.controls.SearchBox;
|
||||
import ctbrec.ui.controls.autocomplete.AutoFillTextField;
|
||||
import ctbrec.ui.controls.autocomplete.ObservableListSuggester;
|
||||
import ctbrec.ui.tabs.TabSelectionListener;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringPropertyBase;
|
|
@ -1,16 +1,11 @@
|
|||
package ctbrec.ui.tabs;
|
||||
package ctbrec.ui.tabs.recorded;
|
||||
|
||||
import static ctbrec.Recording.State.*;
|
||||
import static ctbrec.ui.UnicodeEmoji.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
|
@ -31,6 +26,7 @@ import org.slf4j.LoggerFactory;
|
|||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.ModelGroup;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.StringUtil;
|
||||
import ctbrec.recorder.Recorder;
|
||||
|
@ -40,7 +36,9 @@ import ctbrec.ui.DesktopIntegration;
|
|||
import ctbrec.ui.JavaFxModel;
|
||||
import ctbrec.ui.PreviewPopupHandler;
|
||||
import ctbrec.ui.StreamSourceSelectionDialog;
|
||||
import ctbrec.ui.action.AddToGroupAction;
|
||||
import ctbrec.ui.action.CheckModelAccountAction;
|
||||
import ctbrec.ui.action.EditGroupAction;
|
||||
import ctbrec.ui.action.EditNotesAction;
|
||||
import ctbrec.ui.action.FollowAction;
|
||||
import ctbrec.ui.action.IgnoreModelsAction;
|
||||
|
@ -59,6 +57,7 @@ import ctbrec.ui.controls.Dialogs;
|
|||
import ctbrec.ui.controls.SearchBox;
|
||||
import ctbrec.ui.controls.autocomplete.AutoFillTextField;
|
||||
import ctbrec.ui.controls.autocomplete.ObservableListSuggester;
|
||||
import ctbrec.ui.tabs.TabSelectionListener;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringPropertyBase;
|
||||
|
@ -157,7 +156,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
|
||||
table.setEditable(true);
|
||||
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
PreviewPopupHandler previewPopupHandler = new PreviewPopupHandler(table);
|
||||
var previewPopupHandler = new PreviewPopupHandler(table);
|
||||
table.setRowFactory(tableview -> {
|
||||
TableRow<JavaFxModel> row = new TableRow<>();
|
||||
row.addEventHandler(MouseEvent.ANY, previewPopupHandler);
|
||||
|
@ -174,7 +173,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
TableColumn<JavaFxModel, String> name = new TableColumn<>("Model");
|
||||
name.setPrefWidth(200);
|
||||
name.setCellValueFactory(new PropertyValueFactory<>("displayName"));
|
||||
name.setCellFactory(new ClickableCellFactory<>());
|
||||
name.setCellFactory(param -> new ModelNameTableCell(recorder));
|
||||
name.setEditable(false);
|
||||
name.setId("name");
|
||||
TableColumn<JavaFxModel, String> url = new TableColumn<>("URL");
|
||||
|
@ -183,15 +182,16 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
url.setPrefWidth(400);
|
||||
url.setEditable(false);
|
||||
url.setId("url");
|
||||
TableColumn<JavaFxModel, String> online = new TableColumn<>("Online");
|
||||
TableColumn<JavaFxModel, Boolean> online = new TableColumn<>("Online");
|
||||
online.setCellValueFactory(cdf -> cdf.getValue().getOnlineProperty());
|
||||
online.setCellFactory(param -> new OnlineTableCell());
|
||||
online.setPrefWidth(100);
|
||||
online.setEditable(false);
|
||||
online.setId("online");
|
||||
online.setStyle(STYLE_ALIGN_CENTER);
|
||||
TableColumn<JavaFxModel, String> recording = new TableColumn<>("Recording");
|
||||
TableColumn<JavaFxModel, Boolean> recording = new TableColumn<>("Recording");
|
||||
recording.setCellValueFactory(cdf -> cdf.getValue().getRecordingProperty());
|
||||
recording.setCellFactory(tc -> new RecordingCell());
|
||||
recording.setCellFactory(tc -> new RecordingTableCell());
|
||||
recording.setPrefWidth(100);
|
||||
recording.setEditable(false);
|
||||
recording.setId("recording");
|
||||
|
@ -272,7 +272,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
|
||||
scrollPane.setContent(table);
|
||||
|
||||
HBox addModelBox = new HBox(5);
|
||||
var addModelBox = new HBox(5);
|
||||
modelLabel.setPadding(new Insets(5, 0, 0, 0));
|
||||
ObservableList<String> suggestions = FXCollections.observableArrayList();
|
||||
sites.forEach(site -> suggestions.add(site.getClass().getSimpleName()));
|
||||
|
@ -302,7 +302,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
checkModelAccountExistance.setOnAction(evt -> new CheckModelAccountAction(checkModelAccountExistance, recorder)
|
||||
.execute(Predicate.not(Model::isMarkedForLaterRecording)));
|
||||
|
||||
HBox filterContainer = new HBox();
|
||||
var filterContainer = new HBox();
|
||||
filterContainer.setSpacing(0);
|
||||
filterContainer.setPadding(new Insets(0));
|
||||
filterContainer.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
@ -326,7 +326,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
filterContainer.getChildren().add(filter);
|
||||
addModelBox.getChildren().add(filterContainer);
|
||||
|
||||
BorderPane root = new BorderPane();
|
||||
var root = new BorderPane();
|
||||
root.setPadding(new Insets(5));
|
||||
root.setTop(addModelBox);
|
||||
root.setCenter(scrollPane);
|
||||
|
@ -338,7 +338,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
private void jumpToNextModel(KeyCode code) {
|
||||
if (!table.getItems().isEmpty() && (code.isLetterKey() || code.isDigitKey())) {
|
||||
// determine where to start looking for the next model
|
||||
int startAt = 0;
|
||||
var startAt = 0;
|
||||
if (table.getSelectionModel().getSelectedIndex() >= 0) {
|
||||
startAt = table.getSelectionModel().getSelectedIndex() + 1;
|
||||
if (startAt >= table.getItems().size()) {
|
||||
|
@ -346,7 +346,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
}
|
||||
|
||||
String c = code.getChar().toLowerCase();
|
||||
var c = code.getChar().toLowerCase();
|
||||
int i = startAt;
|
||||
do {
|
||||
JavaFxModel current = table.getItems().get(i);
|
||||
|
@ -378,7 +378,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
private void updatePriority(JavaFxModel model, int priority) {
|
||||
try {
|
||||
if (priority < 0 || priority > 100) {
|
||||
String msg = "Priority has to be between 0 and 100";
|
||||
var msg = "Priority has to be between 0 and 100";
|
||||
Dialogs.showError(table.getScene(), "Invalid value", msg, null);
|
||||
} else {
|
||||
model.setPriority(priority);
|
||||
|
@ -404,7 +404,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
|
||||
private void addModelByUrl(String url) {
|
||||
for (Site site : sites) {
|
||||
Model newModel = site.createModelFromUrl(url);
|
||||
var newModel = site.createModelFromUrl(url);
|
||||
if (newModel != null) {
|
||||
try {
|
||||
recorder.addModel(newModel);
|
||||
|
@ -431,7 +431,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
for (Site site : sites) {
|
||||
if (Objects.equals(siteName.toLowerCase(), site.getClass().getSimpleName().toLowerCase())) {
|
||||
try {
|
||||
Model m = site.createModel(modelName);
|
||||
var m = site.createModel(modelName);
|
||||
recorder.addModel(m);
|
||||
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
|
||||
Dialogs.showError(getTabPane().getScene(), "Couldn't add model", "The model " + modelName + " could not be added:", e1);
|
||||
|
@ -548,18 +548,18 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
String[] tokens = filter.split(" ");
|
||||
observableModels.addAll(filteredModels);
|
||||
filteredModels.clear();
|
||||
for (int i = 0; i < table.getItems().size(); i++) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (var i = 0; i < table.getItems().size(); i++) {
|
||||
var sb = new StringBuilder();
|
||||
for (TableColumn<JavaFxModel, ?> tc : table.getColumns()) {
|
||||
Object cellData = tc.getCellData(i);
|
||||
if (cellData != null) {
|
||||
String content = cellData.toString();
|
||||
var content = cellData.toString();
|
||||
sb.append(content).append(' ');
|
||||
}
|
||||
}
|
||||
String searchText = sb.toString();
|
||||
var searchText = sb.toString();
|
||||
|
||||
boolean tokensMissing = false;
|
||||
var tokensMissing = false;
|
||||
for (String token : tokens) {
|
||||
if (!searchText.toLowerCase().contains(token.toLowerCase())) {
|
||||
tokensMissing = true;
|
||||
|
@ -594,18 +594,14 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
.peek(fxm -> { // NOSONAR
|
||||
for (Recording recording : recordings) {
|
||||
if(recording.getStatus() == RECORDING && Objects.equals(recording.getModel(), fxm)){
|
||||
String recordingValue = HEAVY_CHECK_MARK;
|
||||
if(!Objects.equals(recording.getModel().getRecordUntil(), Instant.ofEpochMilli(Model.RECORD_INDEFINITELY))) {
|
||||
recordingValue += ' ' + CLOCK;
|
||||
}
|
||||
fxm.getRecordingProperty().set(recordingValue);
|
||||
fxm.setRecordingProperty(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (Model onlineModel : onlineModels) {
|
||||
if(Objects.equals(onlineModel, fxm)) {
|
||||
fxm.getOnlineProperty().set(HEAVY_CHECK_MARK);
|
||||
fxm.setOnlineProperty(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -616,7 +612,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
};
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
|
||||
Thread t = new Thread(r);
|
||||
var t = new Thread(r);
|
||||
t.setDaemon(true);
|
||||
t.setName("RecordedModelsTab UpdateService");
|
||||
return t;
|
||||
|
@ -645,44 +641,49 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
if (selectedModels.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
MenuItem stop = new MenuItem("Remove Model");
|
||||
var stop = new MenuItem("Remove Model");
|
||||
stop.setOnAction(e -> stopAction(selectedModels));
|
||||
MenuItem recordLater = new MenuItem("Record Later");
|
||||
var recordLater = new MenuItem("Record Later");
|
||||
recordLater.setOnAction(e -> recordLater(selectedModels));
|
||||
|
||||
MenuItem copyUrl = new MenuItem("Copy URL");
|
||||
var copyUrl = new MenuItem("Copy URL");
|
||||
copyUrl.setOnAction(e -> {
|
||||
Model selected = selectedModels.get(0);
|
||||
final Clipboard clipboard = Clipboard.getSystemClipboard();
|
||||
final ClipboardContent content = new ClipboardContent();
|
||||
final var clipboard = Clipboard.getSystemClipboard();
|
||||
final var content = new ClipboardContent();
|
||||
content.putString(selected.getUrl());
|
||||
clipboard.setContent(content);
|
||||
});
|
||||
|
||||
MenuItem pauseRecording = new MenuItem("Pause Recording");
|
||||
var pauseRecording = new MenuItem("Pause Recording");
|
||||
pauseRecording.setOnAction(e -> pauseRecording(selectedModels));
|
||||
MenuItem resumeRecording = new MenuItem("Resume Recording");
|
||||
var resumeRecording = new MenuItem("Resume Recording");
|
||||
resumeRecording.setOnAction(e -> resumeRecording(selectedModels));
|
||||
MenuItem stopRecordingAt = new MenuItem("Stop Recording at Date");
|
||||
var stopRecordingAt = new MenuItem("Stop Recording at Date");
|
||||
stopRecordingAt.setOnAction(e -> setStopDate(selectedModels.get(0)));
|
||||
MenuItem removeTimeLimit = new MenuItem("Remove Time Limit");
|
||||
var removeTimeLimit = new MenuItem("Remove Time Limit");
|
||||
removeTimeLimit.setOnAction(e -> removeTimeLimit(selectedModels.get(0)));
|
||||
MenuItem openInBrowser = new MenuItem("Open in Browser");
|
||||
var openInBrowser = new MenuItem("Open in Browser");
|
||||
openInBrowser.setOnAction(e -> DesktopIntegration.open(selectedModels.get(0).getUrl()));
|
||||
MenuItem openInPlayer = new MenuItem("Open in Player");
|
||||
var openInPlayer = new MenuItem("Open in Player");
|
||||
openInPlayer.setOnAction(e -> openInPlayer(selectedModels.get(0)));
|
||||
MenuItem switchStreamSource = new MenuItem("Switch resolution");
|
||||
var switchStreamSource = new MenuItem("Switch resolution");
|
||||
switchStreamSource.setOnAction(e -> switchStreamSource(selectedModels.get(0)));
|
||||
MenuItem follow = new MenuItem("Follow");
|
||||
var follow = new MenuItem("Follow");
|
||||
follow.setOnAction(e -> follow(selectedModels));
|
||||
follow.setDisable(!selectedModels.stream().allMatch(m -> m.getSite().supportsFollow() && m.getSite().credentialsAvailable()));
|
||||
MenuItem ignore = new MenuItem("Ignore");
|
||||
var ignore = new MenuItem("Ignore");
|
||||
ignore.setOnAction(e -> ignore(selectedModels));
|
||||
MenuItem notes = new MenuItem("Notes");
|
||||
var notes = new MenuItem("Notes");
|
||||
notes.setOnAction(e -> notes(selectedModels));
|
||||
MenuItem openRecDir = new MenuItem("Open recording directory");
|
||||
var openRecDir = new MenuItem("Open recording directory");
|
||||
openRecDir.setOnAction(e -> new OpenRecordingsDir(table, selectedModels.get(0)).execute());
|
||||
|
||||
var addToGroup = new MenuItem("Add to group");
|
||||
addToGroup.setOnAction(e -> addToGroup(selectedModels.get(0)));
|
||||
var editGroup = new MenuItem("Edit group");
|
||||
editGroup.setOnAction(e -> editGroup(selectedModels.get(0)));
|
||||
|
||||
ContextMenu menu = new CustomMouseBehaviorContextMenu(stop, recordLater);
|
||||
if (selectedModels.size() == 1) {
|
||||
menu.getItems().add(selectedModels.get(0).isSuspended() ? resumeRecording : pauseRecording);
|
||||
|
@ -693,6 +694,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
} else {
|
||||
menu.getItems().addAll(resumeRecording, pauseRecording);
|
||||
}
|
||||
Optional<ModelGroup> modelGroup = getModelGroup(selectedModels.get(0));
|
||||
menu.getItems().add(modelGroup.isEmpty() ? addToGroup : editGroup);
|
||||
menu.getItems().addAll(copyUrl, openInPlayer, openInBrowser, openRecDir, switchStreamSource, follow, notes, ignore);
|
||||
|
||||
if (selectedModels.size() > 1) {
|
||||
|
@ -706,6 +709,25 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
return menu;
|
||||
}
|
||||
|
||||
private Optional<ModelGroup> getModelGroup(Model model) {
|
||||
try {
|
||||
return recorder.getModelGroup(model);
|
||||
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
|
||||
Dialogs.showError(grid.getScene(), "Error", "Couldn't get model group for model " + model, e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private void addToGroup(Model model) {
|
||||
new AddToGroupAction(this.getContent(), recorder, model).execute();
|
||||
table.refresh();
|
||||
}
|
||||
|
||||
private void editGroup(Model model) {
|
||||
new EditGroupAction(this.getContent(), recorder, model).execute();
|
||||
table.refresh();
|
||||
}
|
||||
|
||||
private void setStopDate(JavaFxModel model) {
|
||||
new SetStopDateAction(table, model.getDelegate(), recorder) //
|
||||
.execute() //
|
||||
|
@ -735,7 +757,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
|
||||
private void switchStreamSource(JavaFxModel fxModel) {
|
||||
String couldntSwitchHeaderText = "Couldn't switch stream resolution";
|
||||
var couldntSwitchHeaderText = "Couldn't switch stream resolution";
|
||||
|
||||
try {
|
||||
if (!fxModel.isOnline(true)) {
|
||||
|
@ -781,7 +803,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
|
||||
private boolean stopAction(List<JavaFxModel> selectedModels) {
|
||||
boolean confirmed = true;
|
||||
var confirmed = true;
|
||||
if (Config.getInstance().getSettings().confirmationForDangerousActions) {
|
||||
int n = selectedModels.size();
|
||||
String plural = n > 1 ? "s" : "";
|
||||
|
@ -829,9 +851,9 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
Config.getInstance().getSettings().recordedModelsSortType = col.getSortType().toString();
|
||||
}
|
||||
int columns = table.getColumns().size();
|
||||
double[] columnWidths = new double[columns];
|
||||
String[] columnIds = new String[columns];
|
||||
for (int i = 0; i < columnWidths.length; i++) {
|
||||
var columnWidths = new double[columns];
|
||||
var columnIds = new String[columns];
|
||||
for (var i = 0; i < columnWidths.length; i++) {
|
||||
columnWidths[i] = table.getColumns().get(i).getWidth();
|
||||
columnIds[i] = table.getColumns().get(i).getId();
|
||||
}
|
||||
|
@ -862,8 +884,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
private void restoreColumnOrder() {
|
||||
String[] columnIds = Config.getInstance().getSettings().recordedModelsColumnIds;
|
||||
ObservableList<TableColumn<JavaFxModel,?>> columns = table.getColumns();
|
||||
for (int i = 0; i < columnIds.length; i++) {
|
||||
for (int j = 0; j < table.getColumns().size(); j++) {
|
||||
for (var i = 0; i < columnIds.length; i++) {
|
||||
for (var j = 0; j < table.getColumns().size(); j++) {
|
||||
if(Objects.equals(columnIds[i], columns.get(j).getId())) {
|
||||
TableColumn<JavaFxModel, ?> col = columns.get(j);
|
||||
columns.remove(j); // NOSONAR
|
||||
|
@ -876,7 +898,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
private void restoreColumnWidths() {
|
||||
double[] columnWidths = Config.getInstance().getSettings().recordedModelsColumnWidths;
|
||||
if (columnWidths != null && columnWidths.length == table.getColumns().size()) {
|
||||
for (int i = 0; i < columnWidths.length; i++) {
|
||||
for (var i = 0; i < columnWidths.length; i++) {
|
||||
table.getColumns().get(i).setPrefWidth(columnWidths[i]);
|
||||
}
|
||||
}
|
||||
|
@ -930,25 +952,4 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
return tableCell;
|
||||
}
|
||||
}
|
||||
|
||||
private class RecordingCell extends TableCell<JavaFxModel, String> {
|
||||
@Override
|
||||
protected void updateItem(String value, boolean empty) {
|
||||
super.updateItem(value, empty);
|
||||
if (value == null) {
|
||||
setTooltip(null);
|
||||
setText(null);
|
||||
} else {
|
||||
Model m = getTableView().getItems().get(getTableRow().getIndex());
|
||||
if (m.isRecordingTimeLimited()) {
|
||||
Tooltip tooltip = new Tooltip();
|
||||
DateTimeFormatter dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT);
|
||||
ZonedDateTime zonedDateTime = m.getRecordUntil().atZone(ZoneId.systemDefault());
|
||||
tooltip.setText("Recording until " + dtf.format(zonedDateTime));
|
||||
setTooltip(tooltip);
|
||||
}
|
||||
setText(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
package ctbrec.ui.tabs;
|
||||
package ctbrec.ui.tabs.recorded;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import ctbrec.recorder.Recorder;
|
||||
import ctbrec.sites.Site;
|
||||
import ctbrec.ui.ShutdownListener;
|
||||
import ctbrec.ui.tabs.TabSelectionListener;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.geometry.Side;
|
||||
import javafx.scene.control.Tab;
|
|
@ -0,0 +1,47 @@
|
|||
package ctbrec.ui.tabs.recorded;
|
||||
|
||||
import static ctbrec.ui.Icon.*;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import ctbrec.Model;
|
||||
import ctbrec.SubsequentAction;
|
||||
import javafx.scene.image.ImageView;
|
||||
|
||||
public class RecordingTableCell extends IconTableCell<Boolean> {
|
||||
|
||||
public RecordingTableCell() {
|
||||
super(Map.of( //
|
||||
CHECK_16, new ImageView(CHECK_16.url()), //
|
||||
CLOCK_16, new ImageView(CLOCK_16.url())));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(Boolean value, boolean empty) {
|
||||
tooltip = null;
|
||||
hide(CHECK_16);
|
||||
hide(CLOCK_16);
|
||||
if (value == null || empty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Objects.equals(value, Boolean.TRUE)) {
|
||||
show(CHECK_16);
|
||||
tooltip = "Recording";
|
||||
}
|
||||
|
||||
Model m = getTableView().getItems().get(getTableRow().getIndex());
|
||||
if (m.isRecordingTimeLimited()) {
|
||||
show(CLOCK_16);
|
||||
var dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT);
|
||||
var zonedDateTime = m.getRecordUntil().atZone(ZoneId.systemDefault());
|
||||
String action = m.getRecordUntilSubsequentAction() == SubsequentAction.PAUSE ? "Pause" : "Remove";
|
||||
tooltip = action + " at " + dtf.format(zonedDateTime);
|
||||
}
|
||||
super.updateItem(value, empty);
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 822 B |
After Width: | Height: | Size: 317 B |
|
@ -0,0 +1,57 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
sodipodi:docname="check-small.svg"
|
||||
inkscape:version="1.0.2 (e86c870879, 2021-01-15, custom)"
|
||||
inkscape:export-filename="/tmp/check-small.png"
|
||||
inkscape:export-xdpi="6.1875"
|
||||
inkscape:export-ydpi="6.1875">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1692"
|
||||
inkscape:window-height="996"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.86731066"
|
||||
inkscape:cx="195.31674"
|
||||
inkscape:cy="250.01464"
|
||||
inkscape:window-x="492"
|
||||
inkscape:window-y="126"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg4" />
|
||||
<!-- Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) -->
|
||||
<path
|
||||
id="path2"
|
||||
d="m 381.39691,131.00011 c -3.5266,-0.0142 -7.05951,1.31523 -9.76101,3.9928 L 209.31438,295.84932 140.6532,226.69926 c -5.35937,-5.39755 -14.08537,-5.43238 -19.48837,-0.0784 l -26.089151,25.85232 c -5.402999,5.35397 -5.439003,14.07004 -0.07851,19.46875 L 199.24608,376.92842 c 5.35938,5.39756 14.08538,5.43354 19.48837,0.0784 L 416.9263,180.60297 c 5.40184,-5.35511 5.43567,-14.07117 0.0762,-19.46875 l -25.87831,-26.0629 c -2.67968,-2.69878 -6.20076,-4.05702 -9.72735,-4.07121 z"
|
||||
style="stroke-width:1.14778" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 291 B |
After Width: | Height: | Size: 757 B |
After Width: | Height: | Size: 603 B |
Before Width: | Height: | Size: 9.6 KiB |
|
@ -42,11 +42,14 @@ The part you have to copy is
|
|||
|
||||
<a id="variables" />
|
||||
|
||||
###### 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
|
||||
${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
|
||||
|
@ -116,4 +119,15 @@ The part you have to copy is
|
|||
</table>
|
||||
|
||||
|
||||
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/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:
|
||||
|
||||
${modelGroupName?${modelSanitizedName}}
|
||||
|
||||
It can be read like "use the modelGroupName, but if that is not available use modelSanitizedName".
|
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 8.6 KiB |
|
@ -18,6 +18,7 @@ import java.util.List;
|
|||
import java.util.Map.Entry;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
@ -31,6 +32,7 @@ import ctbrec.Settings.SplitStrategy;
|
|||
import ctbrec.io.FileJsonAdapter;
|
||||
import ctbrec.io.ModelJsonAdapter;
|
||||
import ctbrec.io.PostProcessorJsonAdapter;
|
||||
import ctbrec.io.UuidJSonAdapter;
|
||||
import ctbrec.recorder.postprocessing.DeleteTooShort;
|
||||
import ctbrec.recorder.postprocessing.PostProcessor;
|
||||
import ctbrec.recorder.postprocessing.RemoveKeepFile;
|
||||
|
@ -74,6 +76,7 @@ public class Config {
|
|||
.add(Model.class, new ModelJsonAdapter(sites))
|
||||
.add(PostProcessor.class, new PostProcessorJsonAdapter())
|
||||
.add(File.class, new FileJsonAdapter())
|
||||
.add(UUID.class, new UuidJSonAdapter())
|
||||
.build();
|
||||
JsonAdapter<Settings> adapter = moshi.adapter(Settings.class).lenient();
|
||||
File configFile = new File(configDir, filename);
|
||||
|
@ -182,11 +185,11 @@ public class Config {
|
|||
}
|
||||
// 3.11.0 make Cam4 model names lower case
|
||||
settings.models.stream()
|
||||
.filter(Cam4Model.class::isInstance)
|
||||
.forEach(m -> m.setName(m.getName().toLowerCase()));
|
||||
.filter(Cam4Model.class::isInstance)
|
||||
.forEach(m -> m.setName(m.getName().toLowerCase()));
|
||||
settings.modelsIgnored.stream()
|
||||
.filter(Cam4Model.class::isInstance)
|
||||
.forEach(m -> m.setName(m.getName().toLowerCase()));
|
||||
.filter(Cam4Model.class::isInstance)
|
||||
.forEach(m -> m.setName(m.getName().toLowerCase()));
|
||||
// 4.1.2 reduce models ignore to store only the URL
|
||||
if (settings.modelsIgnored != null && !settings.modelsIgnored.isEmpty()) {
|
||||
settings.ignoredModels = settings.modelsIgnored.stream()
|
||||
|
@ -227,13 +230,14 @@ public class Config {
|
|||
}
|
||||
|
||||
public synchronized void save() throws IOException {
|
||||
if (savingDisabled) {
|
||||
return;
|
||||
}
|
||||
if (savingDisabled) {
|
||||
return;
|
||||
}
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(Model.class, new ModelJsonAdapter())
|
||||
.add(PostProcessor.class, new PostProcessorJsonAdapter())
|
||||
.add(File.class, new FileJsonAdapter())
|
||||
.add(UUID.class, new UuidJSonAdapter())
|
||||
.build();
|
||||
JsonAdapter<Settings> adapter = moshi.adapter(Settings.class).indent(" ");
|
||||
String json = adapter.toJson(settings);
|
||||
|
@ -299,12 +303,12 @@ public class Config {
|
|||
public String getModelNotes(Model m) {
|
||||
return Config.getInstance().getSettings().modelNotes.getOrDefault(m.getUrl(), "");
|
||||
}
|
||||
|
||||
|
||||
public void disableSaving() {
|
||||
savingDisabled = true;
|
||||
savingDisabled = true;
|
||||
}
|
||||
|
||||
|
||||
public void enableSaving() {
|
||||
savingDisabled = false;
|
||||
savingDisabled = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
package ctbrec;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
public class ModelGroup implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private UUID id;
|
||||
private String name;
|
||||
private List<String> modelUrls = new LinkedList<>();
|
||||
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(UUID id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public List<String> getModelUrls() {
|
||||
return modelUrls;
|
||||
}
|
||||
|
||||
public void setModelUrls(List<String> modelUrls) {
|
||||
this.modelUrls = modelUrls;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
ModelGroup other = (ModelGroup) obj;
|
||||
return Objects.equals(id, other.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ModelGroup [id=" + id + ", name=" + name + ", models=" + modelUrls + "]";
|
||||
}
|
||||
|
||||
public void add(String modelUrl) {
|
||||
modelUrls.add(modelUrl);
|
||||
}
|
||||
|
||||
public void add(Model model) {
|
||||
modelUrls.add(model.getUrl());
|
||||
}
|
||||
}
|
|
@ -3,8 +3,10 @@ package ctbrec;
|
|||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import ctbrec.event.EventHandlerConfiguration;
|
||||
import ctbrec.recorder.postprocessing.PostProcessor;
|
||||
|
@ -104,10 +106,12 @@ public class Settings {
|
|||
public String mfcPassword = "";
|
||||
public String mfcUsername = "";
|
||||
public boolean minimizeToTray = false;
|
||||
@Deprecated
|
||||
public int minimumLengthInSeconds = 0;
|
||||
public long minimumSpaceLeftInBytes = 0;
|
||||
public Map<String, String> modelNotes = new HashMap<>();
|
||||
public List<Model> models = new ArrayList<>();
|
||||
public Set<ModelGroup> modelGroups = new HashSet<>();
|
||||
@Deprecated
|
||||
public List<Model> modelsIgnored = new ArrayList<>();
|
||||
public boolean monitorClipboard = false;
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
package ctbrec.io;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
import com.squareup.moshi.JsonWriter;
|
||||
|
||||
public class UuidJSonAdapter extends JsonAdapter<UUID> {
|
||||
|
||||
@Override
|
||||
public UUID fromJson(JsonReader reader) throws IOException {
|
||||
return UUID.fromString(reader.nextString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toJson(JsonWriter writer, UUID value) throws IOException {
|
||||
writer.value(value.toString());
|
||||
}
|
||||
|
||||
}
|
|
@ -22,6 +22,7 @@ import java.util.Map;
|
|||
import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
@ -45,6 +46,7 @@ import com.google.common.eventbus.Subscribe;
|
|||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.ModelGroup;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.Recording.State;
|
||||
import ctbrec.event.Event;
|
||||
|
@ -54,6 +56,7 @@ import ctbrec.event.NoSpaceLeftEvent;
|
|||
import ctbrec.event.RecordingStateChangedEvent;
|
||||
import ctbrec.io.HttpClient;
|
||||
import ctbrec.recorder.download.Download;
|
||||
import ctbrec.recorder.postprocessing.PostProcessingContext;
|
||||
import ctbrec.recorder.postprocessing.PostProcessor;
|
||||
import ctbrec.sites.Site;
|
||||
|
||||
|
@ -88,18 +91,7 @@ public class NextGenLocalRecorder implements Recorder {
|
|||
downloadPool = Executors.newScheduledThreadPool(5, createThreadFactory("Download", MAX_PRIORITY));
|
||||
threadPoolScaler = new ThreadPoolScaler((ThreadPoolExecutor) downloadPool, 5);
|
||||
recordingManager = new RecordingManager(config, sites);
|
||||
config.getSettings().models.stream().forEach(m -> {
|
||||
if (m.getSite() != null) {
|
||||
if (m.getSite().isEnabled()) {
|
||||
models.add(m);
|
||||
} else {
|
||||
LOG.info("{} disabled -> ignoring {}", m.getSite().getName(), m.getName());
|
||||
}
|
||||
} else {
|
||||
LOG.info("Site for model {} is unknown -> ignoring", m.getName());
|
||||
}
|
||||
});
|
||||
|
||||
loadModels();
|
||||
int ppThreads = config.getSettings().postProcessingThreads;
|
||||
ppPool = new ThreadPoolExecutor(ppThreads, ppThreads, 5, TimeUnit.MINUTES, ppQueue, createThreadFactory("PP", MIN_PRIORITY));
|
||||
|
||||
|
@ -127,6 +119,20 @@ public class NextGenLocalRecorder implements Recorder {
|
|||
}, 1, 1, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private void loadModels() {
|
||||
config.getSettings().models.stream().forEach(m -> {
|
||||
if (m.getSite() != null) {
|
||||
if (m.getSite().isEnabled()) {
|
||||
models.add(m);
|
||||
} else {
|
||||
LOG.info("{} disabled -> ignoring {}", m.getSite().getName(), m.getName());
|
||||
}
|
||||
} else {
|
||||
LOG.info("Site for model {} is unknown -> ignoring", m.getName());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void startCompletionHandler() {
|
||||
downloadCompletionPool.submit(() -> {
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
|
@ -209,9 +215,10 @@ public class NextGenLocalRecorder implements Recorder {
|
|||
recordingManager.saveRecording(recording);
|
||||
recording.postprocess();
|
||||
List<PostProcessor> postProcessors = config.getSettings().postProcessors;
|
||||
PostProcessingContext ctx = createPostProcessingContext(recording);
|
||||
for (PostProcessor postProcessor : postProcessors) {
|
||||
LOG.debug("Running post-processor: {}", postProcessor.getName());
|
||||
boolean continuePP = postProcessor.postprocess(recording, recordingManager, config);
|
||||
boolean continuePP = postProcessor.postprocess(ctx);
|
||||
if (!continuePP) {
|
||||
break;
|
||||
}
|
||||
|
@ -237,6 +244,15 @@ public class NextGenLocalRecorder implements Recorder {
|
|||
});
|
||||
}
|
||||
|
||||
private PostProcessingContext createPostProcessingContext(Recording recording) {
|
||||
PostProcessingContext ctx = new PostProcessingContext();
|
||||
ctx.setConfig(config);
|
||||
ctx.setRecorder(this);
|
||||
ctx.setRecording(recording);
|
||||
ctx.setRecordingManager(recordingManager);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
private void setRecordingStatus(Recording recording, State status) {
|
||||
recording.setStatus(status);
|
||||
RecordingStateChangedEvent evt = new RecordingStateChangedEvent(recording.getDownload().getTarget(), status, recording.getModel(),
|
||||
|
@ -761,4 +777,23 @@ public class NextGenLocalRecorder implements Recorder {
|
|||
public int getModelCount() {
|
||||
return (int) models.stream().filter(m -> !m.isMarkedForLaterRecording()).count();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ModelGroup> getModelGroups() {
|
||||
return config.getSettings().modelGroups;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveModelGroup(ModelGroup group) throws IOException {
|
||||
Set<ModelGroup> modelGroups = config.getSettings().modelGroups;
|
||||
modelGroups.remove(group);
|
||||
modelGroups.add(group);
|
||||
config.save();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteModelGroup(ModelGroup group) throws IOException {
|
||||
config.getSettings().modelGroups.remove(group);
|
||||
config.save();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
package ctbrec.recorder;
|
||||
|
||||
import ctbrec.Model;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.io.HttpClient;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import ctbrec.Model;
|
||||
import ctbrec.ModelGroup;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.io.HttpClient;
|
||||
|
||||
public interface Recorder {
|
||||
public void addModel(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException;
|
||||
|
||||
|
@ -143,5 +146,29 @@ public interface Recorder {
|
|||
*/
|
||||
public void resume() throws InvalidKeyException, NoSuchAlgorithmException, IOException;
|
||||
|
||||
/**
|
||||
* Returns the number of models, which are on the recording list and not marked for later recording
|
||||
* @return
|
||||
*/
|
||||
public int getModelCount();
|
||||
|
||||
public Set<ModelGroup> getModelGroups() throws InvalidKeyException, NoSuchAlgorithmException, IOException;
|
||||
|
||||
/**
|
||||
* Saves a model group. If the group already exists, it will be overwritten. Otherwise it will
|
||||
* be saved as a new group.
|
||||
* @param group
|
||||
* @throws IOException
|
||||
* @throws NoSuchAlgorithmException
|
||||
* @throws InvalidKeyException
|
||||
*/
|
||||
public void saveModelGroup(ModelGroup group) throws IOException, InvalidKeyException, NoSuchAlgorithmException;
|
||||
|
||||
public void deleteModelGroup(ModelGroup group) throws IOException, InvalidKeyException, NoSuchAlgorithmException;
|
||||
|
||||
default Optional<ModelGroup> getModelGroup(Model model) throws InvalidKeyException, NoSuchAlgorithmException, IOException {
|
||||
return getModelGroups().stream()
|
||||
.filter(mg -> mg.getModelUrls().contains(model.getUrl()))
|
||||
.findFirst();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,10 @@ package ctbrec.recorder;
|
|||
import static ctbrec.recorder.NextGenLocalRecorder.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
@ -12,6 +15,7 @@ import org.slf4j.LoggerFactory;
|
|||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.ModelGroup;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.recorder.download.Download;
|
||||
|
||||
|
@ -26,7 +30,7 @@ public class RecordingPreconditions {
|
|||
this.recorder = recorder;
|
||||
}
|
||||
|
||||
void check(Model model) throws IOException {
|
||||
void check(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||
ensureRecorderIsActive();
|
||||
ensureModelIsNotSuspended(model);
|
||||
ensureModelIsNotMarkedForLaterRecording(model);
|
||||
|
@ -36,6 +40,7 @@ public class RecordingPreconditions {
|
|||
ensureEnoughSpaceForRecording();
|
||||
ensureDownloadSlotAvailable(model);
|
||||
ensureModelIsOnline(model);
|
||||
ensureNoOtherFromModelGroupIsRecording(model);
|
||||
}
|
||||
|
||||
private void ensureModelIsOnline(Model model) {
|
||||
|
@ -130,4 +135,55 @@ public class RecordingPreconditions {
|
|||
int concurrentRecordings = Config.getInstance().getSettings().concurrentRecordings;
|
||||
return concurrentRecordings == 0 || concurrentRecordings > 0 && recorder.getRecordingProcesses().size() < concurrentRecordings;
|
||||
}
|
||||
|
||||
private void ensureNoOtherFromModelGroupIsRecording(Model model) throws InvalidKeyException, NoSuchAlgorithmException, IOException {
|
||||
Optional<ModelGroup> modelGroup = recorder.getModelGroup(model);
|
||||
if (modelGroup.isPresent()) {
|
||||
for (String modelUrl : modelGroup.get().getModelUrls()) {
|
||||
if (modelUrl.equals(model.getUrl())) {
|
||||
// no other model with higher prio is online, start recording
|
||||
// but before that stop all recordings of models with lower prio
|
||||
stopModelsWithLowerPrio(modelGroup.get());
|
||||
return;
|
||||
} else {
|
||||
Optional<Model> otherModel = getModelForUrl(modelUrl);
|
||||
if (otherModel.isPresent() && otherModelCanBeRecorded(otherModel.get())) {
|
||||
throw new PreconditionNotMetException(otherModel.get() + " from the same group is already recorded");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void stopModelsWithLowerPrio(ModelGroup modelGroup) throws InvalidKeyException, NoSuchAlgorithmException, IOException {
|
||||
recorder.getCurrentlyRecording().stream()
|
||||
.filter(m -> modelGroup.getModelUrls().contains(m.getUrl()))
|
||||
.forEach(recorder::stopRecordingProcess);
|
||||
|
||||
}
|
||||
|
||||
private Optional<Model> getModelForUrl(String modelUrl) {
|
||||
return Config.getInstance().getSettings().models.stream()
|
||||
.filter(m -> Objects.equals(m.getUrl(), modelUrl))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
private boolean otherModelCanBeRecorded(Model model) {
|
||||
try {
|
||||
ensureRecorderIsActive();
|
||||
ensureModelIsNotSuspended(model);
|
||||
ensureModelIsNotMarkedForLaterRecording(model);
|
||||
ensureRecordUntilIsInFuture(model);
|
||||
ensureModelShouldBeRecorded(model);
|
||||
ensureEnoughSpaceForRecording();
|
||||
ensureDownloadSlotAvailable(model);
|
||||
ensureModelIsOnline(model);
|
||||
return true;
|
||||
} catch (PreconditionNotMetException e) {
|
||||
// precondition for other model not met
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Couldn't check if preconditions of other model from group are met. Assuming she's offline", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,5 @@
|
|||
package ctbrec.recorder;
|
||||
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.Moshi;
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Hmac;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.event.EventBusHolder;
|
||||
import ctbrec.event.NoSpaceLeftEvent;
|
||||
import ctbrec.event.RecordingStateChangedEvent;
|
||||
import ctbrec.io.*;
|
||||
import ctbrec.sites.Site;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Request.Builder;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
@ -27,7 +7,43 @@ import java.security.InvalidKeyException;
|
|||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.Moshi;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Hmac;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.ModelGroup;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.event.EventBusHolder;
|
||||
import ctbrec.event.NoSpaceLeftEvent;
|
||||
import ctbrec.event.RecordingStateChangedEvent;
|
||||
import ctbrec.io.BandwidthMeter;
|
||||
import ctbrec.io.FileJsonAdapter;
|
||||
import ctbrec.io.HttpClient;
|
||||
import ctbrec.io.HttpException;
|
||||
import ctbrec.io.InstantJsonAdapter;
|
||||
import ctbrec.io.ModelJsonAdapter;
|
||||
import ctbrec.io.UuidJSonAdapter;
|
||||
import ctbrec.sites.Site;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Request.Builder;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class RemoteRecorder implements Recorder {
|
||||
|
||||
|
@ -41,16 +57,20 @@ public class RemoteRecorder implements Recorder {
|
|||
.add(Instant.class, new InstantJsonAdapter())
|
||||
.add(Model.class, new ModelJsonAdapter())
|
||||
.add(File.class, new FileJsonAdapter())
|
||||
.add(UUID.class, new UuidJSonAdapter())
|
||||
.build();
|
||||
private JsonAdapter<ModelListResponse> modelListResponseAdapter = moshi.adapter(ModelListResponse.class);
|
||||
private JsonAdapter<RecordingListResponse> recordingListResponseAdapter = moshi.adapter(RecordingListResponse.class);
|
||||
private JsonAdapter<ModelRequest> modelRequestAdapter = moshi.adapter(ModelRequest.class);
|
||||
private JsonAdapter<ModelGroupRequest> modelGroupRequestAdapter = moshi.adapter(ModelGroupRequest.class);
|
||||
private JsonAdapter<ModelGroupListResponse> modelGroupListResponseAdapter = moshi.adapter(ModelGroupListResponse.class);
|
||||
private JsonAdapter<RecordingRequest> recordingRequestAdapter = moshi.adapter(RecordingRequest.class);
|
||||
private JsonAdapter<SimpleResponse> simpleResponseAdapter = moshi.adapter(SimpleResponse.class);
|
||||
|
||||
private List<Model> models = Collections.emptyList();
|
||||
private List<Model> onlineModels = Collections.emptyList();
|
||||
private List<Recording> recordings = Collections.emptyList();
|
||||
private Set<ModelGroup> modelGroups = new HashSet<>();
|
||||
private List<Site> sites;
|
||||
private long spaceTotal = -1;
|
||||
private long spaceFree = -1;
|
||||
|
@ -91,7 +111,7 @@ public class RemoteRecorder implements Recorder {
|
|||
|
||||
private Optional<String> sendRequest(String action) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||
String msg = "{\"action\": \"" + action + "\"}";
|
||||
LOG.debug("Sending request to recording server: {}", msg);
|
||||
LOG.trace("Sending request to recording server: {}", msg);
|
||||
RequestBody requestBody = RequestBody.Companion.create(msg, JSON);
|
||||
Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(requestBody);
|
||||
addHmacIfNeeded(msg, builder);
|
||||
|
@ -109,7 +129,7 @@ public class RemoteRecorder implements Recorder {
|
|||
|
||||
private void sendRequest(String action, Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||
String payload = modelRequestAdapter.toJson(new ModelRequest(action, model));
|
||||
LOG.debug("Sending request to recording server: {}", payload);
|
||||
LOG.trace("Sending request to recording server: {}", payload);
|
||||
RequestBody body = RequestBody.Companion.create(payload, JSON);
|
||||
Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
|
||||
addHmacIfNeeded(payload, builder);
|
||||
|
@ -139,6 +159,7 @@ public class RemoteRecorder implements Recorder {
|
|||
String msg = recordingRequestAdapter.toJson(recReq);
|
||||
RequestBody body = RequestBody.Companion.create(msg, JSON);
|
||||
Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
|
||||
LOG.trace("Sending request to recording server: {}", msg);
|
||||
addHmacIfNeeded(msg, builder);
|
||||
Request request = builder.build();
|
||||
try (Response response = client.execute(request)) {
|
||||
|
@ -158,6 +179,33 @@ public class RemoteRecorder implements Recorder {
|
|||
}
|
||||
}
|
||||
|
||||
private void sendRequest(String action, ModelGroup model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||
String payload = modelGroupRequestAdapter.toJson(new ModelGroupRequest(action, model));
|
||||
LOG.trace("Sending request to recording server: {}", payload);
|
||||
RequestBody body = RequestBody.Companion.create(payload, JSON);
|
||||
Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
|
||||
addHmacIfNeeded(payload, builder);
|
||||
Request request = builder.build();
|
||||
try (Response response = client.execute(request)) {
|
||||
if (response.isSuccessful()) {
|
||||
String json = response.body().string();
|
||||
updateModelGroups(json);
|
||||
} else {
|
||||
throw new HttpException(response.code(), response.message());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateModelGroups(String responseBody) throws IOException {
|
||||
ModelGroupListResponse resp = modelGroupListResponseAdapter.fromJson(responseBody);
|
||||
if (!resp.status.equals(SUCCESS)) {
|
||||
throw new IOException("Server returned error " + resp.status + " " + resp.msg);
|
||||
}
|
||||
|
||||
modelGroups.clear();
|
||||
modelGroups.addAll(resp.groups);
|
||||
}
|
||||
|
||||
private void addHmacIfNeeded(String msg, Builder builder) throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
|
||||
if (Config.getInstance().getSettings().requireAuthentication) {
|
||||
byte[] key = Config.getInstance().getSettings().key;
|
||||
|
@ -225,10 +273,25 @@ public class RemoteRecorder implements Recorder {
|
|||
syncOnlineModels();
|
||||
syncSpace();
|
||||
syncRecordings();
|
||||
syncModelGroups();
|
||||
sleep();
|
||||
}
|
||||
}
|
||||
|
||||
private void syncModelGroups() {
|
||||
try {
|
||||
sendRequest("listModelGroups").ifPresent(body -> {
|
||||
try {
|
||||
updateModelGroups(body);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error while loading model groups from server", e);
|
||||
}
|
||||
});
|
||||
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException e) {
|
||||
LOG.error("Error while loading model groups from server", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void syncSpace() {
|
||||
try {
|
||||
String msg = "{\"action\": \"space\"}";
|
||||
|
@ -412,6 +475,12 @@ public class RemoteRecorder implements Recorder {
|
|||
public List<Model> models;
|
||||
}
|
||||
|
||||
private static class ModelGroupListResponse {
|
||||
public String status;
|
||||
public String msg;
|
||||
public List<ModelGroup> groups;
|
||||
}
|
||||
|
||||
private static class SimpleResponse {
|
||||
public String status;
|
||||
public String msg;
|
||||
|
@ -460,6 +529,33 @@ public class RemoteRecorder implements Recorder {
|
|||
}
|
||||
}
|
||||
|
||||
public static class ModelGroupRequest {
|
||||
private String action;
|
||||
private ModelGroup modelGroup;
|
||||
|
||||
public ModelGroupRequest(String action, ModelGroup modelGroup) {
|
||||
super();
|
||||
this.action = action;
|
||||
this.modelGroup = modelGroup;
|
||||
}
|
||||
|
||||
public String getAction() {
|
||||
return action;
|
||||
}
|
||||
|
||||
public void setAction(String action) {
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
public ModelGroup getModelGroup() {
|
||||
return modelGroup;
|
||||
}
|
||||
|
||||
public void setModelGroup(ModelGroup model) {
|
||||
this.modelGroup = model;
|
||||
}
|
||||
}
|
||||
|
||||
public static class RecordingRequest {
|
||||
private String action;
|
||||
private Recording recording;
|
||||
|
@ -592,4 +688,19 @@ public class RemoteRecorder implements Recorder {
|
|||
public int getModelCount() {
|
||||
return (int) models.stream().filter(m -> !m.isMarkedForLaterRecording()).count();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ModelGroup> getModelGroups() throws InvalidKeyException, NoSuchAlgorithmException, IOException {
|
||||
return modelGroups;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveModelGroup(ModelGroup group) throws InvalidKeyException, NoSuchAlgorithmException, IOException {
|
||||
sendRequest("saveModelGroup", group);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteModelGroup(ModelGroup group) throws InvalidKeyException, NoSuchAlgorithmException, IOException {
|
||||
sendRequest("deleteModelGroup", group);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,105 +3,167 @@ package ctbrec.recorder.postprocessing;
|
|||
import static ctbrec.StringUtil.*;
|
||||
import static java.util.Optional.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.ModelGroup;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.StringUtil;
|
||||
import ctbrec.sites.Site;
|
||||
|
||||
public abstract class AbstractPlaceholderAwarePostProcessor extends AbstractPostProcessor {
|
||||
|
||||
public static final String[] PLACE_HOLDERS = {
|
||||
"${modelName}",
|
||||
"${modelDisplayName}",
|
||||
"${modelSanitizedName}",
|
||||
"${siteName}",
|
||||
"${siteSanitizedName}",
|
||||
"${utcDateTime}",
|
||||
"${localDateTime}",
|
||||
"${epochSecond}",
|
||||
"${fileSuffix}",
|
||||
"${modelNotes}",
|
||||
"${recordingNotes}",
|
||||
"${recordingsDir}",
|
||||
"${absolutePath}",
|
||||
"${absoluteParentPath}"
|
||||
};
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractPlaceholderAwarePostProcessor.class);
|
||||
|
||||
public String fillInPlaceHolders(String input, Recording rec, Config config) {
|
||||
// @formatter:off
|
||||
String output = input
|
||||
.replace("${modelName}", ofNullable(rec.getModel().getName()).orElse("modelName"))
|
||||
.replace("${modelDisplayName}", ofNullable(rec.getModel().getDisplayName()).orElse("displayName"))
|
||||
.replace("${modelSanitizedName}", ofNullable(rec.getModel().getSanitizedNamed()).orElse("sanitizedName"))
|
||||
.replace("${siteName}", ofNullable(rec.getModel().getSite()).map(Site::getName).orElse("site"))
|
||||
.replace("${siteSanitizedName}", getSanitizedSiteName(rec))
|
||||
.replace("${fileSuffix}", getFileSuffix(rec))
|
||||
.replace("${epochSecond}", Long.toString(rec.getStartDate().getEpochSecond()))
|
||||
.replace("${modelNotes}", sanitize(config.getModelNotes(rec.getModel())))
|
||||
.replace("${recordingNotes}", getSanitizedRecordingNotes(rec))
|
||||
.replace("${recordingsDir}", config.getSettings().recordingsDir)
|
||||
.replace("${absolutePath}", rec.getPostProcessedFile().getAbsolutePath())
|
||||
.replace("${absoluteParentPath}", rec.getPostProcessedFile().getParentFile().getAbsolutePath())
|
||||
;
|
||||
public String fillInPlaceHolders(String input, PostProcessingContext ctx) {
|
||||
Recording rec = ctx.getRecording();
|
||||
Config config = ctx.getConfig();
|
||||
Optional<ModelGroup> modelGroup;
|
||||
try {
|
||||
modelGroup = ctx.getRecorder().getModelGroup(rec.getModel());
|
||||
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
|
||||
LOG.error("Couldn't get model group for {}", rec.getModel(), e);
|
||||
return input;
|
||||
}
|
||||
|
||||
output = replaceUtcDateTime(rec, output);
|
||||
output = replaceLocalDateTime(rec, output);
|
||||
Map<String, Function<String, Optional<String>>> placeholderValueSuppliers = new HashMap<>();
|
||||
placeholderValueSuppliers.put("modelName", r -> ofNullable(rec.getModel().getName()));
|
||||
placeholderValueSuppliers.put("modelDisplayName", r -> ofNullable(rec.getModel().getDisplayName()));
|
||||
placeholderValueSuppliers.put("modelSanitizedName", r -> getSanitizedName(rec.getModel()));
|
||||
placeholderValueSuppliers.put("siteName", r -> ofNullable(rec.getModel().getSite()).map(Site::getName));
|
||||
placeholderValueSuppliers.put("siteSanitizedName", r -> getSanitizedSiteName(rec));
|
||||
placeholderValueSuppliers.put("fileSuffix", r -> getFileSuffix(rec));
|
||||
placeholderValueSuppliers.put("epochSecond", r -> ofNullable(rec.getStartDate()).map(Instant::getEpochSecond).map(l -> Long.toString(l))); // NOSONAR
|
||||
placeholderValueSuppliers.put("modelNotes", r -> getSanitizedModelNotes(config, rec.getModel()));
|
||||
placeholderValueSuppliers.put("recordingNotes", r -> getSanitizedRecordingNotes(rec));
|
||||
placeholderValueSuppliers.put("recordingsDir", r -> Optional.of(config.getSettings().recordingsDir));
|
||||
placeholderValueSuppliers.put("absolutePath", r -> Optional.of(rec.getPostProcessedFile().getAbsolutePath()));
|
||||
placeholderValueSuppliers.put("absoluteParentPath", r -> Optional.of(rec.getPostProcessedFile().getParentFile().getAbsolutePath()));
|
||||
placeholderValueSuppliers.put("modelGroupName", r -> modelGroup.map(ModelGroup::getName));
|
||||
placeholderValueSuppliers.put("modelGroupId", r -> modelGroup.map(ModelGroup::getId).map(UUID::toString));
|
||||
placeholderValueSuppliers.put("utcDateTime", pattern -> replaceUtcDateTime(rec, pattern));
|
||||
placeholderValueSuppliers.put("localDateTime", pattern -> replaceLocalDateTime(rec, pattern));
|
||||
|
||||
String output = fillInPlaceHolders(input, placeholderValueSuppliers);
|
||||
return output;
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private String replaceUtcDateTime(Recording rec, String filename) {
|
||||
return replaceDateTime(rec, filename, "utcDateTime", ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
private String replaceLocalDateTime(Recording rec, String filename) {
|
||||
return replaceDateTime(rec, filename, "localDateTime", ZoneId.systemDefault());
|
||||
}
|
||||
|
||||
private String replaceDateTime(Recording rec, String filename, String placeHolder, ZoneId zone) {
|
||||
String pattern = "yyyy-MM-dd_HH-mm-ss";
|
||||
Pattern regex = Pattern.compile("\\$\\{" + placeHolder + "(?:\\((.*?)\\))?\\}");
|
||||
Matcher m = regex.matcher(filename);
|
||||
while (m.find()) {
|
||||
String p = m.group(1);
|
||||
if (p != null) {
|
||||
pattern = p;
|
||||
}
|
||||
String formattedDate = getDateTime(rec, pattern, zone);
|
||||
filename = m.replaceFirst(formattedDate);
|
||||
m = regex.matcher(filename);
|
||||
}
|
||||
return filename;
|
||||
}
|
||||
|
||||
private String getDateTime(Recording rec, String pattern, ZoneId zone) {
|
||||
return DateTimeFormatter.ofPattern(pattern)
|
||||
.withLocale(Locale.getDefault())
|
||||
.withZone(zone)
|
||||
.format(rec.getStartDate());
|
||||
}
|
||||
|
||||
private CharSequence getFileSuffix(Recording rec) {
|
||||
if(rec.isSingleFile()) {
|
||||
String filename = rec.getPostProcessedFile().getName();
|
||||
return filename.substring(filename.lastIndexOf('.') + 1);
|
||||
private Optional<String> getSanitizedName(Model model) {
|
||||
String name = model.getSanitizedNamed();
|
||||
if (StringUtil.isBlank(name)) {
|
||||
return Optional.empty();
|
||||
} else {
|
||||
return "";
|
||||
return Optional.of(name);
|
||||
}
|
||||
}
|
||||
|
||||
private CharSequence getSanitizedSiteName(Recording rec) {
|
||||
return sanitize(ofNullable(rec.getModel().getSite()).map(Site::getName).orElse(""));
|
||||
private String fillInPlaceHolders(String input, Map<String, Function<String, Optional<String>>> placeholderValueSuppliers) {
|
||||
boolean somethingReplaced = false;
|
||||
do {
|
||||
somethingReplaced = false;
|
||||
int end = input.indexOf("}");
|
||||
if (end > 0) {
|
||||
int start = input.substring(0, end).lastIndexOf("${");
|
||||
if (start >= 0) {
|
||||
String placeholder = input.substring(start, end + 1);
|
||||
String placeholderName = placeholder.substring(2, placeholder.length() - 1);
|
||||
String defaultValue = null;
|
||||
String expression = null;
|
||||
int questionMark = placeholder.indexOf('?');
|
||||
if (questionMark > 0) {
|
||||
placeholderName = placeholder.substring(2, questionMark);
|
||||
defaultValue = placeholder.substring(questionMark + 1, placeholder.length() - 1);
|
||||
}
|
||||
int bracket = placeholder.indexOf('(');
|
||||
if (bracket > 0) {
|
||||
placeholderName = placeholder.substring(2, bracket);
|
||||
expression = placeholder.substring(bracket + 1, placeholder.indexOf(')', bracket));
|
||||
}
|
||||
|
||||
final String name = placeholderName;
|
||||
Optional<String> optionalValue = placeholderValueSuppliers.getOrDefault(name, r -> Optional.of(name)).apply(expression);
|
||||
String value = optionalValue.orElse(defaultValue);
|
||||
StringBuilder sb = new StringBuilder(input);
|
||||
String output = sb.replace(start, end+1, value).toString();
|
||||
somethingReplaced = !Objects.equals(input, output);
|
||||
input = output;
|
||||
}
|
||||
}
|
||||
} while (somethingReplaced);
|
||||
return input;
|
||||
}
|
||||
|
||||
private CharSequence getSanitizedRecordingNotes(Recording rec) {
|
||||
return sanitize(ofNullable(rec.getNote()).orElse(""));
|
||||
private Optional<String> replaceUtcDateTime(Recording rec, String pattern) {
|
||||
return replaceDateTime(rec, pattern, ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
private Optional<String> replaceLocalDateTime(Recording rec, String filename) {
|
||||
return replaceDateTime(rec, filename, ZoneId.systemDefault());
|
||||
}
|
||||
|
||||
private Optional<String> replaceDateTime(Recording rec, String pattern, ZoneId zone) {
|
||||
pattern = pattern != null ? pattern : "yyyy-MM-dd_HH-mm-ss";
|
||||
return getDateTime(rec, pattern, zone);
|
||||
}
|
||||
|
||||
private Optional<String> getDateTime(Recording rec, String pattern, ZoneId zone) {
|
||||
return Optional.ofNullable(rec.getStartDate()) //
|
||||
.map(DateTimeFormatter.ofPattern(pattern) //
|
||||
.withLocale(Locale.getDefault()) //
|
||||
.withZone(zone) //
|
||||
::format);
|
||||
}
|
||||
|
||||
private Optional<String> getFileSuffix(Recording rec) {
|
||||
if (rec.isSingleFile()) {
|
||||
String filename = rec.getPostProcessedFile().getName();
|
||||
return Optional.of(filename.substring(filename.lastIndexOf('.') + 1));
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<String> getSanitizedSiteName(Recording rec) {
|
||||
Optional<String> name = ofNullable(rec.getModel().getSite()).map(Site::getName);
|
||||
if (name.isPresent()) {
|
||||
return Optional.of(sanitize(name.get()));
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<String> getSanitizedRecordingNotes(Recording rec) {
|
||||
Optional<String> notes = ofNullable(rec.getNote());
|
||||
if (notes.isPresent()) {
|
||||
return Optional.of(sanitize(notes.get()));
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<String> getSanitizedModelNotes(Config config, Model m) {
|
||||
Optional<String> notes = ofNullable(config.getModelNotes(m));
|
||||
if (notes.isPresent()) {
|
||||
return Optional.of(sanitize(notes.get()));
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,9 +8,7 @@ import org.apache.commons.io.FileUtils;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.recorder.RecordingManager;
|
||||
|
||||
public class Copy extends AbstractPostProcessor {
|
||||
|
||||
|
@ -22,7 +20,8 @@ public class Copy extends AbstractPostProcessor {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException {
|
||||
public boolean postprocess(PostProcessingContext ctx) throws IOException, InterruptedException {
|
||||
Recording rec = ctx.getRecording();
|
||||
File orig = rec.getPostProcessedFile();
|
||||
String copyFilename = getFilenameForCopy(orig);
|
||||
File copy = new File(orig.getParentFile(), copyFilename);
|
||||
|
|
|
@ -22,7 +22,6 @@ import ctbrec.OS;
|
|||
import ctbrec.Recording;
|
||||
import ctbrec.io.IoUtils;
|
||||
import ctbrec.recorder.FFmpeg;
|
||||
import ctbrec.recorder.RecordingManager;
|
||||
import ctbrec.recorder.download.ProcessExitedUncleanException;
|
||||
|
||||
@ThreadSafe
|
||||
|
@ -45,7 +44,9 @@ public class CreateContactSheet extends AbstractPlaceholderAwarePostProcessor {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException {
|
||||
public boolean postprocess(PostProcessingContext ctx) throws IOException, InterruptedException {
|
||||
Recording rec = ctx.getRecording();
|
||||
Config config = ctx.getConfig();
|
||||
int totalWidth = Integer.parseInt(getConfig().getOrDefault(TOTAL_SIZE, "1920"));
|
||||
int padding = Integer.parseInt(getConfig().getOrDefault(PADDING, "4"));
|
||||
int cols = Integer.parseInt(getConfig().getOrDefault(COLS, "8"));
|
||||
|
@ -65,7 +66,7 @@ public class CreateContactSheet extends AbstractPlaceholderAwarePostProcessor {
|
|||
color},
|
||||
new StringBuffer(), null).toString();
|
||||
File executionDir = rec.getPostProcessedFile().isDirectory() ? rec.getPostProcessedFile() : rec.getPostProcessedFile().getParentFile();
|
||||
File output = new File(executionDir, fillInPlaceHolders(filename, rec, config));
|
||||
File output = new File(executionDir, fillInPlaceHolders(filename, ctx));
|
||||
|
||||
String[] args = {
|
||||
"-y",
|
||||
|
|
|
@ -2,10 +2,7 @@ package ctbrec.recorder.postprocessing;
|
|||
|
||||
import java.io.IOException;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.NotImplementedExcetion;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.recorder.RecordingManager;
|
||||
|
||||
public class CreateTimelineThumbs extends AbstractPlaceholderAwarePostProcessor {
|
||||
|
||||
|
@ -15,7 +12,7 @@ public class CreateTimelineThumbs extends AbstractPlaceholderAwarePostProcessor
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException {
|
||||
public boolean postprocess(PostProcessingContext ctx) throws IOException, InterruptedException {
|
||||
// create 1 thumb every second with a width of 360 pixels and save it as jpeg with a 5-digit sequence number
|
||||
// ffmpeg -i <file> -vf 'fps=1,scale=360:-1' thumbs/out%05d.jpg
|
||||
throw new NotImplementedExcetion();
|
||||
|
|
|
@ -5,9 +5,7 @@ import static ctbrec.io.IoUtils.*;
|
|||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.recorder.RecordingManager;
|
||||
|
||||
public class DeleteOriginal extends AbstractPostProcessor {
|
||||
|
||||
|
@ -17,7 +15,8 @@ public class DeleteOriginal extends AbstractPostProcessor {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException {
|
||||
public boolean postprocess(PostProcessingContext ctx) throws IOException, InterruptedException {
|
||||
Recording rec = ctx.getRecording();
|
||||
if (rec.getAbsoluteFile().isFile()) {
|
||||
Files.deleteIfExists(rec.getAbsoluteFile().toPath());
|
||||
deleteEmptyParents(rec.getAbsoluteFile().getParentFile());
|
||||
|
|
|
@ -6,7 +6,6 @@ import java.time.Duration;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.recorder.RecordingManager;
|
||||
|
||||
|
@ -21,7 +20,9 @@ public class DeleteTooShort extends AbstractPostProcessor {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException {
|
||||
public boolean postprocess(PostProcessingContext ctx) throws IOException {
|
||||
Recording rec = ctx.getRecording();
|
||||
RecordingManager recordingManager = ctx.getRecordingManager();
|
||||
Duration minimumLengthInSeconds = Duration.ofSeconds(Integer.parseInt(getConfig().getOrDefault(MIN_LEN_IN_SECS, "0")));
|
||||
if (minimumLengthInSeconds.getSeconds() > 0) {
|
||||
Duration recordingLength = rec.getLength();
|
||||
|
|
|
@ -10,9 +10,7 @@ import org.apache.commons.io.FileUtils;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.recorder.RecordingManager;
|
||||
|
||||
public class Move extends AbstractPlaceholderAwarePostProcessor {
|
||||
|
||||
|
@ -26,9 +24,10 @@ public class Move extends AbstractPlaceholderAwarePostProcessor {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException {
|
||||
public boolean postprocess(PostProcessingContext ctx) throws IOException {
|
||||
Recording rec = ctx.getRecording();
|
||||
String pathTemplate = getConfig().getOrDefault(PATH_TEMPLATE, DEFAULT);
|
||||
String path = fillInPlaceHolders(pathTemplate, rec, config);
|
||||
String path = fillInPlaceHolders(pathTemplate, ctx);
|
||||
File src = rec.getPostProcessedFile();
|
||||
boolean isFile = src.isFile();
|
||||
File target = new File(path, src.getName());
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
package ctbrec.recorder.postprocessing;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.recorder.Recorder;
|
||||
import ctbrec.recorder.RecordingManager;
|
||||
|
||||
public class PostProcessingContext {
|
||||
|
||||
private Recorder recorder;
|
||||
private Recording recording;
|
||||
private RecordingManager recordingManager;
|
||||
private Config config;
|
||||
|
||||
public Recorder getRecorder() {
|
||||
return recorder;
|
||||
}
|
||||
|
||||
public void setRecorder(Recorder recorder) {
|
||||
this.recorder = recorder;
|
||||
}
|
||||
|
||||
public Recording getRecording() {
|
||||
return recording;
|
||||
}
|
||||
|
||||
public void setRecording(Recording recording) {
|
||||
this.recording = recording;
|
||||
}
|
||||
|
||||
public RecordingManager getRecordingManager() {
|
||||
return recordingManager;
|
||||
}
|
||||
|
||||
public void setRecordingManager(RecordingManager recordingManager) {
|
||||
this.recordingManager = recordingManager;
|
||||
}
|
||||
|
||||
public Config getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
public void setConfig(Config config) {
|
||||
this.config = config;
|
||||
}
|
||||
}
|
|
@ -4,23 +4,17 @@ import java.io.IOException;
|
|||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.recorder.RecordingManager;
|
||||
|
||||
public interface PostProcessor extends Serializable {
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* Runs the post-processing code on the given recording
|
||||
* @param rec the recording to post-process
|
||||
* @param recordingManager
|
||||
* @param config
|
||||
* @param ctx {@link PostProcessingContext}, which allows access to post-processing related objects
|
||||
* @return false to stop futher post-processing, true to continue
|
||||
* @throws IOException
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
boolean postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException;
|
||||
boolean postprocess(PostProcessingContext ctx) throws IOException, InterruptedException;
|
||||
|
||||
Map<String, String> getConfig();
|
||||
void setConfig(Map<String, String> conf);
|
||||
|
|
|
@ -2,10 +2,6 @@ package ctbrec.recorder.postprocessing;
|
|||
|
||||
import java.io.IOException;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.recorder.RecordingManager;
|
||||
|
||||
public class RemoveKeepFile extends AbstractPostProcessor {
|
||||
|
||||
@Override
|
||||
|
@ -14,9 +10,9 @@ public class RemoveKeepFile extends AbstractPostProcessor {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException {
|
||||
recordingManager.remove(rec);
|
||||
rec.setMetaDataFile(null);
|
||||
public boolean postprocess(PostProcessingContext ctx) throws IOException, InterruptedException {
|
||||
ctx.getRecordingManager().remove(ctx.getRecording());
|
||||
ctx.getRecording().setMetaDataFile(null);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,12 +9,10 @@ import java.util.Objects;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.OS;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.io.IoUtils;
|
||||
import ctbrec.recorder.FFmpeg;
|
||||
import ctbrec.recorder.RecordingManager;
|
||||
|
||||
public class Remux extends AbstractPostProcessor {
|
||||
|
||||
|
@ -29,7 +27,8 @@ public class Remux extends AbstractPostProcessor {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException {
|
||||
public boolean postprocess(PostProcessingContext ctx) throws IOException, InterruptedException {
|
||||
Recording rec = ctx.getRecording();
|
||||
final File inputFile;
|
||||
if (rec.getPostProcessedFile().isDirectory()) {
|
||||
inputFile = new File(rec.getPostProcessedFile(), "playlist.m3u8");
|
||||
|
@ -44,7 +43,7 @@ public class Remux extends AbstractPostProcessor {
|
|||
|
||||
File ffmpegLog = new File(System.getProperty("java.io.tmpdir"), "remux_" + rec.getId() + ".log");
|
||||
FFmpeg ffmpeg = new FFmpeg.Builder()
|
||||
.logOutput(config.getSettings().logFFmpegOutput)
|
||||
.logOutput(ctx.getConfig().getSettings().logFFmpegOutput)
|
||||
.logFile(ffmpegLog)
|
||||
.onExit(exitCode -> finalizeStep(exitCode, rec, inputFile, remuxedFile))
|
||||
.build();
|
||||
|
|
|
@ -7,9 +7,7 @@ import java.util.Objects;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.recorder.RecordingManager;
|
||||
|
||||
public class Rename extends AbstractPlaceholderAwarePostProcessor {
|
||||
|
||||
|
@ -24,10 +22,11 @@ public class Rename extends AbstractPlaceholderAwarePostProcessor {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException {
|
||||
public boolean postprocess(PostProcessingContext ctx) throws IOException {
|
||||
Recording rec = ctx.getRecording();
|
||||
String defaultTemplate = rec.isSingleFile() ? DEFAULT : DEFAULT_DIR;
|
||||
String filenameTemplate = getConfig().getOrDefault(FILE_NAME_TEMPLATE, defaultTemplate);
|
||||
String filename = fillInPlaceHolders(filenameTemplate, rec, config);
|
||||
String filename = fillInPlaceHolders(filenameTemplate, ctx);
|
||||
File src = rec.getPostProcessedFile();
|
||||
File target = new File(src.getParentFile(), filename);
|
||||
if (Objects.equals(src, target)) {
|
||||
|
|
|
@ -10,11 +10,9 @@ import java.util.List;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.OS;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.io.StreamRedirector;
|
||||
import ctbrec.recorder.RecordingManager;
|
||||
import ctbrec.recorder.download.ProcessExitedUncleanException;
|
||||
|
||||
public class Script extends AbstractPlaceholderAwarePostProcessor {
|
||||
|
@ -29,8 +27,9 @@ public class Script extends AbstractPlaceholderAwarePostProcessor {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException {
|
||||
List<String> cmdline = buildCommandLine(rec, config);
|
||||
public boolean postprocess(PostProcessingContext ctx) throws IOException, InterruptedException {
|
||||
Recording rec = ctx.getRecording();
|
||||
List<String> cmdline = buildCommandLine(ctx);
|
||||
Runtime rt = Runtime.getRuntime();
|
||||
String[] args = cmdline.toArray(new String[0]);
|
||||
if (LOG.isDebugEnabled()) {
|
||||
|
@ -50,12 +49,12 @@ public class Script extends AbstractPlaceholderAwarePostProcessor {
|
|||
return true;
|
||||
}
|
||||
|
||||
private List<String> buildCommandLine(Recording rec, Config config) throws IOException {
|
||||
private List<String> buildCommandLine(PostProcessingContext ctx) {
|
||||
String script = getConfig().getOrDefault(SCRIPT_EXECUTABLE, "somescript");
|
||||
String params = getConfig().getOrDefault(SCRIPT_PARAMS, "${absolutePath}");
|
||||
List<String> cmdline = new ArrayList<>();
|
||||
cmdline.add(script);
|
||||
String replacedParams = fillInPlaceHolders(params, rec, config);
|
||||
String replacedParams = fillInPlaceHolders(params, ctx);
|
||||
Arrays.stream(replacedParams.split(" ")).forEach(cmdline::add);
|
||||
return cmdline;
|
||||
}
|
||||
|
|
|
@ -2,10 +2,7 @@ package ctbrec.recorder.postprocessing;
|
|||
|
||||
import java.io.IOException;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.NotImplementedExcetion;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.recorder.RecordingManager;
|
||||
|
||||
public class Webhook extends AbstractPlaceholderAwarePostProcessor {
|
||||
|
||||
|
@ -21,7 +18,7 @@ public class Webhook extends AbstractPlaceholderAwarePostProcessor {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException {
|
||||
public boolean postprocess(PostProcessingContext ctx) throws IOException, InterruptedException {
|
||||
throw new NotImplementedExcetion();
|
||||
}
|
||||
|
||||
|
|
|
@ -37,19 +37,19 @@ public class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest {
|
|||
@Test
|
||||
public void testModelNameReplacement() {
|
||||
String input = "asdf_${modelName}_asdf";
|
||||
assertEquals("asdf_Mockita Boobilicious_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
|
||||
assertEquals("asdf_Mockita Boobilicious_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
|
||||
input = "asdf_${modelDisplayName}_asdf";
|
||||
assertEquals("asdf_Mockita Boobilicious_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
|
||||
assertEquals("asdf_Mockita Boobilicious_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
|
||||
input = "asdf_${modelSanitizedName}_asdf";
|
||||
assertEquals("asdf_Mockita_Boobilicious_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
|
||||
assertEquals("asdf_Mockita_Boobilicious_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSiteNameReplacement() {
|
||||
String input = "asdf_${siteName}_asdf";
|
||||
assertEquals("asdf_Chaturbate_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
|
||||
assertEquals("asdf_Chaturbate_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
|
||||
input = "asdf_${siteSanitizedName}_asdf";
|
||||
assertEquals("asdf_Chaturbate_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
|
||||
assertEquals("asdf_Chaturbate_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -60,7 +60,7 @@ public class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest {
|
|||
.withZone(ZoneOffset.UTC)
|
||||
.format(rec.getStartDate());
|
||||
String input = "asdf_${utcDateTime}_asdf";
|
||||
assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
|
||||
assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
|
||||
|
||||
// with user defined pattern
|
||||
date = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")
|
||||
|
@ -68,7 +68,7 @@ public class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest {
|
|||
.withZone(ZoneOffset.UTC)
|
||||
.format(rec.getStartDate());
|
||||
input = "asdf_${utcDateTime(yyyyMMdd-HHmmss)}_asdf";
|
||||
assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
|
||||
assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
|
||||
|
||||
// multiple occurences with user defined patterns
|
||||
date = DateTimeFormatter.ofPattern("yyyy-MM-dd/yyyy")
|
||||
|
@ -76,7 +76,7 @@ public class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest {
|
|||
.withZone(ZoneOffset.UTC)
|
||||
.format(rec.getStartDate());
|
||||
input = "asdf_${utcDateTime(yyyy)}-${utcDateTime(MM)}-${utcDateTime(dd)}/${utcDateTime(yyyy)}_asdf";
|
||||
assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
|
||||
assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -86,50 +86,68 @@ public class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest {
|
|||
.withZone(ZoneId.systemDefault())
|
||||
.format(rec.getStartDate());
|
||||
String input = "asdf_${localDateTime}_asdf";
|
||||
assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
|
||||
assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
|
||||
|
||||
date = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")
|
||||
.withLocale(Locale.US)
|
||||
.withZone(ZoneId.systemDefault())
|
||||
.format(rec.getStartDate());
|
||||
input = "asdf_${localDateTime(yyyyMMdd-HHmmss)}_asdf";
|
||||
assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
|
||||
assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEpochReplacement() {
|
||||
long epoch = now.toEpochMilli() / 1000;
|
||||
String input = "asdf_${epochSecond}_asdf";
|
||||
assertEquals("asdf_" + epoch + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
|
||||
assertEquals("asdf_" + epoch + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFileSuffixReplacement() {
|
||||
String input = "asdf_${fileSuffix}_asdf";
|
||||
assertEquals("asdf_ts_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
|
||||
assertEquals("asdf_ts_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecordingsDirReplacement() {
|
||||
String input = "asdf_${recordingsDir}_asdf";
|
||||
assertEquals("asdf_" + recDir.toString() + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
|
||||
assertEquals("asdf_" + recDir.toString() + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAbsolutePathReplacement() {
|
||||
String input = "asdf_${absolutePath}_asdf";
|
||||
assertEquals("asdf_" + postProcessed.getAbsolutePath().toString() + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
|
||||
assertEquals("asdf_" + postProcessed.getAbsolutePath().toString() + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAbsoluteParentPathReplacement() {
|
||||
String input = "asdf_${absoluteParentPath}_asdf";
|
||||
assertEquals("asdf_" + postProcessed.getParentFile().toString() + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
|
||||
assertEquals("asdf_" + postProcessed.getParentFile().toString() + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testModelNotesReplacement() {
|
||||
String input = "asdf_${modelNotes}_asdf";
|
||||
assertEquals("asdf_tag,_foo,_bar_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
|
||||
assertEquals("asdf_tag,_foo,_bar_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPlaceholderDefaultValues() throws IOException {
|
||||
String input = "asdf_${modelGroupName?${modelSanitizedName?anonymous}}_asdf";
|
||||
PostProcessingContext ctx = createPostProcessingContext(rec, null, config);
|
||||
ctx.getRecording().getModel().setName(null);
|
||||
assertEquals("asdf_anonymous_asdf", placeHolderAwarePp.fillInPlaceHolders(input, ctx));
|
||||
|
||||
input = "asdf_${modelGroupName?${utcDateTime(yyyy)?anonymous}}_asdf";
|
||||
assertEquals("asdf_2021_asdf", placeHolderAwarePp.fillInPlaceHolders(input, ctx));
|
||||
|
||||
ctx.getRecording().setStartDate(null);
|
||||
input = "asdf_${modelGroupName?${utcDateTime(yyyy)?anonymous}}_asdf";
|
||||
assertEquals("asdf_anonymous_asdf", placeHolderAwarePp.fillInPlaceHolders(input, ctx));
|
||||
|
||||
input = "asdf_${modelGroupName?${utcDateTime?anonymous}}_asdf";
|
||||
assertEquals("asdf_anonymous_asdf", placeHolderAwarePp.fillInPlaceHolders(input, ctx));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import org.mockito.MockedStatic;
|
|||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.Settings;
|
||||
import ctbrec.recorder.RecordingManager;
|
||||
import ctbrec.sites.Site;
|
||||
|
@ -81,4 +82,12 @@ public abstract class AbstractPpTest {
|
|||
settings.recordingsDir = recDir.toString();
|
||||
return settings;
|
||||
}
|
||||
|
||||
PostProcessingContext createPostProcessingContext(Recording rec, RecordingManager recordingManager, Config config) {
|
||||
PostProcessingContext ctx = new PostProcessingContext();
|
||||
ctx.setConfig(config);
|
||||
ctx.setRecording(rec);
|
||||
ctx.setRecordingManager(recordingManager);
|
||||
return ctx;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ public class CopyTest extends AbstractPpTest {
|
|||
rec.setStartDate(now);
|
||||
rec.setSingleFile(false);
|
||||
Copy pp = new Copy();
|
||||
pp.postprocess(rec, recordingManager, config);
|
||||
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
|
||||
|
||||
assertNotEquals(rec.getAbsoluteFile(), rec.getPostProcessedFile());
|
||||
assertTrue(original.exists());
|
||||
|
@ -36,7 +36,7 @@ public class CopyTest extends AbstractPpTest {
|
|||
rec.setStartDate(now);
|
||||
rec.setSingleFile(false);
|
||||
Copy pp = new Copy();
|
||||
pp.postprocess(rec, recordingManager, config);
|
||||
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
|
||||
|
||||
assertNotEquals(rec.getAbsoluteFile(), rec.getPostProcessedFile());
|
||||
assertTrue(originalDir.exists());
|
||||
|
|
|
@ -23,7 +23,7 @@ public class DeleteOriginalTest extends AbstractPpTest {
|
|||
|
||||
Config config = mockConfig();
|
||||
DeleteOriginal pp = new DeleteOriginal();
|
||||
pp.postprocess(rec, null, config);
|
||||
pp.postprocess(createPostProcessingContext(rec, null, config));
|
||||
|
||||
assertEquals(postProcessed, rec.getAbsoluteFile());
|
||||
assertTrue(rec.getAbsoluteFile().exists());
|
||||
|
@ -42,7 +42,7 @@ public class DeleteOriginalTest extends AbstractPpTest {
|
|||
Config config = mockConfig();
|
||||
Files.createDirectories(postProcessedDir.toPath());
|
||||
DeleteOriginal pp = new DeleteOriginal();
|
||||
pp.postprocess(rec, null, config);
|
||||
pp.postprocess(createPostProcessingContext(rec, null, config));
|
||||
|
||||
assertEquals(postProcessedDir, rec.getAbsoluteFile());
|
||||
assertTrue(rec.getAbsoluteFile().exists());
|
||||
|
|
|
@ -42,7 +42,7 @@ public class DeleteTooShortTest extends AbstractPpTest {
|
|||
|
||||
DeleteTooShort pp = new DeleteTooShort();
|
||||
pp.getConfig().put(DeleteTooShort.MIN_LEN_IN_SECS, "10");
|
||||
pp.postprocess(rec, recordingManager, config);
|
||||
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
|
||||
|
||||
assertFalse(rec.getAbsoluteFile().exists());
|
||||
assertFalse(original.exists());
|
||||
|
@ -66,7 +66,7 @@ public class DeleteTooShortTest extends AbstractPpTest {
|
|||
|
||||
DeleteTooShort pp = new DeleteTooShort();
|
||||
pp.getConfig().put(DeleteTooShort.MIN_LEN_IN_SECS, "0");
|
||||
pp.postprocess(rec, recordingManager, config);
|
||||
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
|
||||
|
||||
assertTrue(rec.getAbsoluteFile().exists());
|
||||
assertTrue(original.exists());
|
||||
|
@ -83,7 +83,7 @@ public class DeleteTooShortTest extends AbstractPpTest {
|
|||
|
||||
DeleteTooShort pp = new DeleteTooShort();
|
||||
pp.getConfig().put(DeleteTooShort.MIN_LEN_IN_SECS, "1");
|
||||
pp.postprocess(rec, recordingManager, config);
|
||||
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
|
||||
|
||||
assertTrue(rec.getAbsoluteFile().exists());
|
||||
assertTrue(original.exists());
|
||||
|
|
|
@ -28,7 +28,7 @@ public class MoveDirectoryTest extends AbstractPpTest {
|
|||
rec.setSingleFile(false);
|
||||
Move pp = new Move();
|
||||
pp.getConfig().put(Move.PATH_TEMPLATE, new File(baseDir.toFile(), Move.DEFAULT).getAbsolutePath());
|
||||
pp.postprocess(rec, recordingManager, config);
|
||||
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
|
||||
|
||||
Matcher m = Pattern.compile(baseDir.toString() + "/Mockita_Boobilicious/\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}/original").matcher(rec.getAbsoluteFile().getCanonicalPath());
|
||||
assertTrue(m.matches());
|
||||
|
@ -50,6 +50,6 @@ public class MoveDirectoryTest extends AbstractPpTest {
|
|||
Move pp = new Move();
|
||||
Config config = mockConfig();
|
||||
pp.getConfig().put(Move.PATH_TEMPLATE, new File(baseDir.toFile(), Move.DEFAULT).getAbsolutePath());
|
||||
pp.postprocess(rec, recordingManager, config);
|
||||
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ public class MoveSingleFileTest extends AbstractPpTest {
|
|||
|
||||
Move pp = new Move();
|
||||
pp.getConfig().put(Move.PATH_TEMPLATE, new File(baseDir.toFile(), Move.DEFAULT).getAbsolutePath());
|
||||
pp.postprocess(rec, recordingManager, config);
|
||||
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
|
||||
|
||||
Matcher m = Pattern.compile(baseDir.toFile() + "/Mockita_Boobilicious/\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}/original\\.ts").matcher(rec.getAbsoluteFile().toString());
|
||||
assertTrue(m.matches());
|
||||
|
@ -48,7 +48,7 @@ public class MoveSingleFileTest extends AbstractPpTest {
|
|||
Move pp = new Move();
|
||||
Config config = mockConfig();
|
||||
pp.getConfig().put(Move.PATH_TEMPLATE, original.getParentFile().getCanonicalPath());
|
||||
pp.postprocess(rec, recordingManager, config);
|
||||
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -63,7 +63,7 @@ public class MoveSingleFileTest extends AbstractPpTest {
|
|||
Move pp = new Move();
|
||||
Config config = mockConfig();
|
||||
pp.getConfig().put(Move.PATH_TEMPLATE, new File(baseDir.toFile(), Move.DEFAULT).getAbsolutePath());
|
||||
pp.postprocess(rec, recordingManager, config);
|
||||
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -25,9 +25,9 @@ public class RemoveKeepFileTest extends AbstractPpTest {
|
|||
Config config = mockConfig();
|
||||
RecordingManager rm = new RecordingManager(config, Collections.emptyList());
|
||||
rm.add(rec);
|
||||
assertTrue(rm.getAll().size() == 1);
|
||||
assertEquals(1, rm.getAll().size());
|
||||
RemoveKeepFile pp = new RemoveKeepFile();
|
||||
pp.postprocess(rec, rm, config);
|
||||
pp.postprocess(createPostProcessingContext(rec, rm, config));
|
||||
|
||||
assertTrue(rec.getAbsoluteFile().exists());
|
||||
assertTrue(rec.getPostProcessedFile().exists());
|
||||
|
|
|
@ -26,7 +26,7 @@ public class RenameDirectoryTest extends AbstractPpTest {
|
|||
rec.setStartDate(now);
|
||||
rec.setSingleFile(false);
|
||||
Rename pp = new Rename();
|
||||
pp.postprocess(rec, recordingManager, config);
|
||||
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
|
||||
|
||||
Matcher m = Pattern.compile("Mockita_Boobilicious_\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}").matcher(rec.getAbsoluteFile().getName());
|
||||
assertTrue(m.matches());
|
||||
|
@ -47,6 +47,6 @@ public class RenameDirectoryTest extends AbstractPpTest {
|
|||
|
||||
Files.createDirectories(postProcessedDir.toPath());
|
||||
Rename pp = new Rename();
|
||||
pp.postprocess(rec, recordingManager, config);
|
||||
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ public class RenameSingleFileTest extends AbstractPpTest {
|
|||
rec.setStartDate(now);
|
||||
rec.setSingleFile(true);
|
||||
Rename pp = new Rename();
|
||||
pp.postprocess(rec, recordingManager, config);
|
||||
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
|
||||
|
||||
Matcher m = Pattern.compile("Mockita_Boobilicious_\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}\\.ts").matcher(rec.getAbsoluteFile().getName());
|
||||
assertTrue(m.matches());
|
||||
|
@ -45,7 +45,7 @@ public class RenameSingleFileTest extends AbstractPpTest {
|
|||
doThrow(new RuntimeException("Unexpected call of setAbsoluteFile")).when(rec).setAbsoluteFile(any());
|
||||
Rename pp = new Rename();
|
||||
pp.getConfig().put(Rename.FILE_NAME_TEMPLATE, original.getName());
|
||||
pp.postprocess(rec, recordingManager, config);
|
||||
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -59,7 +59,7 @@ public class RenameSingleFileTest extends AbstractPpTest {
|
|||
when(rec.getStartDate()).thenReturn(now);
|
||||
doThrow(new RuntimeException("Unexpected call of setAbsoluteFile")).when(rec).setAbsoluteFile(any());
|
||||
Rename pp = new Rename();
|
||||
pp.postprocess(rec, recordingManager, config);
|
||||
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -10,6 +10,8 @@ import java.time.Instant;
|
|||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
@ -25,11 +27,13 @@ import com.squareup.moshi.Moshi;
|
|||
import ctbrec.Config;
|
||||
import ctbrec.GlobalThreadPool;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.ModelGroup;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.io.BandwidthMeter;
|
||||
import ctbrec.io.FileJsonAdapter;
|
||||
import ctbrec.io.InstantJsonAdapter;
|
||||
import ctbrec.io.ModelJsonAdapter;
|
||||
import ctbrec.io.UuidJSonAdapter;
|
||||
import ctbrec.recorder.Recorder;
|
||||
import ctbrec.sites.Site;
|
||||
|
||||
|
@ -67,6 +71,7 @@ public class RecorderServlet extends AbstractCtbrecServlet {
|
|||
.add(Instant.class, new InstantJsonAdapter())
|
||||
.add(Model.class, new ModelJsonAdapter(sites))
|
||||
.add(File.class, new FileJsonAdapter())
|
||||
.add(UUID.class, new UuidJSonAdapter())
|
||||
.build();
|
||||
JsonAdapter<Request> requestAdapter = moshi.adapter(Request.class);
|
||||
Request request = requestAdapter.fromJson(json);
|
||||
|
@ -234,6 +239,17 @@ public class RecorderServlet extends AbstractCtbrecServlet {
|
|||
response = "{\"status\": \"success\"}";
|
||||
resp.getWriter().write(response);
|
||||
break;
|
||||
case "saveModelGroup":
|
||||
recorder.saveModelGroup(request.modelGroup);
|
||||
sendModelGroups(resp, recorder.getModelGroups());
|
||||
break;
|
||||
case "deleteModelGroup":
|
||||
recorder.deleteModelGroup(request.modelGroup);
|
||||
sendModelGroups(resp, recorder.getModelGroups());
|
||||
break;
|
||||
case "listModelGroups":
|
||||
sendModelGroups(resp, recorder.getModelGroups());
|
||||
break;
|
||||
default:
|
||||
resp.setStatus(SC_BAD_REQUEST);
|
||||
response = "{\"status\": \"error\", \"msg\": \"Unknown action ["+request.action+"]\"}";
|
||||
|
@ -258,6 +274,13 @@ public class RecorderServlet extends AbstractCtbrecServlet {
|
|||
}
|
||||
}
|
||||
|
||||
private void sendModelGroups(HttpServletResponse resp, Set<ModelGroup> modelGroups) throws IOException {
|
||||
JSONObject jsonResponse = new JSONObject();
|
||||
jsonResponse.put("status", "success");
|
||||
jsonResponse.put("groups", modelGroups);
|
||||
resp.getWriter().write(jsonResponse.toString());
|
||||
}
|
||||
|
||||
private void startByUrl(Request request) throws InvalidKeyException, NoSuchAlgorithmException, IOException {
|
||||
String url = request.model.getUrl();
|
||||
for (Site site : sites) {
|
||||
|
@ -291,5 +314,6 @@ public class RecorderServlet extends AbstractCtbrecServlet {
|
|||
public String action;
|
||||
public Model model;
|
||||
public Recording recording;
|
||||
public ModelGroup modelGroup;
|
||||
}
|
||||
}
|
||||
|
|