forked from j62/ctbrec
1
0
Fork 0

Merge branch 'model-groups' into dev

This commit is contained in:
0xb00bface 2021-05-13 13:20:26 +02:00
commit f3c346a80e
68 changed files with 1555 additions and 352 deletions

View File

@ -63,12 +63,12 @@ import ctbrec.ui.settings.SettingsTab;
import ctbrec.ui.tabs.DonateTabFx; import ctbrec.ui.tabs.DonateTabFx;
import ctbrec.ui.tabs.HelpTab; import ctbrec.ui.tabs.HelpTab;
import ctbrec.ui.tabs.RecentlyWatchedTab; import ctbrec.ui.tabs.RecentlyWatchedTab;
import ctbrec.ui.tabs.RecordedTab;
import ctbrec.ui.tabs.RecordingsTab; import ctbrec.ui.tabs.RecordingsTab;
import ctbrec.ui.tabs.SiteTab; import ctbrec.ui.tabs.SiteTab;
import ctbrec.ui.tabs.TabSelectionListener; import ctbrec.ui.tabs.TabSelectionListener;
import ctbrec.ui.tabs.UpdateTab; import ctbrec.ui.tabs.UpdateTab;
import ctbrec.ui.tabs.logging.LoggingTab; import ctbrec.ui.tabs.logging.LoggingTab;
import ctbrec.ui.tabs.recorded.RecordedTab;
import javafx.application.Application; import javafx.application.Application;
import javafx.application.HostServices; import javafx.application.HostServices;
import javafx.application.Platform; import javafx.application.Platform;

View File

@ -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;
}
}

View File

@ -22,15 +22,13 @@ import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty; 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 * 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 { public class JavaFxModel implements Model {
private transient StringProperty onlineProperty = new SimpleStringProperty(); private transient BooleanProperty onlineProperty = new SimpleBooleanProperty();
private transient StringProperty recordingProperty = new SimpleStringProperty(); private transient BooleanProperty recordingProperty = new SimpleBooleanProperty();
private transient BooleanProperty pausedProperty = new SimpleBooleanProperty(); private transient BooleanProperty pausedProperty = new SimpleBooleanProperty();
private transient SimpleIntegerProperty priorityProperty = new SimpleIntegerProperty(); private transient SimpleIntegerProperty priorityProperty = new SimpleIntegerProperty();
private transient SimpleObjectProperty<Instant> lastSeenProperty = new SimpleObjectProperty<>(); private transient SimpleObjectProperty<Instant> lastSeenProperty = new SimpleObjectProperty<>();
@ -105,14 +103,22 @@ public class JavaFxModel implements Model {
return delegate.toString(); return delegate.toString();
} }
public StringProperty getOnlineProperty() { public BooleanProperty getOnlineProperty() {
return onlineProperty; return onlineProperty;
} }
public StringProperty getRecordingProperty() { public void setOnlineProperty(boolean online) {
this.onlineProperty.set(online);
}
public BooleanProperty getRecordingProperty() {
return recordingProperty; return recordingProperty;
} }
public void setRecordingProperty(boolean recording) {
this.recordingProperty.setValue(recording);
}
public BooleanProperty getPausedProperty() { public BooleanProperty getPausedProperty() {
return pausedProperty; return pausedProperty;
} }
@ -329,6 +335,4 @@ public class JavaFxModel implements Model {
public void setMarkedForLaterRecording(boolean marked) { public void setMarkedForLaterRecording(boolean marked) {
delegate.setMarkedForLaterRecording(marked); delegate.setMarkedForLaterRecording(marked);
} }
} }

View File

@ -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());
}
}
}

View File

@ -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;
}
}
}

View File

@ -4,16 +4,26 @@ import static javafx.scene.control.ButtonType.*;
import java.io.InputStream; import java.io.InputStream;
import java.util.Optional; 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 ctbrec.ui.AutosizeAlert;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.ButtonType; import javafx.scene.control.ButtonType;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Dialog; import javafx.scene.control.Dialog;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea; import javafx.scene.control.TextArea;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
@ -46,7 +56,7 @@ public class Dialogs {
} }
alert.setContentText(content); alert.setContentText(content);
if (parent != null) { if (parent != null) {
Stage stage = (Stage) alert.getDialogPane().getScene().getWindow(); var stage = (Stage) alert.getDialogPane().getScene().getWindow();
stage.getScene().getStylesheets().addAll(parent.getStylesheets()); stage.getScene().getStylesheets().addAll(parent.getStylesheets());
} }
alert.showAndWait(); alert.showAndWait();
@ -63,22 +73,22 @@ public class Dialogs {
Dialog<String> dialog = new Dialog<>(); Dialog<String> dialog = new Dialog<>();
dialog.setTitle(title); dialog.setTitle(title);
dialog.setHeaderText(header); dialog.setHeaderText(header);
dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); dialog.getDialogPane().getButtonTypes().addAll(OK, CANCEL);
dialog.initModality(Modality.APPLICATION_MODAL); dialog.initModality(Modality.APPLICATION_MODAL);
dialog.setResizable(true); dialog.setResizable(true);
InputStream icon = Dialogs.class.getResourceAsStream("/icon.png"); 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)); stage.getIcons().add(new Image(icon));
if (parent != null) { if (parent != null) {
stage.getScene().getStylesheets().addAll(parent.getStylesheets()); stage.getScene().getStylesheets().addAll(parent.getStylesheets());
} }
GridPane grid = new GridPane(); var grid = new GridPane();
grid.setHgap(10); grid.setHgap(10);
grid.setVgap(10); grid.setVgap(10);
grid.setPadding(new Insets(20, 150, 10, 10)); grid.setPadding(new Insets(20, 150, 10, 10));
TextArea notes = new TextArea(text); var notes = new TextArea(text);
notes.setPrefRowCount(3); notes.setPrefRowCount(3);
grid.add(notes, 0, 0); grid.add(notes, 0, 0);
dialog.getDialogPane().setContent(grid); dialog.getDialogPane().setContent(grid);
@ -86,7 +96,7 @@ public class Dialogs {
Platform.runLater(notes::requestFocus); Platform.runLater(notes::requestFocus);
dialog.setResultConverter(dialogButton -> { dialog.setResultConverter(dialogButton -> {
if (dialogButton == ButtonType.OK) { if (dialogButton == OK) {
return notes.getText(); return notes.getText();
} }
return null; return null;
@ -98,31 +108,31 @@ public class Dialogs {
public static Boolean showCustomInput(Scene parent, String title, Region region) { public static Boolean showCustomInput(Scene parent, String title, Region region) {
Dialog<?> dialog = new Dialog<>(); Dialog<?> dialog = new Dialog<>();
dialog.setTitle(title); dialog.setTitle(title);
dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); dialog.getDialogPane().getButtonTypes().addAll(OK, CANCEL);
dialog.initModality(Modality.APPLICATION_MODAL); dialog.initModality(Modality.APPLICATION_MODAL);
dialog.setResizable(true); dialog.setResizable(true);
InputStream icon = Dialogs.class.getResourceAsStream("/icon.png"); 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)); stage.getIcons().add(new Image(icon));
if (parent != null) { if (parent != null) {
stage.getScene().getStylesheets().addAll(parent.getStylesheets()); stage.getScene().getStylesheets().addAll(parent.getStylesheets());
} }
dialog.getDialogPane().setContent(region); dialog.getDialogPane().setContent(region);
dialog.showAndWait(); dialog.showAndWait();
return dialog.getResult() == ButtonType.OK; return dialog.getResult() == OK;
} }
public static boolean showConfirmDialog(String title, String message, String header, Scene parent) { public static boolean showConfirmDialog(String title, String message, String header, Scene parent) {
AutosizeAlert confirm = new AutosizeAlert(AlertType.CONFIRMATION, message, parent, YES, NO); var confirm = new AutosizeAlert(AlertType.CONFIRMATION, message, parent, YES, NO);
confirm.setTitle(title); confirm.setTitle(title);
confirm.setHeaderText(header); confirm.setHeaderText(header);
confirm.showAndWait(); confirm.showAndWait();
return confirm.getResult() == ButtonType.YES; return confirm.getResult() == YES;
} }
public static ButtonType showShutdownDialog(Scene parent) { public static ButtonType showShutdownDialog(Scene parent) {
String message = "There are recordings in progress"; var message = "There are recordings in progress";
AutosizeAlert confirm = new AutosizeAlert(AlertType.CONFIRMATION, "", parent, YES, FINISH, NO); var confirm = new AutosizeAlert(AlertType.CONFIRMATION, "", parent, YES, FINISH, NO);
confirm.setTitle("Shutdown"); confirm.setTitle("Shutdown");
confirm.setHeaderText(message); confirm.setHeaderText(message);
((Button) confirm.getDialogPane().lookupButton(ButtonType.YES)).setText("Shutdown Now"); ((Button) confirm.getDialogPane().lookupButton(ButtonType.YES)).setText("Shutdown Now");
@ -134,4 +144,36 @@ public class Dialogs {
confirm.showAndWait(); confirm.showAndWait();
return confirm.getResult(); 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();
}
}
} }

View File

@ -2,10 +2,8 @@ package ctbrec.ui.tabs;
import java.io.IOException; import java.io.IOException;
import ctbrec.Config;
import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
import ctbrec.recorder.postprocessing.AbstractPlaceholderAwarePostProcessor; import ctbrec.recorder.postprocessing.AbstractPlaceholderAwarePostProcessor;
import ctbrec.recorder.postprocessing.PostProcessingContext;
public class DownloadPostprocessor extends AbstractPlaceholderAwarePostProcessor { public class DownloadPostprocessor extends AbstractPlaceholderAwarePostProcessor {
@ -15,7 +13,7 @@ public class DownloadPostprocessor extends AbstractPlaceholderAwarePostProcessor
} }
@Override @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 // nothing really to do in here, we just inherit from AbstractPlaceholderAwarePostProcessor to use fillInPlaceHolders
return true; return true;
} }

View File

@ -40,6 +40,7 @@ import ctbrec.io.UrlUtil;
import ctbrec.recorder.ProgressListener; import ctbrec.recorder.ProgressListener;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.recorder.RecordingPinnedException; import ctbrec.recorder.RecordingPinnedException;
import ctbrec.recorder.postprocessing.PostProcessingContext;
import ctbrec.ui.AutosizeAlert; import ctbrec.ui.AutosizeAlert;
import ctbrec.ui.CamrecApplication; import ctbrec.ui.CamrecApplication;
import ctbrec.ui.DesktopIntegration; import ctbrec.ui.DesktopIntegration;
@ -688,7 +689,10 @@ public class RecordingsTab extends Tab implements TabSelectionListener, Shutdown
} else { } else {
String downloadFilename = config.getSettings().downloadFilename; String downloadFilename = config.getSettings().downloadFilename;
String fileSuffix = config.getSettings().ffmpegFileSuffix; 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;
} }
} }

View File

@ -4,6 +4,8 @@ import static ctbrec.ui.controls.Dialogs.*;
import java.io.IOException; import java.io.IOException;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -29,6 +31,7 @@ import org.slf4j.LoggerFactory;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.GlobalThreadPool; import ctbrec.GlobalThreadPool;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.ModelGroup;
import ctbrec.event.EventBusHolder; import ctbrec.event.EventBusHolder;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.sites.Site; import ctbrec.sites.Site;
@ -39,10 +42,13 @@ import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.SiteUiFactory; import ctbrec.ui.SiteUiFactory;
import ctbrec.ui.TipDialog; import ctbrec.ui.TipDialog;
import ctbrec.ui.TokenLabel; import ctbrec.ui.TokenLabel;
import ctbrec.ui.action.AddToGroupAction;
import ctbrec.ui.action.EditGroupAction;
import ctbrec.ui.action.IgnoreModelsAction; import ctbrec.ui.action.IgnoreModelsAction;
import ctbrec.ui.action.OpenRecordingsDir; import ctbrec.ui.action.OpenRecordingsDir;
import ctbrec.ui.action.SetStopDateAction; import ctbrec.ui.action.SetStopDateAction;
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu; import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.controls.FasterVerticalScrollPaneSkin; import ctbrec.ui.controls.FasterVerticalScrollPaneSkin;
import ctbrec.ui.controls.SearchBox; import ctbrec.ui.controls.SearchBox;
import ctbrec.ui.controls.SearchPopover; import ctbrec.ui.controls.SearchPopover;
@ -467,52 +473,57 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
} }
private ContextMenu createContextMenu(ThumbCell cell) { private ContextMenu createContextMenu(ThumbCell cell) {
Model model = cell.getModel(); var model = cell.getModel();
boolean modelIsTrackedByRecorder = recorder.isTracked(model); 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))); 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)); 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)); 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))); 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))); 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)); 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)); 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)); 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)); 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)); follow.setOnAction(e -> follow(getSelectedThumbCells(cell), true));
MenuItem unfollow = new MenuItem("Unfollow"); var unfollow = new MenuItem("Unfollow");
unfollow.setOnAction(e -> follow(getSelectedThumbCells(cell), false)); 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))); ignore.setOnAction(e -> ignore(getSelectedThumbCells(cell)));
MenuItem refresh = new MenuItem("Refresh Overview"); var refresh = new MenuItem("Refresh Overview");
refresh.setOnAction(e -> refresh()); 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()); openRecDir.setOnAction(e -> new OpenRecordingsDir(cell, model).execute());
MenuItem copyUrl = createCopyUrlMenuItem(cell); var copyUrl = createCopyUrlMenuItem(cell);
MenuItem openInBrowser = createOpenInBrowser(cell); var openInBrowser = createOpenInBrowser(cell);
MenuItem sendTip = createTipMenuItem(cell); var sendTip = createTipMenuItem(cell);
configureItemsForSelection(cell, openInPlayer, copyUrl, sendTip); configureItemsForSelection(cell, openInPlayer, copyUrl, sendTip);
@ -528,16 +539,18 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
} }
contextMenu.getItems().add(new SeparatorMenuItem()); contextMenu.getItems().add(new SeparatorMenuItem());
if (site.supportsFollow()) { if (site.supportsFollow()) {
MenuItem followOrUnFollow = (this instanceof FollowedTab) ? unfollow : follow; var followOrUnFollow = (this instanceof FollowedTab) ? unfollow : follow;
followOrUnFollow.setDisable(!site.credentialsAvailable()); followOrUnFollow.setDisable(!site.credentialsAvailable());
contextMenu.getItems().add(followOrUnFollow); contextMenu.getItems().add(followOrUnFollow);
} }
if (site.supportsTips()) { if (site.supportsTips()) {
contextMenu.getItems().add(sendTip); contextMenu.getItems().add(sendTip);
} }
Optional<ModelGroup> modelGroup = getModelGroup(model);
contextMenu.getItems().add(modelGroup.isEmpty() ? addToGroup : editGroup);
contextMenu.getItems().addAll(copyUrl, openInBrowser, ignore, refresh, openRecDir); contextMenu.getItems().addAll(copyUrl, openInBrowser, ignore, refresh, openRecDir);
if (model instanceof MyFreeCamsModel && Objects.equals(System.getenv("CTBREC_DEV"), "1")) { 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)); debug.setOnAction(e -> MyFreeCamsClient.getInstance().getSessionState(model));
contextMenu.getItems().add(debug); contextMenu.getItems().add(debug);
} }
@ -545,6 +558,23 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
return contextMenu; 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) { private void recordLater(List<ThumbCell> list, boolean recordLater) {
for (ThumbCell cell : list) { for (ThumbCell cell : list) {
cell.recordLater(recordLater); cell.recordLater(recordLater);

View File

@ -1,5 +1,13 @@
package ctbrec.ui.tabs; 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.Model;
import ctbrec.sites.Site; import ctbrec.sites.Site;
import ctbrec.ui.SiteUiFactory; import ctbrec.ui.SiteUiFactory;
@ -7,13 +15,6 @@ import ctbrec.ui.controls.SearchPopover;
import ctbrec.ui.controls.SearchPopoverTreeList; import ctbrec.ui.controls.SearchPopoverTreeList;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.concurrent.Task; 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>> { public class ThumbOverviewTabSearchTask extends Task<List<Model>> {

View File

@ -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();
}
}
});
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}

View File

@ -1,4 +1,4 @@
package ctbrec.ui.tabs; package ctbrec.ui.tabs.recorded;
import java.io.IOException; import java.io.IOException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
@ -37,6 +37,7 @@ import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.controls.SearchBox; import ctbrec.ui.controls.SearchBox;
import ctbrec.ui.controls.autocomplete.AutoFillTextField; import ctbrec.ui.controls.autocomplete.AutoFillTextField;
import ctbrec.ui.controls.autocomplete.ObservableListSuggester; import ctbrec.ui.controls.autocomplete.ObservableListSuggester;
import ctbrec.ui.tabs.TabSelectionListener;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringPropertyBase; import javafx.beans.property.StringPropertyBase;

View File

@ -1,16 +1,11 @@
package ctbrec.ui.tabs; package ctbrec.ui.tabs.recorded;
import static ctbrec.Recording.State.*; import static ctbrec.Recording.State.*;
import static ctbrec.ui.UnicodeEmoji.*;
import java.io.IOException; import java.io.IOException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.time.Instant; import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
@ -31,6 +26,7 @@ import org.slf4j.LoggerFactory;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.ModelGroup;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.StringUtil; import ctbrec.StringUtil;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
@ -40,7 +36,9 @@ import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.JavaFxModel; import ctbrec.ui.JavaFxModel;
import ctbrec.ui.PreviewPopupHandler; import ctbrec.ui.PreviewPopupHandler;
import ctbrec.ui.StreamSourceSelectionDialog; import ctbrec.ui.StreamSourceSelectionDialog;
import ctbrec.ui.action.AddToGroupAction;
import ctbrec.ui.action.CheckModelAccountAction; import ctbrec.ui.action.CheckModelAccountAction;
import ctbrec.ui.action.EditGroupAction;
import ctbrec.ui.action.EditNotesAction; import ctbrec.ui.action.EditNotesAction;
import ctbrec.ui.action.FollowAction; import ctbrec.ui.action.FollowAction;
import ctbrec.ui.action.IgnoreModelsAction; import ctbrec.ui.action.IgnoreModelsAction;
@ -59,6 +57,7 @@ import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.controls.SearchBox; import ctbrec.ui.controls.SearchBox;
import ctbrec.ui.controls.autocomplete.AutoFillTextField; import ctbrec.ui.controls.autocomplete.AutoFillTextField;
import ctbrec.ui.controls.autocomplete.ObservableListSuggester; import ctbrec.ui.controls.autocomplete.ObservableListSuggester;
import ctbrec.ui.tabs.TabSelectionListener;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringPropertyBase; import javafx.beans.property.StringPropertyBase;
@ -157,7 +156,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
table.setEditable(true); table.setEditable(true);
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
PreviewPopupHandler previewPopupHandler = new PreviewPopupHandler(table); var previewPopupHandler = new PreviewPopupHandler(table);
table.setRowFactory(tableview -> { table.setRowFactory(tableview -> {
TableRow<JavaFxModel> row = new TableRow<>(); TableRow<JavaFxModel> row = new TableRow<>();
row.addEventHandler(MouseEvent.ANY, previewPopupHandler); row.addEventHandler(MouseEvent.ANY, previewPopupHandler);
@ -174,7 +173,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
TableColumn<JavaFxModel, String> name = new TableColumn<>("Model"); TableColumn<JavaFxModel, String> name = new TableColumn<>("Model");
name.setPrefWidth(200); name.setPrefWidth(200);
name.setCellValueFactory(new PropertyValueFactory<>("displayName")); name.setCellValueFactory(new PropertyValueFactory<>("displayName"));
name.setCellFactory(new ClickableCellFactory<>()); name.setCellFactory(param -> new ModelNameTableCell(recorder));
name.setEditable(false); name.setEditable(false);
name.setId("name"); name.setId("name");
TableColumn<JavaFxModel, String> url = new TableColumn<>("URL"); TableColumn<JavaFxModel, String> url = new TableColumn<>("URL");
@ -183,15 +182,16 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
url.setPrefWidth(400); url.setPrefWidth(400);
url.setEditable(false); url.setEditable(false);
url.setId("url"); url.setId("url");
TableColumn<JavaFxModel, String> online = new TableColumn<>("Online"); TableColumn<JavaFxModel, Boolean> online = new TableColumn<>("Online");
online.setCellValueFactory(cdf -> cdf.getValue().getOnlineProperty()); online.setCellValueFactory(cdf -> cdf.getValue().getOnlineProperty());
online.setCellFactory(param -> new OnlineTableCell());
online.setPrefWidth(100); online.setPrefWidth(100);
online.setEditable(false); online.setEditable(false);
online.setId("online"); online.setId("online");
online.setStyle(STYLE_ALIGN_CENTER); 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.setCellValueFactory(cdf -> cdf.getValue().getRecordingProperty());
recording.setCellFactory(tc -> new RecordingCell()); recording.setCellFactory(tc -> new RecordingTableCell());
recording.setPrefWidth(100); recording.setPrefWidth(100);
recording.setEditable(false); recording.setEditable(false);
recording.setId("recording"); recording.setId("recording");
@ -272,7 +272,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
scrollPane.setContent(table); scrollPane.setContent(table);
HBox addModelBox = new HBox(5); var addModelBox = new HBox(5);
modelLabel.setPadding(new Insets(5, 0, 0, 0)); modelLabel.setPadding(new Insets(5, 0, 0, 0));
ObservableList<String> suggestions = FXCollections.observableArrayList(); ObservableList<String> suggestions = FXCollections.observableArrayList();
sites.forEach(site -> suggestions.add(site.getClass().getSimpleName())); 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) checkModelAccountExistance.setOnAction(evt -> new CheckModelAccountAction(checkModelAccountExistance, recorder)
.execute(Predicate.not(Model::isMarkedForLaterRecording))); .execute(Predicate.not(Model::isMarkedForLaterRecording)));
HBox filterContainer = new HBox(); var filterContainer = new HBox();
filterContainer.setSpacing(0); filterContainer.setSpacing(0);
filterContainer.setPadding(new Insets(0)); filterContainer.setPadding(new Insets(0));
filterContainer.setAlignment(Pos.CENTER_RIGHT); filterContainer.setAlignment(Pos.CENTER_RIGHT);
@ -326,7 +326,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
filterContainer.getChildren().add(filter); filterContainer.getChildren().add(filter);
addModelBox.getChildren().add(filterContainer); addModelBox.getChildren().add(filterContainer);
BorderPane root = new BorderPane(); var root = new BorderPane();
root.setPadding(new Insets(5)); root.setPadding(new Insets(5));
root.setTop(addModelBox); root.setTop(addModelBox);
root.setCenter(scrollPane); root.setCenter(scrollPane);
@ -338,7 +338,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
private void jumpToNextModel(KeyCode code) { private void jumpToNextModel(KeyCode code) {
if (!table.getItems().isEmpty() && (code.isLetterKey() || code.isDigitKey())) { if (!table.getItems().isEmpty() && (code.isLetterKey() || code.isDigitKey())) {
// determine where to start looking for the next model // determine where to start looking for the next model
int startAt = 0; var startAt = 0;
if (table.getSelectionModel().getSelectedIndex() >= 0) { if (table.getSelectionModel().getSelectedIndex() >= 0) {
startAt = table.getSelectionModel().getSelectedIndex() + 1; startAt = table.getSelectionModel().getSelectedIndex() + 1;
if (startAt >= table.getItems().size()) { 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; int i = startAt;
do { do {
JavaFxModel current = table.getItems().get(i); JavaFxModel current = table.getItems().get(i);
@ -378,7 +378,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
private void updatePriority(JavaFxModel model, int priority) { private void updatePriority(JavaFxModel model, int priority) {
try { try {
if (priority < 0 || priority > 100) { 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); Dialogs.showError(table.getScene(), "Invalid value", msg, null);
} else { } else {
model.setPriority(priority); model.setPriority(priority);
@ -404,7 +404,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
private void addModelByUrl(String url) { private void addModelByUrl(String url) {
for (Site site : sites) { for (Site site : sites) {
Model newModel = site.createModelFromUrl(url); var newModel = site.createModelFromUrl(url);
if (newModel != null) { if (newModel != null) {
try { try {
recorder.addModel(newModel); recorder.addModel(newModel);
@ -431,7 +431,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
for (Site site : sites) { for (Site site : sites) {
if (Objects.equals(siteName.toLowerCase(), site.getClass().getSimpleName().toLowerCase())) { if (Objects.equals(siteName.toLowerCase(), site.getClass().getSimpleName().toLowerCase())) {
try { try {
Model m = site.createModel(modelName); var m = site.createModel(modelName);
recorder.addModel(m); recorder.addModel(m);
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) { } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
Dialogs.showError(getTabPane().getScene(), "Couldn't add model", "The model " + modelName + " could not be added:", 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(" "); String[] tokens = filter.split(" ");
observableModels.addAll(filteredModels); observableModels.addAll(filteredModels);
filteredModels.clear(); filteredModels.clear();
for (int i = 0; i < table.getItems().size(); i++) { for (var i = 0; i < table.getItems().size(); i++) {
StringBuilder sb = new StringBuilder(); var sb = new StringBuilder();
for (TableColumn<JavaFxModel, ?> tc : table.getColumns()) { for (TableColumn<JavaFxModel, ?> tc : table.getColumns()) {
Object cellData = tc.getCellData(i); Object cellData = tc.getCellData(i);
if (cellData != null) { if (cellData != null) {
String content = cellData.toString(); var content = cellData.toString();
sb.append(content).append(' '); sb.append(content).append(' ');
} }
} }
String searchText = sb.toString(); var searchText = sb.toString();
boolean tokensMissing = false; var tokensMissing = false;
for (String token : tokens) { for (String token : tokens) {
if (!searchText.toLowerCase().contains(token.toLowerCase())) { if (!searchText.toLowerCase().contains(token.toLowerCase())) {
tokensMissing = true; tokensMissing = true;
@ -594,18 +594,14 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
.peek(fxm -> { // NOSONAR .peek(fxm -> { // NOSONAR
for (Recording recording : recordings) { for (Recording recording : recordings) {
if(recording.getStatus() == RECORDING && Objects.equals(recording.getModel(), fxm)){ if(recording.getStatus() == RECORDING && Objects.equals(recording.getModel(), fxm)){
String recordingValue = HEAVY_CHECK_MARK; fxm.setRecordingProperty(true);
if(!Objects.equals(recording.getModel().getRecordUntil(), Instant.ofEpochMilli(Model.RECORD_INDEFINITELY))) {
recordingValue += ' ' + CLOCK;
}
fxm.getRecordingProperty().set(recordingValue);
break; break;
} }
} }
for (Model onlineModel : onlineModels) { for (Model onlineModel : onlineModels) {
if(Objects.equals(onlineModel, fxm)) { if(Objects.equals(onlineModel, fxm)) {
fxm.getOnlineProperty().set(HEAVY_CHECK_MARK); fxm.setOnlineProperty(true);
break; break;
} }
} }
@ -616,7 +612,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
} }
}; };
ExecutorService executor = Executors.newSingleThreadExecutor(r -> { ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r); var t = new Thread(r);
t.setDaemon(true); t.setDaemon(true);
t.setName("RecordedModelsTab UpdateService"); t.setName("RecordedModelsTab UpdateService");
return t; return t;
@ -645,44 +641,49 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
if (selectedModels.isEmpty()) { if (selectedModels.isEmpty()) {
return null; return null;
} }
MenuItem stop = new MenuItem("Remove Model"); var stop = new MenuItem("Remove Model");
stop.setOnAction(e -> stopAction(selectedModels)); stop.setOnAction(e -> stopAction(selectedModels));
MenuItem recordLater = new MenuItem("Record Later"); var recordLater = new MenuItem("Record Later");
recordLater.setOnAction(e -> recordLater(selectedModels)); recordLater.setOnAction(e -> recordLater(selectedModels));
MenuItem copyUrl = new MenuItem("Copy URL"); var copyUrl = new MenuItem("Copy URL");
copyUrl.setOnAction(e -> { copyUrl.setOnAction(e -> {
Model selected = selectedModels.get(0); Model selected = selectedModels.get(0);
final Clipboard clipboard = Clipboard.getSystemClipboard(); final var clipboard = Clipboard.getSystemClipboard();
final ClipboardContent content = new ClipboardContent(); final var content = new ClipboardContent();
content.putString(selected.getUrl()); content.putString(selected.getUrl());
clipboard.setContent(content); clipboard.setContent(content);
}); });
MenuItem pauseRecording = new MenuItem("Pause Recording"); var pauseRecording = new MenuItem("Pause Recording");
pauseRecording.setOnAction(e -> pauseRecording(selectedModels)); pauseRecording.setOnAction(e -> pauseRecording(selectedModels));
MenuItem resumeRecording = new MenuItem("Resume Recording"); var resumeRecording = new MenuItem("Resume Recording");
resumeRecording.setOnAction(e -> resumeRecording(selectedModels)); 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))); 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))); 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())); 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))); 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))); switchStreamSource.setOnAction(e -> switchStreamSource(selectedModels.get(0)));
MenuItem follow = new MenuItem("Follow"); var follow = new MenuItem("Follow");
follow.setOnAction(e -> follow(selectedModels)); follow.setOnAction(e -> follow(selectedModels));
follow.setDisable(!selectedModels.stream().allMatch(m -> m.getSite().supportsFollow() && m.getSite().credentialsAvailable())); 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)); ignore.setOnAction(e -> ignore(selectedModels));
MenuItem notes = new MenuItem("Notes"); var notes = new MenuItem("Notes");
notes.setOnAction(e -> notes(selectedModels)); 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()); 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); ContextMenu menu = new CustomMouseBehaviorContextMenu(stop, recordLater);
if (selectedModels.size() == 1) { if (selectedModels.size() == 1) {
menu.getItems().add(selectedModels.get(0).isSuspended() ? resumeRecording : pauseRecording); menu.getItems().add(selectedModels.get(0).isSuspended() ? resumeRecording : pauseRecording);
@ -693,6 +694,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
} else { } else {
menu.getItems().addAll(resumeRecording, pauseRecording); 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); menu.getItems().addAll(copyUrl, openInPlayer, openInBrowser, openRecDir, switchStreamSource, follow, notes, ignore);
if (selectedModels.size() > 1) { if (selectedModels.size() > 1) {
@ -706,6 +709,25 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
return menu; 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) { private void setStopDate(JavaFxModel model) {
new SetStopDateAction(table, model.getDelegate(), recorder) // new SetStopDateAction(table, model.getDelegate(), recorder) //
.execute() // .execute() //
@ -735,7 +757,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
} }
private void switchStreamSource(JavaFxModel fxModel) { private void switchStreamSource(JavaFxModel fxModel) {
String couldntSwitchHeaderText = "Couldn't switch stream resolution"; var couldntSwitchHeaderText = "Couldn't switch stream resolution";
try { try {
if (!fxModel.isOnline(true)) { if (!fxModel.isOnline(true)) {
@ -781,7 +803,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
} }
private boolean stopAction(List<JavaFxModel> selectedModels) { private boolean stopAction(List<JavaFxModel> selectedModels) {
boolean confirmed = true; var confirmed = true;
if (Config.getInstance().getSettings().confirmationForDangerousActions) { if (Config.getInstance().getSettings().confirmationForDangerousActions) {
int n = selectedModels.size(); int n = selectedModels.size();
String plural = n > 1 ? "s" : ""; String plural = n > 1 ? "s" : "";
@ -829,9 +851,9 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
Config.getInstance().getSettings().recordedModelsSortType = col.getSortType().toString(); Config.getInstance().getSettings().recordedModelsSortType = col.getSortType().toString();
} }
int columns = table.getColumns().size(); int columns = table.getColumns().size();
double[] columnWidths = new double[columns]; var columnWidths = new double[columns];
String[] columnIds = new String[columns]; var columnIds = new String[columns];
for (int i = 0; i < columnWidths.length; i++) { for (var i = 0; i < columnWidths.length; i++) {
columnWidths[i] = table.getColumns().get(i).getWidth(); columnWidths[i] = table.getColumns().get(i).getWidth();
columnIds[i] = table.getColumns().get(i).getId(); columnIds[i] = table.getColumns().get(i).getId();
} }
@ -862,8 +884,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
private void restoreColumnOrder() { private void restoreColumnOrder() {
String[] columnIds = Config.getInstance().getSettings().recordedModelsColumnIds; String[] columnIds = Config.getInstance().getSettings().recordedModelsColumnIds;
ObservableList<TableColumn<JavaFxModel,?>> columns = table.getColumns(); ObservableList<TableColumn<JavaFxModel,?>> columns = table.getColumns();
for (int i = 0; i < columnIds.length; i++) { for (var i = 0; i < columnIds.length; i++) {
for (int j = 0; j < table.getColumns().size(); j++) { for (var j = 0; j < table.getColumns().size(); j++) {
if(Objects.equals(columnIds[i], columns.get(j).getId())) { if(Objects.equals(columnIds[i], columns.get(j).getId())) {
TableColumn<JavaFxModel, ?> col = columns.get(j); TableColumn<JavaFxModel, ?> col = columns.get(j);
columns.remove(j); // NOSONAR columns.remove(j); // NOSONAR
@ -876,7 +898,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
private void restoreColumnWidths() { private void restoreColumnWidths() {
double[] columnWidths = Config.getInstance().getSettings().recordedModelsColumnWidths; double[] columnWidths = Config.getInstance().getSettings().recordedModelsColumnWidths;
if (columnWidths != null && columnWidths.length == table.getColumns().size()) { 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]); table.getColumns().get(i).setPrefWidth(columnWidths[i]);
} }
} }
@ -930,25 +952,4 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
return tableCell; 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);
}
}
}
} }

View File

@ -1,10 +1,11 @@
package ctbrec.ui.tabs; package ctbrec.ui.tabs.recorded;
import java.util.List; import java.util.List;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.sites.Site; import ctbrec.sites.Site;
import ctbrec.ui.ShutdownListener; import ctbrec.ui.ShutdownListener;
import ctbrec.ui.tabs.TabSelectionListener;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
import javafx.geometry.Side; import javafx.geometry.Side;
import javafx.scene.control.Tab; import javafx.scene.control.Tab;

View File

@ -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);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -42,11 +42,14 @@ The part you have to copy is
<a id="variables" /> <a id="variables" />
###### Available variables:
- **${modelName}** - the name of the recorded model - **${modelName}** - the name of the recorded model
- **${modelDisplayName}** - the name of the recorded model, which is shown on the webpage. Might be the same as - **${modelDisplayName}** - the name of the recorded model, which is shown on the webpage. Might be the same as
${modelName} ${modelName}
- **${modelSanitizedName}** - sanitized name of the model. The following characters are replaced by an underscore: - **${modelSanitizedName}** - sanitized name of the model. The following characters are replaced by an underscore:
\\, /, ', " and space \\, /, ', " 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 - **${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: - **${siteSanitizedName}** - sanitized name of the site. The following characters are replaced by an underscore:
\\, /, ', " and space \\, /, ', " and space
@ -116,4 +119,15 @@ The part you have to copy is
</table> </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".

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

View File

@ -18,6 +18,7 @@ import java.util.List;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
@ -31,6 +32,7 @@ import ctbrec.Settings.SplitStrategy;
import ctbrec.io.FileJsonAdapter; import ctbrec.io.FileJsonAdapter;
import ctbrec.io.ModelJsonAdapter; import ctbrec.io.ModelJsonAdapter;
import ctbrec.io.PostProcessorJsonAdapter; import ctbrec.io.PostProcessorJsonAdapter;
import ctbrec.io.UuidJSonAdapter;
import ctbrec.recorder.postprocessing.DeleteTooShort; import ctbrec.recorder.postprocessing.DeleteTooShort;
import ctbrec.recorder.postprocessing.PostProcessor; import ctbrec.recorder.postprocessing.PostProcessor;
import ctbrec.recorder.postprocessing.RemoveKeepFile; import ctbrec.recorder.postprocessing.RemoveKeepFile;
@ -74,6 +76,7 @@ public class Config {
.add(Model.class, new ModelJsonAdapter(sites)) .add(Model.class, new ModelJsonAdapter(sites))
.add(PostProcessor.class, new PostProcessorJsonAdapter()) .add(PostProcessor.class, new PostProcessorJsonAdapter())
.add(File.class, new FileJsonAdapter()) .add(File.class, new FileJsonAdapter())
.add(UUID.class, new UuidJSonAdapter())
.build(); .build();
JsonAdapter<Settings> adapter = moshi.adapter(Settings.class).lenient(); JsonAdapter<Settings> adapter = moshi.adapter(Settings.class).lenient();
File configFile = new File(configDir, filename); File configFile = new File(configDir, filename);
@ -182,11 +185,11 @@ public class Config {
} }
// 3.11.0 make Cam4 model names lower case // 3.11.0 make Cam4 model names lower case
settings.models.stream() settings.models.stream()
.filter(Cam4Model.class::isInstance) .filter(Cam4Model.class::isInstance)
.forEach(m -> m.setName(m.getName().toLowerCase())); .forEach(m -> m.setName(m.getName().toLowerCase()));
settings.modelsIgnored.stream() settings.modelsIgnored.stream()
.filter(Cam4Model.class::isInstance) .filter(Cam4Model.class::isInstance)
.forEach(m -> m.setName(m.getName().toLowerCase())); .forEach(m -> m.setName(m.getName().toLowerCase()));
// 4.1.2 reduce models ignore to store only the URL // 4.1.2 reduce models ignore to store only the URL
if (settings.modelsIgnored != null && !settings.modelsIgnored.isEmpty()) { if (settings.modelsIgnored != null && !settings.modelsIgnored.isEmpty()) {
settings.ignoredModels = settings.modelsIgnored.stream() settings.ignoredModels = settings.modelsIgnored.stream()
@ -227,13 +230,14 @@ public class Config {
} }
public synchronized void save() throws IOException { public synchronized void save() throws IOException {
if (savingDisabled) { if (savingDisabled) {
return; return;
} }
Moshi moshi = new Moshi.Builder() Moshi moshi = new Moshi.Builder()
.add(Model.class, new ModelJsonAdapter()) .add(Model.class, new ModelJsonAdapter())
.add(PostProcessor.class, new PostProcessorJsonAdapter()) .add(PostProcessor.class, new PostProcessorJsonAdapter())
.add(File.class, new FileJsonAdapter()) .add(File.class, new FileJsonAdapter())
.add(UUID.class, new UuidJSonAdapter())
.build(); .build();
JsonAdapter<Settings> adapter = moshi.adapter(Settings.class).indent(" "); JsonAdapter<Settings> adapter = moshi.adapter(Settings.class).indent(" ");
String json = adapter.toJson(settings); String json = adapter.toJson(settings);
@ -299,12 +303,12 @@ public class Config {
public String getModelNotes(Model m) { public String getModelNotes(Model m) {
return Config.getInstance().getSettings().modelNotes.getOrDefault(m.getUrl(), ""); return Config.getInstance().getSettings().modelNotes.getOrDefault(m.getUrl(), "");
} }
public void disableSaving() { public void disableSaving() {
savingDisabled = true; savingDisabled = true;
} }
public void enableSaving() { public void enableSaving() {
savingDisabled = false; savingDisabled = false;
} }
} }

View File

@ -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());
}
}

View File

@ -3,8 +3,10 @@ package ctbrec;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import ctbrec.event.EventHandlerConfiguration; import ctbrec.event.EventHandlerConfiguration;
import ctbrec.recorder.postprocessing.PostProcessor; import ctbrec.recorder.postprocessing.PostProcessor;
@ -104,10 +106,12 @@ public class Settings {
public String mfcPassword = ""; public String mfcPassword = "";
public String mfcUsername = ""; public String mfcUsername = "";
public boolean minimizeToTray = false; public boolean minimizeToTray = false;
@Deprecated
public int minimumLengthInSeconds = 0; public int minimumLengthInSeconds = 0;
public long minimumSpaceLeftInBytes = 0; public long minimumSpaceLeftInBytes = 0;
public Map<String, String> modelNotes = new HashMap<>(); public Map<String, String> modelNotes = new HashMap<>();
public List<Model> models = new ArrayList<>(); public List<Model> models = new ArrayList<>();
public Set<ModelGroup> modelGroups = new HashSet<>();
@Deprecated @Deprecated
public List<Model> modelsIgnored = new ArrayList<>(); public List<Model> modelsIgnored = new ArrayList<>();
public boolean monitorClipboard = false; public boolean monitorClipboard = false;

View File

@ -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());
}
}

View File

@ -22,6 +22,7 @@ import java.util.Map;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@ -45,6 +46,7 @@ import com.google.common.eventbus.Subscribe;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.ModelGroup;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.Recording.State; import ctbrec.Recording.State;
import ctbrec.event.Event; import ctbrec.event.Event;
@ -54,6 +56,7 @@ import ctbrec.event.NoSpaceLeftEvent;
import ctbrec.event.RecordingStateChangedEvent; import ctbrec.event.RecordingStateChangedEvent;
import ctbrec.io.HttpClient; import ctbrec.io.HttpClient;
import ctbrec.recorder.download.Download; import ctbrec.recorder.download.Download;
import ctbrec.recorder.postprocessing.PostProcessingContext;
import ctbrec.recorder.postprocessing.PostProcessor; import ctbrec.recorder.postprocessing.PostProcessor;
import ctbrec.sites.Site; import ctbrec.sites.Site;
@ -88,18 +91,7 @@ public class NextGenLocalRecorder implements Recorder {
downloadPool = Executors.newScheduledThreadPool(5, createThreadFactory("Download", MAX_PRIORITY)); downloadPool = Executors.newScheduledThreadPool(5, createThreadFactory("Download", MAX_PRIORITY));
threadPoolScaler = new ThreadPoolScaler((ThreadPoolExecutor) downloadPool, 5); threadPoolScaler = new ThreadPoolScaler((ThreadPoolExecutor) downloadPool, 5);
recordingManager = new RecordingManager(config, sites); recordingManager = new RecordingManager(config, sites);
config.getSettings().models.stream().forEach(m -> { loadModels();
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());
}
});
int ppThreads = config.getSettings().postProcessingThreads; int ppThreads = config.getSettings().postProcessingThreads;
ppPool = new ThreadPoolExecutor(ppThreads, ppThreads, 5, TimeUnit.MINUTES, ppQueue, createThreadFactory("PP", MIN_PRIORITY)); 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); }, 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() { private void startCompletionHandler() {
downloadCompletionPool.submit(() -> { downloadCompletionPool.submit(() -> {
while (!Thread.currentThread().isInterrupted()) { while (!Thread.currentThread().isInterrupted()) {
@ -209,9 +215,10 @@ public class NextGenLocalRecorder implements Recorder {
recordingManager.saveRecording(recording); recordingManager.saveRecording(recording);
recording.postprocess(); recording.postprocess();
List<PostProcessor> postProcessors = config.getSettings().postProcessors; List<PostProcessor> postProcessors = config.getSettings().postProcessors;
PostProcessingContext ctx = createPostProcessingContext(recording);
for (PostProcessor postProcessor : postProcessors) { for (PostProcessor postProcessor : postProcessors) {
LOG.debug("Running post-processor: {}", postProcessor.getName()); LOG.debug("Running post-processor: {}", postProcessor.getName());
boolean continuePP = postProcessor.postprocess(recording, recordingManager, config); boolean continuePP = postProcessor.postprocess(ctx);
if (!continuePP) { if (!continuePP) {
break; 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) { private void setRecordingStatus(Recording recording, State status) {
recording.setStatus(status); recording.setStatus(status);
RecordingStateChangedEvent evt = new RecordingStateChangedEvent(recording.getDownload().getTarget(), status, recording.getModel(), RecordingStateChangedEvent evt = new RecordingStateChangedEvent(recording.getDownload().getTarget(), status, recording.getModel(),
@ -761,4 +777,23 @@ public class NextGenLocalRecorder implements Recorder {
public int getModelCount() { public int getModelCount() {
return (int) models.stream().filter(m -> !m.isMarkedForLaterRecording()).count(); 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();
}
} }

View File

@ -1,15 +1,18 @@
package ctbrec.recorder; package ctbrec.recorder;
import ctbrec.Model;
import ctbrec.Recording;
import ctbrec.io.HttpClient;
import java.io.IOException; import java.io.IOException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import ctbrec.Model;
import ctbrec.ModelGroup;
import ctbrec.Recording;
import ctbrec.io.HttpClient;
public interface Recorder { public interface Recorder {
public void addModel(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException; public void addModel(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException;
@ -143,5 +146,29 @@ public interface Recorder {
*/ */
public void resume() throws InvalidKeyException, NoSuchAlgorithmException, IOException; 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 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();
}
} }

View File

@ -2,7 +2,10 @@ package ctbrec.recorder;
import static ctbrec.recorder.NextGenLocalRecorder.*; import static ctbrec.recorder.NextGenLocalRecorder.*;
import java.io.IOException; import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant; import java.time.Instant;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -12,6 +15,7 @@ import org.slf4j.LoggerFactory;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.ModelGroup;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.recorder.download.Download; import ctbrec.recorder.download.Download;
@ -26,7 +30,7 @@ public class RecordingPreconditions {
this.recorder = recorder; this.recorder = recorder;
} }
void check(Model model) throws IOException { void check(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
ensureRecorderIsActive(); ensureRecorderIsActive();
ensureModelIsNotSuspended(model); ensureModelIsNotSuspended(model);
ensureModelIsNotMarkedForLaterRecording(model); ensureModelIsNotMarkedForLaterRecording(model);
@ -36,6 +40,7 @@ public class RecordingPreconditions {
ensureEnoughSpaceForRecording(); ensureEnoughSpaceForRecording();
ensureDownloadSlotAvailable(model); ensureDownloadSlotAvailable(model);
ensureModelIsOnline(model); ensureModelIsOnline(model);
ensureNoOtherFromModelGroupIsRecording(model);
} }
private void ensureModelIsOnline(Model model) { private void ensureModelIsOnline(Model model) {
@ -130,4 +135,55 @@ public class RecordingPreconditions {
int concurrentRecordings = Config.getInstance().getSettings().concurrentRecordings; int concurrentRecordings = Config.getInstance().getSettings().concurrentRecordings;
return concurrentRecordings == 0 || concurrentRecordings > 0 && recorder.getRecordingProcesses().size() < 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;
}
} }

View File

@ -1,25 +1,5 @@
package ctbrec.recorder; 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.File;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
@ -27,7 +7,43 @@ import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.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 { public class RemoteRecorder implements Recorder {
@ -41,16 +57,20 @@ public class RemoteRecorder implements Recorder {
.add(Instant.class, new InstantJsonAdapter()) .add(Instant.class, new InstantJsonAdapter())
.add(Model.class, new ModelJsonAdapter()) .add(Model.class, new ModelJsonAdapter())
.add(File.class, new FileJsonAdapter()) .add(File.class, new FileJsonAdapter())
.add(UUID.class, new UuidJSonAdapter())
.build(); .build();
private JsonAdapter<ModelListResponse> modelListResponseAdapter = moshi.adapter(ModelListResponse.class); private JsonAdapter<ModelListResponse> modelListResponseAdapter = moshi.adapter(ModelListResponse.class);
private JsonAdapter<RecordingListResponse> recordingListResponseAdapter = moshi.adapter(RecordingListResponse.class); private JsonAdapter<RecordingListResponse> recordingListResponseAdapter = moshi.adapter(RecordingListResponse.class);
private JsonAdapter<ModelRequest> modelRequestAdapter = moshi.adapter(ModelRequest.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<RecordingRequest> recordingRequestAdapter = moshi.adapter(RecordingRequest.class);
private JsonAdapter<SimpleResponse> simpleResponseAdapter = moshi.adapter(SimpleResponse.class); private JsonAdapter<SimpleResponse> simpleResponseAdapter = moshi.adapter(SimpleResponse.class);
private List<Model> models = Collections.emptyList(); private List<Model> models = Collections.emptyList();
private List<Model> onlineModels = Collections.emptyList(); private List<Model> onlineModels = Collections.emptyList();
private List<Recording> recordings = Collections.emptyList(); private List<Recording> recordings = Collections.emptyList();
private Set<ModelGroup> modelGroups = new HashSet<>();
private List<Site> sites; private List<Site> sites;
private long spaceTotal = -1; private long spaceTotal = -1;
private long spaceFree = -1; private long spaceFree = -1;
@ -91,7 +111,7 @@ public class RemoteRecorder implements Recorder {
private Optional<String> sendRequest(String action) throws IOException, InvalidKeyException, NoSuchAlgorithmException { private Optional<String> sendRequest(String action) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
String msg = "{\"action\": \"" + action + "\"}"; 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); RequestBody requestBody = RequestBody.Companion.create(msg, JSON);
Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(requestBody); Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(requestBody);
addHmacIfNeeded(msg, builder); addHmacIfNeeded(msg, builder);
@ -109,7 +129,7 @@ public class RemoteRecorder implements Recorder {
private void sendRequest(String action, Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException { private void sendRequest(String action, Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
String payload = modelRequestAdapter.toJson(new ModelRequest(action, model)); String payload = modelRequestAdapter.toJson(new ModelRequest(action, model));
LOG.debug("Sending request to recording server: {}", payload); LOG.trace("Sending request to recording server: {}", payload);
RequestBody body = RequestBody.Companion.create(payload, JSON); RequestBody body = RequestBody.Companion.create(payload, JSON);
Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body); Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
addHmacIfNeeded(payload, builder); addHmacIfNeeded(payload, builder);
@ -139,6 +159,7 @@ public class RemoteRecorder implements Recorder {
String msg = recordingRequestAdapter.toJson(recReq); String msg = recordingRequestAdapter.toJson(recReq);
RequestBody body = RequestBody.Companion.create(msg, JSON); RequestBody body = RequestBody.Companion.create(msg, JSON);
Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body); Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
LOG.trace("Sending request to recording server: {}", msg);
addHmacIfNeeded(msg, builder); addHmacIfNeeded(msg, builder);
Request request = builder.build(); Request request = builder.build();
try (Response response = client.execute(request)) { 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 { private void addHmacIfNeeded(String msg, Builder builder) throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
if (Config.getInstance().getSettings().requireAuthentication) { if (Config.getInstance().getSettings().requireAuthentication) {
byte[] key = Config.getInstance().getSettings().key; byte[] key = Config.getInstance().getSettings().key;
@ -225,10 +273,25 @@ public class RemoteRecorder implements Recorder {
syncOnlineModels(); syncOnlineModels();
syncSpace(); syncSpace();
syncRecordings(); syncRecordings();
syncModelGroups();
sleep(); 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() { private void syncSpace() {
try { try {
String msg = "{\"action\": \"space\"}"; String msg = "{\"action\": \"space\"}";
@ -412,6 +475,12 @@ public class RemoteRecorder implements Recorder {
public List<Model> models; public List<Model> models;
} }
private static class ModelGroupListResponse {
public String status;
public String msg;
public List<ModelGroup> groups;
}
private static class SimpleResponse { private static class SimpleResponse {
public String status; public String status;
public String msg; 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 { public static class RecordingRequest {
private String action; private String action;
private Recording recording; private Recording recording;
@ -592,4 +688,19 @@ public class RemoteRecorder implements Recorder {
public int getModelCount() { public int getModelCount() {
return (int) models.stream().filter(m -> !m.isMarkedForLaterRecording()).count(); 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);
}
} }

View File

@ -3,105 +3,167 @@ package ctbrec.recorder.postprocessing;
import static ctbrec.StringUtil.*; import static ctbrec.StringUtil.*;
import static java.util.Optional.*; 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.ZoneId;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Locale; import java.util.Locale;
import java.util.regex.Matcher; import java.util.Map;
import java.util.regex.Pattern; 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.Config;
import ctbrec.Model;
import ctbrec.ModelGroup;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.StringUtil;
import ctbrec.sites.Site; import ctbrec.sites.Site;
public abstract class AbstractPlaceholderAwarePostProcessor extends AbstractPostProcessor { public abstract class AbstractPlaceholderAwarePostProcessor extends AbstractPostProcessor {
public static final String[] PLACE_HOLDERS = { private static final Logger LOG = LoggerFactory.getLogger(AbstractPlaceholderAwarePostProcessor.class);
"${modelName}",
"${modelDisplayName}",
"${modelSanitizedName}",
"${siteName}",
"${siteSanitizedName}",
"${utcDateTime}",
"${localDateTime}",
"${epochSecond}",
"${fileSuffix}",
"${modelNotes}",
"${recordingNotes}",
"${recordingsDir}",
"${absolutePath}",
"${absoluteParentPath}"
};
public String fillInPlaceHolders(String input, Recording rec, Config config) { public String fillInPlaceHolders(String input, PostProcessingContext ctx) {
// @formatter:off Recording rec = ctx.getRecording();
String output = input Config config = ctx.getConfig();
.replace("${modelName}", ofNullable(rec.getModel().getName()).orElse("modelName")) Optional<ModelGroup> modelGroup;
.replace("${modelDisplayName}", ofNullable(rec.getModel().getDisplayName()).orElse("displayName")) try {
.replace("${modelSanitizedName}", ofNullable(rec.getModel().getSanitizedNamed()).orElse("sanitizedName")) modelGroup = ctx.getRecorder().getModelGroup(rec.getModel());
.replace("${siteName}", ofNullable(rec.getModel().getSite()).map(Site::getName).orElse("site")) } catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
.replace("${siteSanitizedName}", getSanitizedSiteName(rec)) LOG.error("Couldn't get model group for {}", rec.getModel(), e);
.replace("${fileSuffix}", getFileSuffix(rec)) return input;
.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())
;
output = replaceUtcDateTime(rec, output); Map<String, Function<String, Optional<String>>> placeholderValueSuppliers = new HashMap<>();
output = replaceLocalDateTime(rec, output); 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; return output;
// @formatter:on
} }
private String replaceUtcDateTime(Recording rec, String filename) { private Optional<String> getSanitizedName(Model model) {
return replaceDateTime(rec, filename, "utcDateTime", ZoneOffset.UTC); String name = model.getSanitizedNamed();
} if (StringUtil.isBlank(name)) {
return Optional.empty();
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);
} else { } else {
return ""; return Optional.of(name);
} }
} }
private CharSequence getSanitizedSiteName(Recording rec) { private String fillInPlaceHolders(String input, Map<String, Function<String, Optional<String>>> placeholderValueSuppliers) {
return sanitize(ofNullable(rec.getModel().getSite()).map(Site::getName).orElse("")); 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) { private Optional<String> replaceUtcDateTime(Recording rec, String pattern) {
return sanitize(ofNullable(rec.getNote()).orElse("")); 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();
}
} }
} }

View File

@ -8,9 +8,7 @@ import org.apache.commons.io.FileUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
public class Copy extends AbstractPostProcessor { public class Copy extends AbstractPostProcessor {
@ -22,7 +20,8 @@ public class Copy extends AbstractPostProcessor {
} }
@Override @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(); File orig = rec.getPostProcessedFile();
String copyFilename = getFilenameForCopy(orig); String copyFilename = getFilenameForCopy(orig);
File copy = new File(orig.getParentFile(), copyFilename); File copy = new File(orig.getParentFile(), copyFilename);

View File

@ -22,7 +22,6 @@ import ctbrec.OS;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.io.IoUtils; import ctbrec.io.IoUtils;
import ctbrec.recorder.FFmpeg; import ctbrec.recorder.FFmpeg;
import ctbrec.recorder.RecordingManager;
import ctbrec.recorder.download.ProcessExitedUncleanException; import ctbrec.recorder.download.ProcessExitedUncleanException;
@ThreadSafe @ThreadSafe
@ -45,7 +44,9 @@ public class CreateContactSheet extends AbstractPlaceholderAwarePostProcessor {
} }
@Override @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 totalWidth = Integer.parseInt(getConfig().getOrDefault(TOTAL_SIZE, "1920"));
int padding = Integer.parseInt(getConfig().getOrDefault(PADDING, "4")); int padding = Integer.parseInt(getConfig().getOrDefault(PADDING, "4"));
int cols = Integer.parseInt(getConfig().getOrDefault(COLS, "8")); int cols = Integer.parseInt(getConfig().getOrDefault(COLS, "8"));
@ -65,7 +66,7 @@ public class CreateContactSheet extends AbstractPlaceholderAwarePostProcessor {
color}, color},
new StringBuffer(), null).toString(); new StringBuffer(), null).toString();
File executionDir = rec.getPostProcessedFile().isDirectory() ? rec.getPostProcessedFile() : rec.getPostProcessedFile().getParentFile(); 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 = { String[] args = {
"-y", "-y",

View File

@ -2,10 +2,7 @@ package ctbrec.recorder.postprocessing;
import java.io.IOException; import java.io.IOException;
import ctbrec.Config;
import ctbrec.NotImplementedExcetion; import ctbrec.NotImplementedExcetion;
import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
public class CreateTimelineThumbs extends AbstractPlaceholderAwarePostProcessor { public class CreateTimelineThumbs extends AbstractPlaceholderAwarePostProcessor {
@ -15,7 +12,7 @@ public class CreateTimelineThumbs extends AbstractPlaceholderAwarePostProcessor
} }
@Override @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 // 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 // ffmpeg -i <file> -vf 'fps=1,scale=360:-1' thumbs/out%05d.jpg
throw new NotImplementedExcetion(); throw new NotImplementedExcetion();

View File

@ -5,9 +5,7 @@ import static ctbrec.io.IoUtils.*;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import ctbrec.Config;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
public class DeleteOriginal extends AbstractPostProcessor { public class DeleteOriginal extends AbstractPostProcessor {
@ -17,7 +15,8 @@ public class DeleteOriginal extends AbstractPostProcessor {
} }
@Override @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()) { if (rec.getAbsoluteFile().isFile()) {
Files.deleteIfExists(rec.getAbsoluteFile().toPath()); Files.deleteIfExists(rec.getAbsoluteFile().toPath());
deleteEmptyParents(rec.getAbsoluteFile().getParentFile()); deleteEmptyParents(rec.getAbsoluteFile().getParentFile());

View File

@ -6,7 +6,6 @@ import java.time.Duration;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.recorder.RecordingManager; import ctbrec.recorder.RecordingManager;
@ -21,7 +20,9 @@ public class DeleteTooShort extends AbstractPostProcessor {
} }
@Override @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"))); Duration minimumLengthInSeconds = Duration.ofSeconds(Integer.parseInt(getConfig().getOrDefault(MIN_LEN_IN_SECS, "0")));
if (minimumLengthInSeconds.getSeconds() > 0) { if (minimumLengthInSeconds.getSeconds() > 0) {
Duration recordingLength = rec.getLength(); Duration recordingLength = rec.getLength();

View File

@ -10,9 +10,7 @@ import org.apache.commons.io.FileUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
public class Move extends AbstractPlaceholderAwarePostProcessor { public class Move extends AbstractPlaceholderAwarePostProcessor {
@ -26,9 +24,10 @@ public class Move extends AbstractPlaceholderAwarePostProcessor {
} }
@Override @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 pathTemplate = getConfig().getOrDefault(PATH_TEMPLATE, DEFAULT);
String path = fillInPlaceHolders(pathTemplate, rec, config); String path = fillInPlaceHolders(pathTemplate, ctx);
File src = rec.getPostProcessedFile(); File src = rec.getPostProcessedFile();
boolean isFile = src.isFile(); boolean isFile = src.isFile();
File target = new File(path, src.getName()); File target = new File(path, src.getName());

View File

@ -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;
}
}

View File

@ -4,23 +4,17 @@ import java.io.IOException;
import java.io.Serializable; import java.io.Serializable;
import java.util.Map; import java.util.Map;
import ctbrec.Config;
import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
public interface PostProcessor extends Serializable { public interface PostProcessor extends Serializable {
String getName(); String getName();
/** /**
* Runs the post-processing code on the given recording * Runs the post-processing code on the given recording
* @param rec the recording to post-process * @param ctx {@link PostProcessingContext}, which allows access to post-processing related objects
* @param recordingManager
* @param config
* @return false to stop futher post-processing, true to continue * @return false to stop futher post-processing, true to continue
* @throws IOException * @throws IOException
* @throws InterruptedException * @throws InterruptedException
*/ */
boolean postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException; boolean postprocess(PostProcessingContext ctx) throws IOException, InterruptedException;
Map<String, String> getConfig(); Map<String, String> getConfig();
void setConfig(Map<String, String> conf); void setConfig(Map<String, String> conf);

View File

@ -2,10 +2,6 @@ package ctbrec.recorder.postprocessing;
import java.io.IOException; import java.io.IOException;
import ctbrec.Config;
import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
public class RemoveKeepFile extends AbstractPostProcessor { public class RemoveKeepFile extends AbstractPostProcessor {
@Override @Override
@ -14,9 +10,9 @@ public class RemoveKeepFile extends AbstractPostProcessor {
} }
@Override @Override
public boolean postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException { public boolean postprocess(PostProcessingContext ctx) throws IOException, InterruptedException {
recordingManager.remove(rec); ctx.getRecordingManager().remove(ctx.getRecording());
rec.setMetaDataFile(null); ctx.getRecording().setMetaDataFile(null);
return true; return true;
} }
} }

View File

@ -9,12 +9,10 @@ import java.util.Objects;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.OS; import ctbrec.OS;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.io.IoUtils; import ctbrec.io.IoUtils;
import ctbrec.recorder.FFmpeg; import ctbrec.recorder.FFmpeg;
import ctbrec.recorder.RecordingManager;
public class Remux extends AbstractPostProcessor { public class Remux extends AbstractPostProcessor {
@ -29,7 +27,8 @@ public class Remux extends AbstractPostProcessor {
} }
@Override @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; final File inputFile;
if (rec.getPostProcessedFile().isDirectory()) { if (rec.getPostProcessedFile().isDirectory()) {
inputFile = new File(rec.getPostProcessedFile(), "playlist.m3u8"); 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"); File ffmpegLog = new File(System.getProperty("java.io.tmpdir"), "remux_" + rec.getId() + ".log");
FFmpeg ffmpeg = new FFmpeg.Builder() FFmpeg ffmpeg = new FFmpeg.Builder()
.logOutput(config.getSettings().logFFmpegOutput) .logOutput(ctx.getConfig().getSettings().logFFmpegOutput)
.logFile(ffmpegLog) .logFile(ffmpegLog)
.onExit(exitCode -> finalizeStep(exitCode, rec, inputFile, remuxedFile)) .onExit(exitCode -> finalizeStep(exitCode, rec, inputFile, remuxedFile))
.build(); .build();

View File

@ -7,9 +7,7 @@ import java.util.Objects;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
public class Rename extends AbstractPlaceholderAwarePostProcessor { public class Rename extends AbstractPlaceholderAwarePostProcessor {
@ -24,10 +22,11 @@ public class Rename extends AbstractPlaceholderAwarePostProcessor {
} }
@Override @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 defaultTemplate = rec.isSingleFile() ? DEFAULT : DEFAULT_DIR;
String filenameTemplate = getConfig().getOrDefault(FILE_NAME_TEMPLATE, defaultTemplate); String filenameTemplate = getConfig().getOrDefault(FILE_NAME_TEMPLATE, defaultTemplate);
String filename = fillInPlaceHolders(filenameTemplate, rec, config); String filename = fillInPlaceHolders(filenameTemplate, ctx);
File src = rec.getPostProcessedFile(); File src = rec.getPostProcessedFile();
File target = new File(src.getParentFile(), filename); File target = new File(src.getParentFile(), filename);
if (Objects.equals(src, target)) { if (Objects.equals(src, target)) {

View File

@ -10,11 +10,9 @@ import java.util.List;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.OS; import ctbrec.OS;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.io.StreamRedirector; import ctbrec.io.StreamRedirector;
import ctbrec.recorder.RecordingManager;
import ctbrec.recorder.download.ProcessExitedUncleanException; import ctbrec.recorder.download.ProcessExitedUncleanException;
public class Script extends AbstractPlaceholderAwarePostProcessor { public class Script extends AbstractPlaceholderAwarePostProcessor {
@ -29,8 +27,9 @@ public class Script extends AbstractPlaceholderAwarePostProcessor {
} }
@Override @Override
public boolean postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException { public boolean postprocess(PostProcessingContext ctx) throws IOException, InterruptedException {
List<String> cmdline = buildCommandLine(rec, config); Recording rec = ctx.getRecording();
List<String> cmdline = buildCommandLine(ctx);
Runtime rt = Runtime.getRuntime(); Runtime rt = Runtime.getRuntime();
String[] args = cmdline.toArray(new String[0]); String[] args = cmdline.toArray(new String[0]);
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
@ -50,12 +49,12 @@ public class Script extends AbstractPlaceholderAwarePostProcessor {
return true; 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 script = getConfig().getOrDefault(SCRIPT_EXECUTABLE, "somescript");
String params = getConfig().getOrDefault(SCRIPT_PARAMS, "${absolutePath}"); String params = getConfig().getOrDefault(SCRIPT_PARAMS, "${absolutePath}");
List<String> cmdline = new ArrayList<>(); List<String> cmdline = new ArrayList<>();
cmdline.add(script); cmdline.add(script);
String replacedParams = fillInPlaceHolders(params, rec, config); String replacedParams = fillInPlaceHolders(params, ctx);
Arrays.stream(replacedParams.split(" ")).forEach(cmdline::add); Arrays.stream(replacedParams.split(" ")).forEach(cmdline::add);
return cmdline; return cmdline;
} }

View File

@ -2,10 +2,7 @@ package ctbrec.recorder.postprocessing;
import java.io.IOException; import java.io.IOException;
import ctbrec.Config;
import ctbrec.NotImplementedExcetion; import ctbrec.NotImplementedExcetion;
import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
public class Webhook extends AbstractPlaceholderAwarePostProcessor { public class Webhook extends AbstractPlaceholderAwarePostProcessor {
@ -21,7 +18,7 @@ public class Webhook extends AbstractPlaceholderAwarePostProcessor {
} }
@Override @Override
public boolean postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException { public boolean postprocess(PostProcessingContext ctx) throws IOException, InterruptedException {
throw new NotImplementedExcetion(); throw new NotImplementedExcetion();
} }

View File

@ -37,19 +37,19 @@ public class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest {
@Test @Test
public void testModelNameReplacement() { public void testModelNameReplacement() {
String input = "asdf_${modelName}_asdf"; 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"; 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"; 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 @Test
public void testSiteNameReplacement() { public void testSiteNameReplacement() {
String input = "asdf_${siteName}_asdf"; 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"; 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 @Test
@ -60,7 +60,7 @@ public class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest {
.withZone(ZoneOffset.UTC) .withZone(ZoneOffset.UTC)
.format(rec.getStartDate()); .format(rec.getStartDate());
String input = "asdf_${utcDateTime}_asdf"; 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 // with user defined pattern
date = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss") date = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")
@ -68,7 +68,7 @@ public class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest {
.withZone(ZoneOffset.UTC) .withZone(ZoneOffset.UTC)
.format(rec.getStartDate()); .format(rec.getStartDate());
input = "asdf_${utcDateTime(yyyyMMdd-HHmmss)}_asdf"; 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 // multiple occurences with user defined patterns
date = DateTimeFormatter.ofPattern("yyyy-MM-dd/yyyy") date = DateTimeFormatter.ofPattern("yyyy-MM-dd/yyyy")
@ -76,7 +76,7 @@ public class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest {
.withZone(ZoneOffset.UTC) .withZone(ZoneOffset.UTC)
.format(rec.getStartDate()); .format(rec.getStartDate());
input = "asdf_${utcDateTime(yyyy)}-${utcDateTime(MM)}-${utcDateTime(dd)}/${utcDateTime(yyyy)}_asdf"; 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 @Test
@ -86,50 +86,68 @@ public class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest {
.withZone(ZoneId.systemDefault()) .withZone(ZoneId.systemDefault())
.format(rec.getStartDate()); .format(rec.getStartDate());
String input = "asdf_${localDateTime}_asdf"; 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") date = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")
.withLocale(Locale.US) .withLocale(Locale.US)
.withZone(ZoneId.systemDefault()) .withZone(ZoneId.systemDefault())
.format(rec.getStartDate()); .format(rec.getStartDate());
input = "asdf_${localDateTime(yyyyMMdd-HHmmss)}_asdf"; 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 @Test
public void testEpochReplacement() { public void testEpochReplacement() {
long epoch = now.toEpochMilli() / 1000; long epoch = now.toEpochMilli() / 1000;
String input = "asdf_${epochSecond}_asdf"; 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 @Test
public void testFileSuffixReplacement() { public void testFileSuffixReplacement() {
String input = "asdf_${fileSuffix}_asdf"; 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 @Test
public void testRecordingsDirReplacement() { public void testRecordingsDirReplacement() {
String input = "asdf_${recordingsDir}_asdf"; 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 @Test
public void testAbsolutePathReplacement() { public void testAbsolutePathReplacement() {
String input = "asdf_${absolutePath}_asdf"; 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 @Test
public void testAbsoluteParentPathReplacement() { public void testAbsoluteParentPathReplacement() {
String input = "asdf_${absoluteParentPath}_asdf"; 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 @Test
public void testModelNotesReplacement() { public void testModelNotesReplacement() {
String input = "asdf_${modelNotes}_asdf"; 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));
} }
} }

View File

@ -18,6 +18,7 @@ import org.mockito.MockedStatic;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.Recording;
import ctbrec.Settings; import ctbrec.Settings;
import ctbrec.recorder.RecordingManager; import ctbrec.recorder.RecordingManager;
import ctbrec.sites.Site; import ctbrec.sites.Site;
@ -81,4 +82,12 @@ public abstract class AbstractPpTest {
settings.recordingsDir = recDir.toString(); settings.recordingsDir = recDir.toString();
return settings; 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;
}
} }

View File

@ -20,7 +20,7 @@ public class CopyTest extends AbstractPpTest {
rec.setStartDate(now); rec.setStartDate(now);
rec.setSingleFile(false); rec.setSingleFile(false);
Copy pp = new Copy(); Copy pp = new Copy();
pp.postprocess(rec, recordingManager, config); pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
assertNotEquals(rec.getAbsoluteFile(), rec.getPostProcessedFile()); assertNotEquals(rec.getAbsoluteFile(), rec.getPostProcessedFile());
assertTrue(original.exists()); assertTrue(original.exists());
@ -36,7 +36,7 @@ public class CopyTest extends AbstractPpTest {
rec.setStartDate(now); rec.setStartDate(now);
rec.setSingleFile(false); rec.setSingleFile(false);
Copy pp = new Copy(); Copy pp = new Copy();
pp.postprocess(rec, recordingManager, config); pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
assertNotEquals(rec.getAbsoluteFile(), rec.getPostProcessedFile()); assertNotEquals(rec.getAbsoluteFile(), rec.getPostProcessedFile());
assertTrue(originalDir.exists()); assertTrue(originalDir.exists());

View File

@ -23,7 +23,7 @@ public class DeleteOriginalTest extends AbstractPpTest {
Config config = mockConfig(); Config config = mockConfig();
DeleteOriginal pp = new DeleteOriginal(); DeleteOriginal pp = new DeleteOriginal();
pp.postprocess(rec, null, config); pp.postprocess(createPostProcessingContext(rec, null, config));
assertEquals(postProcessed, rec.getAbsoluteFile()); assertEquals(postProcessed, rec.getAbsoluteFile());
assertTrue(rec.getAbsoluteFile().exists()); assertTrue(rec.getAbsoluteFile().exists());
@ -42,7 +42,7 @@ public class DeleteOriginalTest extends AbstractPpTest {
Config config = mockConfig(); Config config = mockConfig();
Files.createDirectories(postProcessedDir.toPath()); Files.createDirectories(postProcessedDir.toPath());
DeleteOriginal pp = new DeleteOriginal(); DeleteOriginal pp = new DeleteOriginal();
pp.postprocess(rec, null, config); pp.postprocess(createPostProcessingContext(rec, null, config));
assertEquals(postProcessedDir, rec.getAbsoluteFile()); assertEquals(postProcessedDir, rec.getAbsoluteFile());
assertTrue(rec.getAbsoluteFile().exists()); assertTrue(rec.getAbsoluteFile().exists());

View File

@ -42,7 +42,7 @@ public class DeleteTooShortTest extends AbstractPpTest {
DeleteTooShort pp = new DeleteTooShort(); DeleteTooShort pp = new DeleteTooShort();
pp.getConfig().put(DeleteTooShort.MIN_LEN_IN_SECS, "10"); 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(rec.getAbsoluteFile().exists());
assertFalse(original.exists()); assertFalse(original.exists());
@ -66,7 +66,7 @@ public class DeleteTooShortTest extends AbstractPpTest {
DeleteTooShort pp = new DeleteTooShort(); DeleteTooShort pp = new DeleteTooShort();
pp.getConfig().put(DeleteTooShort.MIN_LEN_IN_SECS, "0"); 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(rec.getAbsoluteFile().exists());
assertTrue(original.exists()); assertTrue(original.exists());
@ -83,7 +83,7 @@ public class DeleteTooShortTest extends AbstractPpTest {
DeleteTooShort pp = new DeleteTooShort(); DeleteTooShort pp = new DeleteTooShort();
pp.getConfig().put(DeleteTooShort.MIN_LEN_IN_SECS, "1"); 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(rec.getAbsoluteFile().exists());
assertTrue(original.exists()); assertTrue(original.exists());

View File

@ -28,7 +28,7 @@ public class MoveDirectoryTest extends AbstractPpTest {
rec.setSingleFile(false); rec.setSingleFile(false);
Move pp = new Move(); Move pp = new Move();
pp.getConfig().put(Move.PATH_TEMPLATE, new File(baseDir.toFile(), Move.DEFAULT).getAbsolutePath()); 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()); 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()); assertTrue(m.matches());
@ -50,6 +50,6 @@ public class MoveDirectoryTest extends AbstractPpTest {
Move pp = new Move(); Move pp = new Move();
Config config = mockConfig(); Config config = mockConfig();
pp.getConfig().put(Move.PATH_TEMPLATE, new File(baseDir.toFile(), Move.DEFAULT).getAbsolutePath()); pp.getConfig().put(Move.PATH_TEMPLATE, new File(baseDir.toFile(), Move.DEFAULT).getAbsolutePath());
pp.postprocess(rec, recordingManager, config); pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
} }
} }

View File

@ -28,7 +28,7 @@ public class MoveSingleFileTest extends AbstractPpTest {
Move pp = new Move(); Move pp = new Move();
pp.getConfig().put(Move.PATH_TEMPLATE, new File(baseDir.toFile(), Move.DEFAULT).getAbsolutePath()); 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()); 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()); assertTrue(m.matches());
@ -48,7 +48,7 @@ public class MoveSingleFileTest extends AbstractPpTest {
Move pp = new Move(); Move pp = new Move();
Config config = mockConfig(); Config config = mockConfig();
pp.getConfig().put(Move.PATH_TEMPLATE, original.getParentFile().getCanonicalPath()); pp.getConfig().put(Move.PATH_TEMPLATE, original.getParentFile().getCanonicalPath());
pp.postprocess(rec, recordingManager, config); pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
} }
@Test @Test
@ -63,7 +63,7 @@ public class MoveSingleFileTest extends AbstractPpTest {
Move pp = new Move(); Move pp = new Move();
Config config = mockConfig(); Config config = mockConfig();
pp.getConfig().put(Move.PATH_TEMPLATE, new File(baseDir.toFile(), Move.DEFAULT).getAbsolutePath()); 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 @Test

View File

@ -25,9 +25,9 @@ public class RemoveKeepFileTest extends AbstractPpTest {
Config config = mockConfig(); Config config = mockConfig();
RecordingManager rm = new RecordingManager(config, Collections.emptyList()); RecordingManager rm = new RecordingManager(config, Collections.emptyList());
rm.add(rec); rm.add(rec);
assertTrue(rm.getAll().size() == 1); assertEquals(1, rm.getAll().size());
RemoveKeepFile pp = new RemoveKeepFile(); RemoveKeepFile pp = new RemoveKeepFile();
pp.postprocess(rec, rm, config); pp.postprocess(createPostProcessingContext(rec, rm, config));
assertTrue(rec.getAbsoluteFile().exists()); assertTrue(rec.getAbsoluteFile().exists());
assertTrue(rec.getPostProcessedFile().exists()); assertTrue(rec.getPostProcessedFile().exists());

View File

@ -26,7 +26,7 @@ public class RenameDirectoryTest extends AbstractPpTest {
rec.setStartDate(now); rec.setStartDate(now);
rec.setSingleFile(false); rec.setSingleFile(false);
Rename pp = new Rename(); 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()); 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()); assertTrue(m.matches());
@ -47,6 +47,6 @@ public class RenameDirectoryTest extends AbstractPpTest {
Files.createDirectories(postProcessedDir.toPath()); Files.createDirectories(postProcessedDir.toPath());
Rename pp = new Rename(); Rename pp = new Rename();
pp.postprocess(rec, recordingManager, config); pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
} }
} }

View File

@ -25,7 +25,7 @@ public class RenameSingleFileTest extends AbstractPpTest {
rec.setStartDate(now); rec.setStartDate(now);
rec.setSingleFile(true); rec.setSingleFile(true);
Rename pp = new Rename(); 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()); 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()); assertTrue(m.matches());
@ -45,7 +45,7 @@ public class RenameSingleFileTest extends AbstractPpTest {
doThrow(new RuntimeException("Unexpected call of setAbsoluteFile")).when(rec).setAbsoluteFile(any()); doThrow(new RuntimeException("Unexpected call of setAbsoluteFile")).when(rec).setAbsoluteFile(any());
Rename pp = new Rename(); Rename pp = new Rename();
pp.getConfig().put(Rename.FILE_NAME_TEMPLATE, original.getName()); pp.getConfig().put(Rename.FILE_NAME_TEMPLATE, original.getName());
pp.postprocess(rec, recordingManager, config); pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
} }
@Test @Test
@ -59,7 +59,7 @@ public class RenameSingleFileTest extends AbstractPpTest {
when(rec.getStartDate()).thenReturn(now); when(rec.getStartDate()).thenReturn(now);
doThrow(new RuntimeException("Unexpected call of setAbsoluteFile")).when(rec).setAbsoluteFile(any()); doThrow(new RuntimeException("Unexpected call of setAbsoluteFile")).when(rec).setAbsoluteFile(any());
Rename pp = new Rename(); Rename pp = new Rename();
pp.postprocess(rec, recordingManager, config); pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
} }
@Test @Test

View File

@ -10,6 +10,8 @@ import java.time.Instant;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@ -25,11 +27,13 @@ import com.squareup.moshi.Moshi;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.GlobalThreadPool; import ctbrec.GlobalThreadPool;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.ModelGroup;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.io.BandwidthMeter; import ctbrec.io.BandwidthMeter;
import ctbrec.io.FileJsonAdapter; import ctbrec.io.FileJsonAdapter;
import ctbrec.io.InstantJsonAdapter; import ctbrec.io.InstantJsonAdapter;
import ctbrec.io.ModelJsonAdapter; import ctbrec.io.ModelJsonAdapter;
import ctbrec.io.UuidJSonAdapter;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.sites.Site; import ctbrec.sites.Site;
@ -67,6 +71,7 @@ public class RecorderServlet extends AbstractCtbrecServlet {
.add(Instant.class, new InstantJsonAdapter()) .add(Instant.class, new InstantJsonAdapter())
.add(Model.class, new ModelJsonAdapter(sites)) .add(Model.class, new ModelJsonAdapter(sites))
.add(File.class, new FileJsonAdapter()) .add(File.class, new FileJsonAdapter())
.add(UUID.class, new UuidJSonAdapter())
.build(); .build();
JsonAdapter<Request> requestAdapter = moshi.adapter(Request.class); JsonAdapter<Request> requestAdapter = moshi.adapter(Request.class);
Request request = requestAdapter.fromJson(json); Request request = requestAdapter.fromJson(json);
@ -234,6 +239,17 @@ public class RecorderServlet extends AbstractCtbrecServlet {
response = "{\"status\": \"success\"}"; response = "{\"status\": \"success\"}";
resp.getWriter().write(response); resp.getWriter().write(response);
break; 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: default:
resp.setStatus(SC_BAD_REQUEST); resp.setStatus(SC_BAD_REQUEST);
response = "{\"status\": \"error\", \"msg\": \"Unknown action ["+request.action+"]\"}"; 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 { private void startByUrl(Request request) throws InvalidKeyException, NoSuchAlgorithmException, IOException {
String url = request.model.getUrl(); String url = request.model.getUrl();
for (Site site : sites) { for (Site site : sites) {
@ -291,5 +314,6 @@ public class RecorderServlet extends AbstractCtbrecServlet {
public String action; public String action;
public Model model; public Model model;
public Recording recording; public Recording recording;
public ModelGroup modelGroup;
} }
} }