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.HelpTab;
import ctbrec.ui.tabs.RecentlyWatchedTab;
import ctbrec.ui.tabs.RecordedTab;
import ctbrec.ui.tabs.RecordingsTab;
import ctbrec.ui.tabs.SiteTab;
import ctbrec.ui.tabs.TabSelectionListener;
import ctbrec.ui.tabs.UpdateTab;
import ctbrec.ui.tabs.logging.LoggingTab;
import ctbrec.ui.tabs.recorded.RecordedTab;
import javafx.application.Application;
import javafx.application.HostServices;
import javafx.application.Platform;

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.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
/**
* Just a wrapper for Model, which augments it with JavaFX value binding properties, so that UI widgets get updated proeprly
*/
public class JavaFxModel implements Model {
private transient StringProperty onlineProperty = new SimpleStringProperty();
private transient StringProperty recordingProperty = new SimpleStringProperty();
private transient BooleanProperty onlineProperty = new SimpleBooleanProperty();
private transient BooleanProperty recordingProperty = new SimpleBooleanProperty();
private transient BooleanProperty pausedProperty = new SimpleBooleanProperty();
private transient SimpleIntegerProperty priorityProperty = new SimpleIntegerProperty();
private transient SimpleObjectProperty<Instant> lastSeenProperty = new SimpleObjectProperty<>();
@ -105,14 +103,22 @@ public class JavaFxModel implements Model {
return delegate.toString();
}
public StringProperty getOnlineProperty() {
public BooleanProperty getOnlineProperty() {
return onlineProperty;
}
public StringProperty getRecordingProperty() {
public void setOnlineProperty(boolean online) {
this.onlineProperty.set(online);
}
public BooleanProperty getRecordingProperty() {
return recordingProperty;
}
public void setRecordingProperty(boolean recording) {
this.recordingProperty.setValue(recording);
}
public BooleanProperty getPausedProperty() {
return pausedProperty;
}
@ -329,6 +335,4 @@ public class JavaFxModel implements Model {
public void setMarkedForLaterRecording(boolean marked) {
delegate.setMarkedForLaterRecording(marked);
}
}

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.util.Optional;
import java.util.Set;
import java.util.UUID;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.ModelGroup;
import ctbrec.StringUtil;
import ctbrec.ui.AutosizeAlert;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Dialog;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.image.Image;
import javafx.scene.layout.GridPane;
@ -46,7 +56,7 @@ public class Dialogs {
}
alert.setContentText(content);
if (parent != null) {
Stage stage = (Stage) alert.getDialogPane().getScene().getWindow();
var stage = (Stage) alert.getDialogPane().getScene().getWindow();
stage.getScene().getStylesheets().addAll(parent.getStylesheets());
}
alert.showAndWait();
@ -63,22 +73,22 @@ public class Dialogs {
Dialog<String> dialog = new Dialog<>();
dialog.setTitle(title);
dialog.setHeaderText(header);
dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
dialog.getDialogPane().getButtonTypes().addAll(OK, CANCEL);
dialog.initModality(Modality.APPLICATION_MODAL);
dialog.setResizable(true);
InputStream icon = Dialogs.class.getResourceAsStream("/icon.png");
Stage stage = (Stage) dialog.getDialogPane().getScene().getWindow();
var stage = (Stage) dialog.getDialogPane().getScene().getWindow();
stage.getIcons().add(new Image(icon));
if (parent != null) {
stage.getScene().getStylesheets().addAll(parent.getStylesheets());
}
GridPane grid = new GridPane();
var grid = new GridPane();
grid.setHgap(10);
grid.setVgap(10);
grid.setPadding(new Insets(20, 150, 10, 10));
TextArea notes = new TextArea(text);
var notes = new TextArea(text);
notes.setPrefRowCount(3);
grid.add(notes, 0, 0);
dialog.getDialogPane().setContent(grid);
@ -86,7 +96,7 @@ public class Dialogs {
Platform.runLater(notes::requestFocus);
dialog.setResultConverter(dialogButton -> {
if (dialogButton == ButtonType.OK) {
if (dialogButton == OK) {
return notes.getText();
}
return null;
@ -98,31 +108,31 @@ public class Dialogs {
public static Boolean showCustomInput(Scene parent, String title, Region region) {
Dialog<?> dialog = new Dialog<>();
dialog.setTitle(title);
dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
dialog.getDialogPane().getButtonTypes().addAll(OK, CANCEL);
dialog.initModality(Modality.APPLICATION_MODAL);
dialog.setResizable(true);
InputStream icon = Dialogs.class.getResourceAsStream("/icon.png");
Stage stage = (Stage) dialog.getDialogPane().getScene().getWindow();
var stage = (Stage) dialog.getDialogPane().getScene().getWindow();
stage.getIcons().add(new Image(icon));
if (parent != null) {
stage.getScene().getStylesheets().addAll(parent.getStylesheets());
}
dialog.getDialogPane().setContent(region);
dialog.showAndWait();
return dialog.getResult() == ButtonType.OK;
return dialog.getResult() == OK;
}
public static boolean showConfirmDialog(String title, String message, String header, Scene parent) {
AutosizeAlert confirm = new AutosizeAlert(AlertType.CONFIRMATION, message, parent, YES, NO);
var confirm = new AutosizeAlert(AlertType.CONFIRMATION, message, parent, YES, NO);
confirm.setTitle(title);
confirm.setHeaderText(header);
confirm.showAndWait();
return confirm.getResult() == ButtonType.YES;
return confirm.getResult() == YES;
}
public static ButtonType showShutdownDialog(Scene parent) {
String message = "There are recordings in progress";
AutosizeAlert confirm = new AutosizeAlert(AlertType.CONFIRMATION, "", parent, YES, FINISH, NO);
var message = "There are recordings in progress";
var confirm = new AutosizeAlert(AlertType.CONFIRMATION, "", parent, YES, FINISH, NO);
confirm.setTitle("Shutdown");
confirm.setHeaderText(message);
((Button) confirm.getDialogPane().lookupButton(ButtonType.YES)).setText("Shutdown Now");
@ -134,4 +144,36 @@ public class Dialogs {
confirm.showAndWait();
return confirm.getResult();
}
public static Optional<ModelGroup> showModelGroupSelectionDialog(Scene parent, Model model) {
var dialogPane = new GridPane();
Set<ModelGroup> modelGroups = Config.getInstance().getSettings().modelGroups;
ObservableList<ModelGroup> comboBoxModel = FXCollections.observableArrayList(modelGroups);
ComboBox<ModelGroup> comboBox = new ComboBox<>(comboBoxModel);
comboBox.setEditable(true);
comboBox.setPlaceholder(new Label(" type in a name to a add a new group "));
dialogPane.add(new Label("Model group"), 0, 0);
dialogPane.add(comboBox, 1, 0);
boolean ok = showCustomInput(parent, "Add model to group", dialogPane);
if (ok) {
String text = comboBox.getEditor().getText();
if (StringUtil.isBlank(text)) {
return Optional.empty();
}
Optional<ModelGroup> existingGroup = modelGroups.stream().filter(mg -> mg.getName().equalsIgnoreCase(text)).findFirst();
if (existingGroup.isPresent()) {
existingGroup.get().add(model);
return existingGroup;
} else {
var group = new ModelGroup();
group.setId(UUID.randomUUID());
group.setName(text);
group.add(model);
modelGroups.add(group);
return Optional.of(group);
}
} else {
return Optional.empty();
}
}
}

View File

@ -2,10 +2,8 @@ package ctbrec.ui.tabs;
import java.io.IOException;
import ctbrec.Config;
import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
import ctbrec.recorder.postprocessing.AbstractPlaceholderAwarePostProcessor;
import ctbrec.recorder.postprocessing.PostProcessingContext;
public class DownloadPostprocessor extends AbstractPlaceholderAwarePostProcessor {
@ -15,7 +13,7 @@ public class DownloadPostprocessor extends AbstractPlaceholderAwarePostProcessor
}
@Override
public boolean postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException {
public boolean postprocess(PostProcessingContext ctx) throws IOException, InterruptedException {
// nothing really to do in here, we just inherit from AbstractPlaceholderAwarePostProcessor to use fillInPlaceHolders
return true;
}

View File

@ -40,6 +40,7 @@ import ctbrec.io.UrlUtil;
import ctbrec.recorder.ProgressListener;
import ctbrec.recorder.Recorder;
import ctbrec.recorder.RecordingPinnedException;
import ctbrec.recorder.postprocessing.PostProcessingContext;
import ctbrec.ui.AutosizeAlert;
import ctbrec.ui.CamrecApplication;
import ctbrec.ui.DesktopIntegration;
@ -688,7 +689,10 @@ public class RecordingsTab extends Tab implements TabSelectionListener, Shutdown
} else {
String downloadFilename = config.getSettings().downloadFilename;
String fileSuffix = config.getSettings().ffmpegFileSuffix;
return new DownloadPostprocessor().fillInPlaceHolders(downloadFilename, recording, config) + '.' + fileSuffix;
PostProcessingContext ctx = new PostProcessingContext();
ctx.setRecording(recording);
ctx.setConfig(config);
return new DownloadPostprocessor().fillInPlaceHolders(downloadFilename, ctx) + '.' + fileSuffix;
}
}

View File

@ -4,6 +4,8 @@ import static ctbrec.ui.controls.Dialogs.*;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
@ -29,6 +31,7 @@ import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.GlobalThreadPool;
import ctbrec.Model;
import ctbrec.ModelGroup;
import ctbrec.event.EventBusHolder;
import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;
@ -39,10 +42,13 @@ import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.SiteUiFactory;
import ctbrec.ui.TipDialog;
import ctbrec.ui.TokenLabel;
import ctbrec.ui.action.AddToGroupAction;
import ctbrec.ui.action.EditGroupAction;
import ctbrec.ui.action.IgnoreModelsAction;
import ctbrec.ui.action.OpenRecordingsDir;
import ctbrec.ui.action.SetStopDateAction;
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.controls.FasterVerticalScrollPaneSkin;
import ctbrec.ui.controls.SearchBox;
import ctbrec.ui.controls.SearchPopover;
@ -467,52 +473,57 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
}
private ContextMenu createContextMenu(ThumbCell cell) {
Model model = cell.getModel();
var model = cell.getModel();
boolean modelIsTrackedByRecorder = recorder.isTracked(model);
MenuItem openInPlayer = new MenuItem("Open in Player");
var openInPlayer = new MenuItem("Open in Player");
openInPlayer.setOnAction(e -> startPlayer(getSelectedThumbCells(cell)));
MenuItem start = new MenuItem("Start Recording");
var start = new MenuItem("Start Recording");
start.setOnAction(e -> startStopAction(getSelectedThumbCells(cell), true));
MenuItem stop = new MenuItem("Stop Recording");
var stop = new MenuItem("Stop Recording");
stop.setOnAction(e -> startStopAction(getSelectedThumbCells(cell), false));
MenuItem startStop = recorder.isTracked(model) ? stop : start;
var startStop = recorder.isTracked(model) ? stop : start;
MenuItem recordUntil = new MenuItem("Start Recording Until");
var recordUntil = new MenuItem("Start Recording Until");
recordUntil.setOnAction(e -> startRecordingWithTimeLimit(getSelectedThumbCells(cell)));
MenuItem addPaused = new MenuItem("Add in paused state");
var addPaused = new MenuItem("Add in paused state");
addPaused.setOnAction(e -> addPaused(getSelectedThumbCells(cell)));
MenuItem recordLater = new MenuItem("Record Later");
var recordLater = new MenuItem("Record Later");
recordLater.setOnAction(e -> recordLater(getSelectedThumbCells(cell), true));
MenuItem removeRecordLater = new MenuItem("Forget Model");
var removeRecordLater = new MenuItem("Forget Model");
removeRecordLater.setOnAction(e -> recordLater(getSelectedThumbCells(cell), false));
MenuItem addRemoveBookmark = recorder.isMarkedForLaterRecording(model) ? removeRecordLater : recordLater;
var addRemoveBookmark = recorder.isMarkedForLaterRecording(model) ? removeRecordLater : recordLater;
MenuItem pause = new MenuItem("Pause Recording");
var pause = new MenuItem("Pause Recording");
pause.setOnAction(e -> pauseResumeAction(getSelectedThumbCells(cell), true));
MenuItem resume = new MenuItem("Resume Recording");
var resume = new MenuItem("Resume Recording");
resume.setOnAction(e -> pauseResumeAction(getSelectedThumbCells(cell), false));
MenuItem pauseResume = recorder.isSuspended(model) ? resume : pause;
var pauseResume = recorder.isSuspended(model) ? resume : pause;
MenuItem follow = new MenuItem("Follow");
var follow = new MenuItem("Follow");
follow.setOnAction(e -> follow(getSelectedThumbCells(cell), true));
MenuItem unfollow = new MenuItem("Unfollow");
var unfollow = new MenuItem("Unfollow");
unfollow.setOnAction(e -> follow(getSelectedThumbCells(cell), false));
MenuItem ignore = new MenuItem("Ignore");
var addToGroup = new MenuItem("Add to group");
addToGroup.setOnAction(e -> addToGroup(model));
var editGroup = new MenuItem("Edit group");
editGroup.setOnAction(e -> editGroup(model));
var ignore = new MenuItem("Ignore");
ignore.setOnAction(e -> ignore(getSelectedThumbCells(cell)));
MenuItem refresh = new MenuItem("Refresh Overview");
var refresh = new MenuItem("Refresh Overview");
refresh.setOnAction(e -> refresh());
MenuItem openRecDir = new MenuItem("Open recording directory");
var openRecDir = new MenuItem("Open recording directory");
openRecDir.setOnAction(e -> new OpenRecordingsDir(cell, model).execute());
MenuItem copyUrl = createCopyUrlMenuItem(cell);
MenuItem openInBrowser = createOpenInBrowser(cell);
MenuItem sendTip = createTipMenuItem(cell);
var copyUrl = createCopyUrlMenuItem(cell);
var openInBrowser = createOpenInBrowser(cell);
var sendTip = createTipMenuItem(cell);
configureItemsForSelection(cell, openInPlayer, copyUrl, sendTip);
@ -528,16 +539,18 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
}
contextMenu.getItems().add(new SeparatorMenuItem());
if (site.supportsFollow()) {
MenuItem followOrUnFollow = (this instanceof FollowedTab) ? unfollow : follow;
var followOrUnFollow = (this instanceof FollowedTab) ? unfollow : follow;
followOrUnFollow.setDisable(!site.credentialsAvailable());
contextMenu.getItems().add(followOrUnFollow);
}
if (site.supportsTips()) {
contextMenu.getItems().add(sendTip);
}
Optional<ModelGroup> modelGroup = getModelGroup(model);
contextMenu.getItems().add(modelGroup.isEmpty() ? addToGroup : editGroup);
contextMenu.getItems().addAll(copyUrl, openInBrowser, ignore, refresh, openRecDir);
if (model instanceof MyFreeCamsModel && Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
MenuItem debug = new MenuItem("debug");
var debug = new MenuItem("debug");
debug.setOnAction(e -> MyFreeCamsClient.getInstance().getSessionState(model));
contextMenu.getItems().add(debug);
}
@ -545,6 +558,23 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
return contextMenu;
}
private Optional<ModelGroup> getModelGroup(Model model) {
try {
return recorder.getModelGroup(model);
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
Dialogs.showError(grid.getScene(), "Error", "Couldn't get model group for model " + model, e);
return Optional.empty();
}
}
private void editGroup(Model model) {
new EditGroupAction(this.getContent(), recorder, model).execute();
}
private void addToGroup(Model model) {
new AddToGroupAction(this.getContent(), recorder, model).execute();
}
private void recordLater(List<ThumbCell> list, boolean recordLater) {
for (ThumbCell cell : list) {
cell.recordLater(recordLater);

View File

@ -1,5 +1,13 @@
package ctbrec.ui.tabs;
import static ctbrec.ui.controls.Dialogs.*;
import java.io.IOException;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Model;
import ctbrec.sites.Site;
import ctbrec.ui.SiteUiFactory;
@ -7,13 +15,6 @@ import ctbrec.ui.controls.SearchPopover;
import ctbrec.ui.controls.SearchPopoverTreeList;
import javafx.application.Platform;
import javafx.concurrent.Task;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.List;
import static ctbrec.ui.controls.Dialogs.showError;
public class ThumbOverviewTabSearchTask extends Task<List<Model>> {

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.security.InvalidKeyException;
@ -37,6 +37,7 @@ import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.controls.SearchBox;
import ctbrec.ui.controls.autocomplete.AutoFillTextField;
import ctbrec.ui.controls.autocomplete.ObservableListSuggester;
import ctbrec.ui.tabs.TabSelectionListener;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringPropertyBase;

View File

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

View File

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

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" />
###### Available variables:
- **${modelName}** - the name of the recorded model
- **${modelDisplayName}** - the name of the recorded model, which is shown on the webpage. Might be the same as
${modelName}
- **${modelSanitizedName}** - sanitized name of the model. The following characters are replaced by an underscore:
\\, /, ', " and space
- **${modelGroupName}** - name of the model group, if the model is part of a group
- **${modelGroupId}** - the unique ID of the model group, if the model is part of a group
- **${siteName}** - the name of the cam site, the model streams on
- **${siteSanitizedName}** - sanitized name of the site. The following characters are replaced by an underscore:
\\, /, ', " and space
@ -116,4 +119,15 @@ The part you have to copy is
</table>
For more information see: [DateTimeFormatter](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html)
For more information see: [DateTimeFormatter](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html)
###### Fallback values:
You can define a fallback value for each variable in case there is no value available for the variable. The syntax is
${placeholder?foobar}
Let's for example say you have created some model groups. For models, which are part of a group, you want to use the group name. But for models, which
are not part of a group you want to use the sanitized name. You can achieve that by using the following expression:
${modelGroupName?${modelSanitizedName}}
It can be read like "use the modelGroupName, but if that is not available use modelSanitizedName".

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

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

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.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
@ -45,6 +46,7 @@ import com.google.common.eventbus.Subscribe;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.ModelGroup;
import ctbrec.Recording;
import ctbrec.Recording.State;
import ctbrec.event.Event;
@ -54,6 +56,7 @@ import ctbrec.event.NoSpaceLeftEvent;
import ctbrec.event.RecordingStateChangedEvent;
import ctbrec.io.HttpClient;
import ctbrec.recorder.download.Download;
import ctbrec.recorder.postprocessing.PostProcessingContext;
import ctbrec.recorder.postprocessing.PostProcessor;
import ctbrec.sites.Site;
@ -88,18 +91,7 @@ public class NextGenLocalRecorder implements Recorder {
downloadPool = Executors.newScheduledThreadPool(5, createThreadFactory("Download", MAX_PRIORITY));
threadPoolScaler = new ThreadPoolScaler((ThreadPoolExecutor) downloadPool, 5);
recordingManager = new RecordingManager(config, sites);
config.getSettings().models.stream().forEach(m -> {
if (m.getSite() != null) {
if (m.getSite().isEnabled()) {
models.add(m);
} else {
LOG.info("{} disabled -> ignoring {}", m.getSite().getName(), m.getName());
}
} else {
LOG.info("Site for model {} is unknown -> ignoring", m.getName());
}
});
loadModels();
int ppThreads = config.getSettings().postProcessingThreads;
ppPool = new ThreadPoolExecutor(ppThreads, ppThreads, 5, TimeUnit.MINUTES, ppQueue, createThreadFactory("PP", MIN_PRIORITY));
@ -127,6 +119,20 @@ public class NextGenLocalRecorder implements Recorder {
}, 1, 1, TimeUnit.SECONDS);
}
private void loadModels() {
config.getSettings().models.stream().forEach(m -> {
if (m.getSite() != null) {
if (m.getSite().isEnabled()) {
models.add(m);
} else {
LOG.info("{} disabled -> ignoring {}", m.getSite().getName(), m.getName());
}
} else {
LOG.info("Site for model {} is unknown -> ignoring", m.getName());
}
});
}
private void startCompletionHandler() {
downloadCompletionPool.submit(() -> {
while (!Thread.currentThread().isInterrupted()) {
@ -209,9 +215,10 @@ public class NextGenLocalRecorder implements Recorder {
recordingManager.saveRecording(recording);
recording.postprocess();
List<PostProcessor> postProcessors = config.getSettings().postProcessors;
PostProcessingContext ctx = createPostProcessingContext(recording);
for (PostProcessor postProcessor : postProcessors) {
LOG.debug("Running post-processor: {}", postProcessor.getName());
boolean continuePP = postProcessor.postprocess(recording, recordingManager, config);
boolean continuePP = postProcessor.postprocess(ctx);
if (!continuePP) {
break;
}
@ -237,6 +244,15 @@ public class NextGenLocalRecorder implements Recorder {
});
}
private PostProcessingContext createPostProcessingContext(Recording recording) {
PostProcessingContext ctx = new PostProcessingContext();
ctx.setConfig(config);
ctx.setRecorder(this);
ctx.setRecording(recording);
ctx.setRecordingManager(recordingManager);
return ctx;
}
private void setRecordingStatus(Recording recording, State status) {
recording.setStatus(status);
RecordingStateChangedEvent evt = new RecordingStateChangedEvent(recording.getDownload().getTarget(), status, recording.getModel(),
@ -761,4 +777,23 @@ public class NextGenLocalRecorder implements Recorder {
public int getModelCount() {
return (int) models.stream().filter(m -> !m.isMarkedForLaterRecording()).count();
}
@Override
public Set<ModelGroup> getModelGroups() {
return config.getSettings().modelGroups;
}
@Override
public void saveModelGroup(ModelGroup group) throws IOException {
Set<ModelGroup> modelGroups = config.getSettings().modelGroups;
modelGroups.remove(group);
modelGroups.add(group);
config.save();
}
@Override
public void deleteModelGroup(ModelGroup group) throws IOException {
config.getSettings().modelGroups.remove(group);
config.save();
}
}

View File

@ -1,15 +1,18 @@
package ctbrec.recorder;
import ctbrec.Model;
import ctbrec.Recording;
import ctbrec.io.HttpClient;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import ctbrec.Model;
import ctbrec.ModelGroup;
import ctbrec.Recording;
import ctbrec.io.HttpClient;
public interface Recorder {
public void addModel(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException;
@ -143,5 +146,29 @@ public interface Recorder {
*/
public void resume() throws InvalidKeyException, NoSuchAlgorithmException, IOException;
/**
* Returns the number of models, which are on the recording list and not marked for later recording
* @return
*/
public int getModelCount();
public Set<ModelGroup> getModelGroups() throws InvalidKeyException, NoSuchAlgorithmException, IOException;
/**
* Saves a model group. If the group already exists, it will be overwritten. Otherwise it will
* be saved as a new group.
* @param group
* @throws IOException
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
*/
public void saveModelGroup(ModelGroup group) throws IOException, InvalidKeyException, NoSuchAlgorithmException;
public void deleteModelGroup(ModelGroup group) throws IOException, InvalidKeyException, NoSuchAlgorithmException;
default Optional<ModelGroup> getModelGroup(Model model) throws InvalidKeyException, NoSuchAlgorithmException, IOException {
return getModelGroups().stream()
.filter(mg -> mg.getModelUrls().contains(model.getUrl()))
.findFirst();
}
}

View File

@ -2,7 +2,10 @@ package ctbrec.recorder;
import static ctbrec.recorder.NextGenLocalRecorder.*;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@ -12,6 +15,7 @@ import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.ModelGroup;
import ctbrec.Recording;
import ctbrec.recorder.download.Download;
@ -26,7 +30,7 @@ public class RecordingPreconditions {
this.recorder = recorder;
}
void check(Model model) throws IOException {
void check(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
ensureRecorderIsActive();
ensureModelIsNotSuspended(model);
ensureModelIsNotMarkedForLaterRecording(model);
@ -36,6 +40,7 @@ public class RecordingPreconditions {
ensureEnoughSpaceForRecording();
ensureDownloadSlotAvailable(model);
ensureModelIsOnline(model);
ensureNoOtherFromModelGroupIsRecording(model);
}
private void ensureModelIsOnline(Model model) {
@ -130,4 +135,55 @@ public class RecordingPreconditions {
int concurrentRecordings = Config.getInstance().getSettings().concurrentRecordings;
return concurrentRecordings == 0 || concurrentRecordings > 0 && recorder.getRecordingProcesses().size() < concurrentRecordings;
}
private void ensureNoOtherFromModelGroupIsRecording(Model model) throws InvalidKeyException, NoSuchAlgorithmException, IOException {
Optional<ModelGroup> modelGroup = recorder.getModelGroup(model);
if (modelGroup.isPresent()) {
for (String modelUrl : modelGroup.get().getModelUrls()) {
if (modelUrl.equals(model.getUrl())) {
// no other model with higher prio is online, start recording
// but before that stop all recordings of models with lower prio
stopModelsWithLowerPrio(modelGroup.get());
return;
} else {
Optional<Model> otherModel = getModelForUrl(modelUrl);
if (otherModel.isPresent() && otherModelCanBeRecorded(otherModel.get())) {
throw new PreconditionNotMetException(otherModel.get() + " from the same group is already recorded");
}
}
}
}
}
private void stopModelsWithLowerPrio(ModelGroup modelGroup) throws InvalidKeyException, NoSuchAlgorithmException, IOException {
recorder.getCurrentlyRecording().stream()
.filter(m -> modelGroup.getModelUrls().contains(m.getUrl()))
.forEach(recorder::stopRecordingProcess);
}
private Optional<Model> getModelForUrl(String modelUrl) {
return Config.getInstance().getSettings().models.stream()
.filter(m -> Objects.equals(m.getUrl(), modelUrl))
.findFirst();
}
private boolean otherModelCanBeRecorded(Model model) {
try {
ensureRecorderIsActive();
ensureModelIsNotSuspended(model);
ensureModelIsNotMarkedForLaterRecording(model);
ensureRecordUntilIsInFuture(model);
ensureModelShouldBeRecorded(model);
ensureEnoughSpaceForRecording();
ensureDownloadSlotAvailable(model);
ensureModelIsOnline(model);
return true;
} catch (PreconditionNotMetException e) {
// precondition for other model not met
} catch (IOException e) {
LOG.warn("Couldn't check if preconditions of other model from group are met. Assuming she's offline", e);
}
return false;
}
}

View File

@ -1,25 +1,5 @@
package ctbrec.recorder;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import ctbrec.Config;
import ctbrec.Hmac;
import ctbrec.Model;
import ctbrec.Recording;
import ctbrec.event.EventBusHolder;
import ctbrec.event.NoSpaceLeftEvent;
import ctbrec.event.RecordingStateChangedEvent;
import ctbrec.io.*;
import ctbrec.sites.Site;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.Request.Builder;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
@ -27,7 +7,43 @@ import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import ctbrec.Config;
import ctbrec.Hmac;
import ctbrec.Model;
import ctbrec.ModelGroup;
import ctbrec.Recording;
import ctbrec.event.EventBusHolder;
import ctbrec.event.NoSpaceLeftEvent;
import ctbrec.event.RecordingStateChangedEvent;
import ctbrec.io.BandwidthMeter;
import ctbrec.io.FileJsonAdapter;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import ctbrec.io.InstantJsonAdapter;
import ctbrec.io.ModelJsonAdapter;
import ctbrec.io.UuidJSonAdapter;
import ctbrec.sites.Site;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.Request.Builder;
import okhttp3.RequestBody;
import okhttp3.Response;
public class RemoteRecorder implements Recorder {
@ -41,16 +57,20 @@ public class RemoteRecorder implements Recorder {
.add(Instant.class, new InstantJsonAdapter())
.add(Model.class, new ModelJsonAdapter())
.add(File.class, new FileJsonAdapter())
.add(UUID.class, new UuidJSonAdapter())
.build();
private JsonAdapter<ModelListResponse> modelListResponseAdapter = moshi.adapter(ModelListResponse.class);
private JsonAdapter<RecordingListResponse> recordingListResponseAdapter = moshi.adapter(RecordingListResponse.class);
private JsonAdapter<ModelRequest> modelRequestAdapter = moshi.adapter(ModelRequest.class);
private JsonAdapter<ModelGroupRequest> modelGroupRequestAdapter = moshi.adapter(ModelGroupRequest.class);
private JsonAdapter<ModelGroupListResponse> modelGroupListResponseAdapter = moshi.adapter(ModelGroupListResponse.class);
private JsonAdapter<RecordingRequest> recordingRequestAdapter = moshi.adapter(RecordingRequest.class);
private JsonAdapter<SimpleResponse> simpleResponseAdapter = moshi.adapter(SimpleResponse.class);
private List<Model> models = Collections.emptyList();
private List<Model> onlineModels = Collections.emptyList();
private List<Recording> recordings = Collections.emptyList();
private Set<ModelGroup> modelGroups = new HashSet<>();
private List<Site> sites;
private long spaceTotal = -1;
private long spaceFree = -1;
@ -91,7 +111,7 @@ public class RemoteRecorder implements Recorder {
private Optional<String> sendRequest(String action) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
String msg = "{\"action\": \"" + action + "\"}";
LOG.debug("Sending request to recording server: {}", msg);
LOG.trace("Sending request to recording server: {}", msg);
RequestBody requestBody = RequestBody.Companion.create(msg, JSON);
Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(requestBody);
addHmacIfNeeded(msg, builder);
@ -109,7 +129,7 @@ public class RemoteRecorder implements Recorder {
private void sendRequest(String action, Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
String payload = modelRequestAdapter.toJson(new ModelRequest(action, model));
LOG.debug("Sending request to recording server: {}", payload);
LOG.trace("Sending request to recording server: {}", payload);
RequestBody body = RequestBody.Companion.create(payload, JSON);
Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
addHmacIfNeeded(payload, builder);
@ -139,6 +159,7 @@ public class RemoteRecorder implements Recorder {
String msg = recordingRequestAdapter.toJson(recReq);
RequestBody body = RequestBody.Companion.create(msg, JSON);
Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
LOG.trace("Sending request to recording server: {}", msg);
addHmacIfNeeded(msg, builder);
Request request = builder.build();
try (Response response = client.execute(request)) {
@ -158,6 +179,33 @@ public class RemoteRecorder implements Recorder {
}
}
private void sendRequest(String action, ModelGroup model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
String payload = modelGroupRequestAdapter.toJson(new ModelGroupRequest(action, model));
LOG.trace("Sending request to recording server: {}", payload);
RequestBody body = RequestBody.Companion.create(payload, JSON);
Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
addHmacIfNeeded(payload, builder);
Request request = builder.build();
try (Response response = client.execute(request)) {
if (response.isSuccessful()) {
String json = response.body().string();
updateModelGroups(json);
} else {
throw new HttpException(response.code(), response.message());
}
}
}
private void updateModelGroups(String responseBody) throws IOException {
ModelGroupListResponse resp = modelGroupListResponseAdapter.fromJson(responseBody);
if (!resp.status.equals(SUCCESS)) {
throw new IOException("Server returned error " + resp.status + " " + resp.msg);
}
modelGroups.clear();
modelGroups.addAll(resp.groups);
}
private void addHmacIfNeeded(String msg, Builder builder) throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
if (Config.getInstance().getSettings().requireAuthentication) {
byte[] key = Config.getInstance().getSettings().key;
@ -225,10 +273,25 @@ public class RemoteRecorder implements Recorder {
syncOnlineModels();
syncSpace();
syncRecordings();
syncModelGroups();
sleep();
}
}
private void syncModelGroups() {
try {
sendRequest("listModelGroups").ifPresent(body -> {
try {
updateModelGroups(body);
} catch (IOException e) {
LOG.error("Error while loading model groups from server", e);
}
});
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException e) {
LOG.error("Error while loading model groups from server", e);
}
}
private void syncSpace() {
try {
String msg = "{\"action\": \"space\"}";
@ -412,6 +475,12 @@ public class RemoteRecorder implements Recorder {
public List<Model> models;
}
private static class ModelGroupListResponse {
public String status;
public String msg;
public List<ModelGroup> groups;
}
private static class SimpleResponse {
public String status;
public String msg;
@ -460,6 +529,33 @@ public class RemoteRecorder implements Recorder {
}
}
public static class ModelGroupRequest {
private String action;
private ModelGroup modelGroup;
public ModelGroupRequest(String action, ModelGroup modelGroup) {
super();
this.action = action;
this.modelGroup = modelGroup;
}
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
public ModelGroup getModelGroup() {
return modelGroup;
}
public void setModelGroup(ModelGroup model) {
this.modelGroup = model;
}
}
public static class RecordingRequest {
private String action;
private Recording recording;
@ -592,4 +688,19 @@ public class RemoteRecorder implements Recorder {
public int getModelCount() {
return (int) models.stream().filter(m -> !m.isMarkedForLaterRecording()).count();
}
@Override
public Set<ModelGroup> getModelGroups() throws InvalidKeyException, NoSuchAlgorithmException, IOException {
return modelGroups;
}
@Override
public void saveModelGroup(ModelGroup group) throws InvalidKeyException, NoSuchAlgorithmException, IOException {
sendRequest("saveModelGroup", group);
}
@Override
public void deleteModelGroup(ModelGroup group) throws InvalidKeyException, NoSuchAlgorithmException, IOException {
sendRequest("deleteModelGroup", group);
}
}

View File

@ -3,105 +3,167 @@ package ctbrec.recorder.postprocessing;
import static ctbrec.StringUtil.*;
import static java.util.Optional.*;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.ModelGroup;
import ctbrec.Recording;
import ctbrec.StringUtil;
import ctbrec.sites.Site;
public abstract class AbstractPlaceholderAwarePostProcessor extends AbstractPostProcessor {
public static final String[] PLACE_HOLDERS = {
"${modelName}",
"${modelDisplayName}",
"${modelSanitizedName}",
"${siteName}",
"${siteSanitizedName}",
"${utcDateTime}",
"${localDateTime}",
"${epochSecond}",
"${fileSuffix}",
"${modelNotes}",
"${recordingNotes}",
"${recordingsDir}",
"${absolutePath}",
"${absoluteParentPath}"
};
private static final Logger LOG = LoggerFactory.getLogger(AbstractPlaceholderAwarePostProcessor.class);
public String fillInPlaceHolders(String input, Recording rec, Config config) {
// @formatter:off
String output = input
.replace("${modelName}", ofNullable(rec.getModel().getName()).orElse("modelName"))
.replace("${modelDisplayName}", ofNullable(rec.getModel().getDisplayName()).orElse("displayName"))
.replace("${modelSanitizedName}", ofNullable(rec.getModel().getSanitizedNamed()).orElse("sanitizedName"))
.replace("${siteName}", ofNullable(rec.getModel().getSite()).map(Site::getName).orElse("site"))
.replace("${siteSanitizedName}", getSanitizedSiteName(rec))
.replace("${fileSuffix}", getFileSuffix(rec))
.replace("${epochSecond}", Long.toString(rec.getStartDate().getEpochSecond()))
.replace("${modelNotes}", sanitize(config.getModelNotes(rec.getModel())))
.replace("${recordingNotes}", getSanitizedRecordingNotes(rec))
.replace("${recordingsDir}", config.getSettings().recordingsDir)
.replace("${absolutePath}", rec.getPostProcessedFile().getAbsolutePath())
.replace("${absoluteParentPath}", rec.getPostProcessedFile().getParentFile().getAbsolutePath())
;
public String fillInPlaceHolders(String input, PostProcessingContext ctx) {
Recording rec = ctx.getRecording();
Config config = ctx.getConfig();
Optional<ModelGroup> modelGroup;
try {
modelGroup = ctx.getRecorder().getModelGroup(rec.getModel());
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
LOG.error("Couldn't get model group for {}", rec.getModel(), e);
return input;
}
output = replaceUtcDateTime(rec, output);
output = replaceLocalDateTime(rec, output);
Map<String, Function<String, Optional<String>>> placeholderValueSuppliers = new HashMap<>();
placeholderValueSuppliers.put("modelName", r -> ofNullable(rec.getModel().getName()));
placeholderValueSuppliers.put("modelDisplayName", r -> ofNullable(rec.getModel().getDisplayName()));
placeholderValueSuppliers.put("modelSanitizedName", r -> getSanitizedName(rec.getModel()));
placeholderValueSuppliers.put("siteName", r -> ofNullable(rec.getModel().getSite()).map(Site::getName));
placeholderValueSuppliers.put("siteSanitizedName", r -> getSanitizedSiteName(rec));
placeholderValueSuppliers.put("fileSuffix", r -> getFileSuffix(rec));
placeholderValueSuppliers.put("epochSecond", r -> ofNullable(rec.getStartDate()).map(Instant::getEpochSecond).map(l -> Long.toString(l))); // NOSONAR
placeholderValueSuppliers.put("modelNotes", r -> getSanitizedModelNotes(config, rec.getModel()));
placeholderValueSuppliers.put("recordingNotes", r -> getSanitizedRecordingNotes(rec));
placeholderValueSuppliers.put("recordingsDir", r -> Optional.of(config.getSettings().recordingsDir));
placeholderValueSuppliers.put("absolutePath", r -> Optional.of(rec.getPostProcessedFile().getAbsolutePath()));
placeholderValueSuppliers.put("absoluteParentPath", r -> Optional.of(rec.getPostProcessedFile().getParentFile().getAbsolutePath()));
placeholderValueSuppliers.put("modelGroupName", r -> modelGroup.map(ModelGroup::getName));
placeholderValueSuppliers.put("modelGroupId", r -> modelGroup.map(ModelGroup::getId).map(UUID::toString));
placeholderValueSuppliers.put("utcDateTime", pattern -> replaceUtcDateTime(rec, pattern));
placeholderValueSuppliers.put("localDateTime", pattern -> replaceLocalDateTime(rec, pattern));
String output = fillInPlaceHolders(input, placeholderValueSuppliers);
return output;
// @formatter:on
}
private String replaceUtcDateTime(Recording rec, String filename) {
return replaceDateTime(rec, filename, "utcDateTime", ZoneOffset.UTC);
}
private String replaceLocalDateTime(Recording rec, String filename) {
return replaceDateTime(rec, filename, "localDateTime", ZoneId.systemDefault());
}
private String replaceDateTime(Recording rec, String filename, String placeHolder, ZoneId zone) {
String pattern = "yyyy-MM-dd_HH-mm-ss";
Pattern regex = Pattern.compile("\\$\\{" + placeHolder + "(?:\\((.*?)\\))?\\}");
Matcher m = regex.matcher(filename);
while (m.find()) {
String p = m.group(1);
if (p != null) {
pattern = p;
}
String formattedDate = getDateTime(rec, pattern, zone);
filename = m.replaceFirst(formattedDate);
m = regex.matcher(filename);
}
return filename;
}
private String getDateTime(Recording rec, String pattern, ZoneId zone) {
return DateTimeFormatter.ofPattern(pattern)
.withLocale(Locale.getDefault())
.withZone(zone)
.format(rec.getStartDate());
}
private CharSequence getFileSuffix(Recording rec) {
if(rec.isSingleFile()) {
String filename = rec.getPostProcessedFile().getName();
return filename.substring(filename.lastIndexOf('.') + 1);
private Optional<String> getSanitizedName(Model model) {
String name = model.getSanitizedNamed();
if (StringUtil.isBlank(name)) {
return Optional.empty();
} else {
return "";
return Optional.of(name);
}
}
private CharSequence getSanitizedSiteName(Recording rec) {
return sanitize(ofNullable(rec.getModel().getSite()).map(Site::getName).orElse(""));
private String fillInPlaceHolders(String input, Map<String, Function<String, Optional<String>>> placeholderValueSuppliers) {
boolean somethingReplaced = false;
do {
somethingReplaced = false;
int end = input.indexOf("}");
if (end > 0) {
int start = input.substring(0, end).lastIndexOf("${");
if (start >= 0) {
String placeholder = input.substring(start, end + 1);
String placeholderName = placeholder.substring(2, placeholder.length() - 1);
String defaultValue = null;
String expression = null;
int questionMark = placeholder.indexOf('?');
if (questionMark > 0) {
placeholderName = placeholder.substring(2, questionMark);
defaultValue = placeholder.substring(questionMark + 1, placeholder.length() - 1);
}
int bracket = placeholder.indexOf('(');
if (bracket > 0) {
placeholderName = placeholder.substring(2, bracket);
expression = placeholder.substring(bracket + 1, placeholder.indexOf(')', bracket));
}
final String name = placeholderName;
Optional<String> optionalValue = placeholderValueSuppliers.getOrDefault(name, r -> Optional.of(name)).apply(expression);
String value = optionalValue.orElse(defaultValue);
StringBuilder sb = new StringBuilder(input);
String output = sb.replace(start, end+1, value).toString();
somethingReplaced = !Objects.equals(input, output);
input = output;
}
}
} while (somethingReplaced);
return input;
}
private CharSequence getSanitizedRecordingNotes(Recording rec) {
return sanitize(ofNullable(rec.getNote()).orElse(""));
private Optional<String> replaceUtcDateTime(Recording rec, String pattern) {
return replaceDateTime(rec, pattern, ZoneOffset.UTC);
}
private Optional<String> replaceLocalDateTime(Recording rec, String filename) {
return replaceDateTime(rec, filename, ZoneId.systemDefault());
}
private Optional<String> replaceDateTime(Recording rec, String pattern, ZoneId zone) {
pattern = pattern != null ? pattern : "yyyy-MM-dd_HH-mm-ss";
return getDateTime(rec, pattern, zone);
}
private Optional<String> getDateTime(Recording rec, String pattern, ZoneId zone) {
return Optional.ofNullable(rec.getStartDate()) //
.map(DateTimeFormatter.ofPattern(pattern) //
.withLocale(Locale.getDefault()) //
.withZone(zone) //
::format);
}
private Optional<String> getFileSuffix(Recording rec) {
if (rec.isSingleFile()) {
String filename = rec.getPostProcessedFile().getName();
return Optional.of(filename.substring(filename.lastIndexOf('.') + 1));
} else {
return Optional.empty();
}
}
private Optional<String> getSanitizedSiteName(Recording rec) {
Optional<String> name = ofNullable(rec.getModel().getSite()).map(Site::getName);
if (name.isPresent()) {
return Optional.of(sanitize(name.get()));
} else {
return Optional.empty();
}
}
private Optional<String> getSanitizedRecordingNotes(Recording rec) {
Optional<String> notes = ofNullable(rec.getNote());
if (notes.isPresent()) {
return Optional.of(sanitize(notes.get()));
} else {
return Optional.empty();
}
}
private Optional<String> getSanitizedModelNotes(Config config, Model m) {
Optional<String> notes = ofNullable(config.getModelNotes(m));
if (notes.isPresent()) {
return Optional.of(sanitize(notes.get()));
} else {
return Optional.empty();
}
}
}

View File

@ -8,9 +8,7 @@ import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
public class Copy extends AbstractPostProcessor {
@ -22,7 +20,8 @@ public class Copy extends AbstractPostProcessor {
}
@Override
public boolean postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException {
public boolean postprocess(PostProcessingContext ctx) throws IOException, InterruptedException {
Recording rec = ctx.getRecording();
File orig = rec.getPostProcessedFile();
String copyFilename = getFilenameForCopy(orig);
File copy = new File(orig.getParentFile(), copyFilename);

View File

@ -22,7 +22,6 @@ import ctbrec.OS;
import ctbrec.Recording;
import ctbrec.io.IoUtils;
import ctbrec.recorder.FFmpeg;
import ctbrec.recorder.RecordingManager;
import ctbrec.recorder.download.ProcessExitedUncleanException;
@ThreadSafe
@ -45,7 +44,9 @@ public class CreateContactSheet extends AbstractPlaceholderAwarePostProcessor {
}
@Override
public boolean postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException {
public boolean postprocess(PostProcessingContext ctx) throws IOException, InterruptedException {
Recording rec = ctx.getRecording();
Config config = ctx.getConfig();
int totalWidth = Integer.parseInt(getConfig().getOrDefault(TOTAL_SIZE, "1920"));
int padding = Integer.parseInt(getConfig().getOrDefault(PADDING, "4"));
int cols = Integer.parseInt(getConfig().getOrDefault(COLS, "8"));
@ -65,7 +66,7 @@ public class CreateContactSheet extends AbstractPlaceholderAwarePostProcessor {
color},
new StringBuffer(), null).toString();
File executionDir = rec.getPostProcessedFile().isDirectory() ? rec.getPostProcessedFile() : rec.getPostProcessedFile().getParentFile();
File output = new File(executionDir, fillInPlaceHolders(filename, rec, config));
File output = new File(executionDir, fillInPlaceHolders(filename, ctx));
String[] args = {
"-y",

View File

@ -2,10 +2,7 @@ package ctbrec.recorder.postprocessing;
import java.io.IOException;
import ctbrec.Config;
import ctbrec.NotImplementedExcetion;
import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
public class CreateTimelineThumbs extends AbstractPlaceholderAwarePostProcessor {
@ -15,7 +12,7 @@ public class CreateTimelineThumbs extends AbstractPlaceholderAwarePostProcessor
}
@Override
public boolean postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException {
public boolean postprocess(PostProcessingContext ctx) throws IOException, InterruptedException {
// create 1 thumb every second with a width of 360 pixels and save it as jpeg with a 5-digit sequence number
// ffmpeg -i <file> -vf 'fps=1,scale=360:-1' thumbs/out%05d.jpg
throw new NotImplementedExcetion();

View File

@ -5,9 +5,7 @@ import static ctbrec.io.IoUtils.*;
import java.io.IOException;
import java.nio.file.Files;
import ctbrec.Config;
import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
public class DeleteOriginal extends AbstractPostProcessor {
@ -17,7 +15,8 @@ public class DeleteOriginal extends AbstractPostProcessor {
}
@Override
public boolean postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException {
public boolean postprocess(PostProcessingContext ctx) throws IOException, InterruptedException {
Recording rec = ctx.getRecording();
if (rec.getAbsoluteFile().isFile()) {
Files.deleteIfExists(rec.getAbsoluteFile().toPath());
deleteEmptyParents(rec.getAbsoluteFile().getParentFile());

View File

@ -6,7 +6,6 @@ import java.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
@ -21,7 +20,9 @@ public class DeleteTooShort extends AbstractPostProcessor {
}
@Override
public boolean postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException {
public boolean postprocess(PostProcessingContext ctx) throws IOException {
Recording rec = ctx.getRecording();
RecordingManager recordingManager = ctx.getRecordingManager();
Duration minimumLengthInSeconds = Duration.ofSeconds(Integer.parseInt(getConfig().getOrDefault(MIN_LEN_IN_SECS, "0")));
if (minimumLengthInSeconds.getSeconds() > 0) {
Duration recordingLength = rec.getLength();

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -37,19 +37,19 @@ public class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest {
@Test
public void testModelNameReplacement() {
String input = "asdf_${modelName}_asdf";
assertEquals("asdf_Mockita Boobilicious_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
assertEquals("asdf_Mockita Boobilicious_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
input = "asdf_${modelDisplayName}_asdf";
assertEquals("asdf_Mockita Boobilicious_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
assertEquals("asdf_Mockita Boobilicious_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
input = "asdf_${modelSanitizedName}_asdf";
assertEquals("asdf_Mockita_Boobilicious_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
assertEquals("asdf_Mockita_Boobilicious_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
}
@Test
public void testSiteNameReplacement() {
String input = "asdf_${siteName}_asdf";
assertEquals("asdf_Chaturbate_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
assertEquals("asdf_Chaturbate_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
input = "asdf_${siteSanitizedName}_asdf";
assertEquals("asdf_Chaturbate_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
assertEquals("asdf_Chaturbate_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
}
@Test
@ -60,7 +60,7 @@ public class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest {
.withZone(ZoneOffset.UTC)
.format(rec.getStartDate());
String input = "asdf_${utcDateTime}_asdf";
assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
// with user defined pattern
date = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")
@ -68,7 +68,7 @@ public class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest {
.withZone(ZoneOffset.UTC)
.format(rec.getStartDate());
input = "asdf_${utcDateTime(yyyyMMdd-HHmmss)}_asdf";
assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
// multiple occurences with user defined patterns
date = DateTimeFormatter.ofPattern("yyyy-MM-dd/yyyy")
@ -76,7 +76,7 @@ public class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest {
.withZone(ZoneOffset.UTC)
.format(rec.getStartDate());
input = "asdf_${utcDateTime(yyyy)}-${utcDateTime(MM)}-${utcDateTime(dd)}/${utcDateTime(yyyy)}_asdf";
assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
}
@Test
@ -86,50 +86,68 @@ public class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest {
.withZone(ZoneId.systemDefault())
.format(rec.getStartDate());
String input = "asdf_${localDateTime}_asdf";
assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
date = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")
.withLocale(Locale.US)
.withZone(ZoneId.systemDefault())
.format(rec.getStartDate());
input = "asdf_${localDateTime(yyyyMMdd-HHmmss)}_asdf";
assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
}
@Test
public void testEpochReplacement() {
long epoch = now.toEpochMilli() / 1000;
String input = "asdf_${epochSecond}_asdf";
assertEquals("asdf_" + epoch + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
assertEquals("asdf_" + epoch + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
}
@Test
public void testFileSuffixReplacement() {
String input = "asdf_${fileSuffix}_asdf";
assertEquals("asdf_ts_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
assertEquals("asdf_ts_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
}
@Test
public void testRecordingsDirReplacement() {
String input = "asdf_${recordingsDir}_asdf";
assertEquals("asdf_" + recDir.toString() + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
assertEquals("asdf_" + recDir.toString() + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
}
@Test
public void testAbsolutePathReplacement() {
String input = "asdf_${absolutePath}_asdf";
assertEquals("asdf_" + postProcessed.getAbsolutePath().toString() + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
assertEquals("asdf_" + postProcessed.getAbsolutePath().toString() + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
}
@Test
public void testAbsoluteParentPathReplacement() {
String input = "asdf_${absoluteParentPath}_asdf";
assertEquals("asdf_" + postProcessed.getParentFile().toString() + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
assertEquals("asdf_" + postProcessed.getParentFile().toString() + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
}
@Test
public void testModelNotesReplacement() {
String input = "asdf_${modelNotes}_asdf";
assertEquals("asdf_tag,_foo,_bar_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
assertEquals("asdf_tag,_foo,_bar_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
}
@Test
public void testPlaceholderDefaultValues() throws IOException {
String input = "asdf_${modelGroupName?${modelSanitizedName?anonymous}}_asdf";
PostProcessingContext ctx = createPostProcessingContext(rec, null, config);
ctx.getRecording().getModel().setName(null);
assertEquals("asdf_anonymous_asdf", placeHolderAwarePp.fillInPlaceHolders(input, ctx));
input = "asdf_${modelGroupName?${utcDateTime(yyyy)?anonymous}}_asdf";
assertEquals("asdf_2021_asdf", placeHolderAwarePp.fillInPlaceHolders(input, ctx));
ctx.getRecording().setStartDate(null);
input = "asdf_${modelGroupName?${utcDateTime(yyyy)?anonymous}}_asdf";
assertEquals("asdf_anonymous_asdf", placeHolderAwarePp.fillInPlaceHolders(input, ctx));
input = "asdf_${modelGroupName?${utcDateTime?anonymous}}_asdf";
assertEquals("asdf_anonymous_asdf", placeHolderAwarePp.fillInPlaceHolders(input, ctx));
}
}

View File

@ -18,6 +18,7 @@ import org.mockito.MockedStatic;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.Recording;
import ctbrec.Settings;
import ctbrec.recorder.RecordingManager;
import ctbrec.sites.Site;
@ -81,4 +82,12 @@ public abstract class AbstractPpTest {
settings.recordingsDir = recDir.toString();
return settings;
}
PostProcessingContext createPostProcessingContext(Recording rec, RecordingManager recordingManager, Config config) {
PostProcessingContext ctx = new PostProcessingContext();
ctx.setConfig(config);
ctx.setRecording(rec);
ctx.setRecordingManager(recordingManager);
return ctx;
}
}

View File

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

View File

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

View File

@ -42,7 +42,7 @@ public class DeleteTooShortTest extends AbstractPpTest {
DeleteTooShort pp = new DeleteTooShort();
pp.getConfig().put(DeleteTooShort.MIN_LEN_IN_SECS, "10");
pp.postprocess(rec, recordingManager, config);
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
assertFalse(rec.getAbsoluteFile().exists());
assertFalse(original.exists());
@ -66,7 +66,7 @@ public class DeleteTooShortTest extends AbstractPpTest {
DeleteTooShort pp = new DeleteTooShort();
pp.getConfig().put(DeleteTooShort.MIN_LEN_IN_SECS, "0");
pp.postprocess(rec, recordingManager, config);
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
assertTrue(rec.getAbsoluteFile().exists());
assertTrue(original.exists());
@ -83,7 +83,7 @@ public class DeleteTooShortTest extends AbstractPpTest {
DeleteTooShort pp = new DeleteTooShort();
pp.getConfig().put(DeleteTooShort.MIN_LEN_IN_SECS, "1");
pp.postprocess(rec, recordingManager, config);
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
assertTrue(rec.getAbsoluteFile().exists());
assertTrue(original.exists());

View File

@ -28,7 +28,7 @@ public class MoveDirectoryTest extends AbstractPpTest {
rec.setSingleFile(false);
Move pp = new Move();
pp.getConfig().put(Move.PATH_TEMPLATE, new File(baseDir.toFile(), Move.DEFAULT).getAbsolutePath());
pp.postprocess(rec, recordingManager, config);
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
Matcher m = Pattern.compile(baseDir.toString() + "/Mockita_Boobilicious/\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}/original").matcher(rec.getAbsoluteFile().getCanonicalPath());
assertTrue(m.matches());
@ -50,6 +50,6 @@ public class MoveDirectoryTest extends AbstractPpTest {
Move pp = new Move();
Config config = mockConfig();
pp.getConfig().put(Move.PATH_TEMPLATE, new File(baseDir.toFile(), Move.DEFAULT).getAbsolutePath());
pp.postprocess(rec, recordingManager, config);
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
}
}

View File

@ -28,7 +28,7 @@ public class MoveSingleFileTest extends AbstractPpTest {
Move pp = new Move();
pp.getConfig().put(Move.PATH_TEMPLATE, new File(baseDir.toFile(), Move.DEFAULT).getAbsolutePath());
pp.postprocess(rec, recordingManager, config);
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
Matcher m = Pattern.compile(baseDir.toFile() + "/Mockita_Boobilicious/\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}/original\\.ts").matcher(rec.getAbsoluteFile().toString());
assertTrue(m.matches());
@ -48,7 +48,7 @@ public class MoveSingleFileTest extends AbstractPpTest {
Move pp = new Move();
Config config = mockConfig();
pp.getConfig().put(Move.PATH_TEMPLATE, original.getParentFile().getCanonicalPath());
pp.postprocess(rec, recordingManager, config);
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
}
@Test
@ -63,7 +63,7 @@ public class MoveSingleFileTest extends AbstractPpTest {
Move pp = new Move();
Config config = mockConfig();
pp.getConfig().put(Move.PATH_TEMPLATE, new File(baseDir.toFile(), Move.DEFAULT).getAbsolutePath());
pp.postprocess(rec, recordingManager, config);
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
}
@Test

View File

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

View File

@ -26,7 +26,7 @@ public class RenameDirectoryTest extends AbstractPpTest {
rec.setStartDate(now);
rec.setSingleFile(false);
Rename pp = new Rename();
pp.postprocess(rec, recordingManager, config);
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
Matcher m = Pattern.compile("Mockita_Boobilicious_\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}").matcher(rec.getAbsoluteFile().getName());
assertTrue(m.matches());
@ -47,6 +47,6 @@ public class RenameDirectoryTest extends AbstractPpTest {
Files.createDirectories(postProcessedDir.toPath());
Rename pp = new Rename();
pp.postprocess(rec, recordingManager, config);
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
}
}

View File

@ -25,7 +25,7 @@ public class RenameSingleFileTest extends AbstractPpTest {
rec.setStartDate(now);
rec.setSingleFile(true);
Rename pp = new Rename();
pp.postprocess(rec, recordingManager, config);
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
Matcher m = Pattern.compile("Mockita_Boobilicious_\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}\\.ts").matcher(rec.getAbsoluteFile().getName());
assertTrue(m.matches());
@ -45,7 +45,7 @@ public class RenameSingleFileTest extends AbstractPpTest {
doThrow(new RuntimeException("Unexpected call of setAbsoluteFile")).when(rec).setAbsoluteFile(any());
Rename pp = new Rename();
pp.getConfig().put(Rename.FILE_NAME_TEMPLATE, original.getName());
pp.postprocess(rec, recordingManager, config);
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
}
@Test
@ -59,7 +59,7 @@ public class RenameSingleFileTest extends AbstractPpTest {
when(rec.getStartDate()).thenReturn(now);
doThrow(new RuntimeException("Unexpected call of setAbsoluteFile")).when(rec).setAbsoluteFile(any());
Rename pp = new Rename();
pp.postprocess(rec, recordingManager, config);
pp.postprocess(createPostProcessingContext(rec, recordingManager, config));
}
@Test

View File

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