forked from j62/ctbrec
Add first configurable PP step
This commit is contained in:
parent
4f8e7dbca2
commit
17a32cd928
|
@ -0,0 +1,52 @@
|
||||||
|
package ctbrec.ui.settings;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import ctbrec.recorder.postprocessing.PostProcessor;
|
||||||
|
import ctbrec.ui.settings.api.Preferences;
|
||||||
|
import ctbrec.ui.settings.api.PreferencesStorage;
|
||||||
|
import ctbrec.ui.settings.api.Setting;
|
||||||
|
import javafx.beans.property.Property;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
|
||||||
|
public abstract class AbstractPostProcessingPaneFactory {
|
||||||
|
|
||||||
|
private PostProcessor pp;
|
||||||
|
Set<Property<?>> properties = new HashSet<>();
|
||||||
|
|
||||||
|
public abstract Preferences doCreatePostProcessorPane(PostProcessor pp);
|
||||||
|
|
||||||
|
public Preferences createPostProcessorPane(PostProcessor pp) {
|
||||||
|
this.pp = pp;
|
||||||
|
return doCreatePostProcessorPane(pp);
|
||||||
|
}
|
||||||
|
|
||||||
|
class MapPreferencesStorage implements PreferencesStorage {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save(Preferences preferences) throws IOException {
|
||||||
|
for (Property<?> property : properties) {
|
||||||
|
String key = property.getName();
|
||||||
|
Object value = preferences.getSetting(key).get().getProperty().getValue();
|
||||||
|
pp.getConfig().put(key, value.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void load(Preferences preferences) {
|
||||||
|
// no op
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public Node createGui(Setting setting) throws Exception {
|
||||||
|
TextField input = new TextField();
|
||||||
|
input.textProperty().bindBidirectional(setting.getProperty());
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
package ctbrec.ui.settings;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.recorder.postprocessing.PostProcessor;
|
||||||
|
import ctbrec.recorder.postprocessing.Remuxer;
|
||||||
|
import ctbrec.ui.controls.Dialogs;
|
||||||
|
import ctbrec.ui.settings.api.Preferences;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
|
||||||
|
public class PostProcessingDialogFactory {
|
||||||
|
|
||||||
|
static Map<Class<?>, Class<?>> ppToDialogMap = new HashMap<>();
|
||||||
|
static {
|
||||||
|
ppToDialogMap.put(Remuxer.class, RemuxerPaneFactory.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PostProcessingDialogFactory() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void openNewDialog(PostProcessor pp, Config config, Scene scene, ObservableList<PostProcessor> stepList) {
|
||||||
|
openDialog(pp, config, scene, stepList, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void openEditDialog(PostProcessor pp, Config config, Scene scene, ObservableList<PostProcessor> stepList) {
|
||||||
|
openDialog(pp, config, scene, stepList, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void openDialog(PostProcessor pp, Config config, Scene scene, ObservableList<PostProcessor> stepList, boolean newEntry) {
|
||||||
|
boolean ok;
|
||||||
|
try {
|
||||||
|
Preferences preferences = createPreferences(pp);
|
||||||
|
ok = Dialogs.showCustomInput(scene, "Configure " + pp.getName(), preferences.getView(false));
|
||||||
|
if (ok) {
|
||||||
|
preferences.save();
|
||||||
|
if (newEntry) {
|
||||||
|
stepList.add(pp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
|
||||||
|
| InstantiationException | IOException e) {
|
||||||
|
Dialogs.showError("New post-processing step", "Couldn't create dialog for " + pp.getName(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Preferences createPreferences(PostProcessor pp) throws InstantiationException, IllegalAccessException, IllegalArgumentException,
|
||||||
|
InvocationTargetException, NoSuchMethodException, SecurityException {
|
||||||
|
Class<?> paneFactoryClass = ppToDialogMap.get(pp.getClass());
|
||||||
|
AbstractPostProcessingPaneFactory factory = (AbstractPostProcessingPaneFactory) paneFactoryClass.getDeclaredConstructor().newInstance();
|
||||||
|
return factory.createPostProcessorPane(pp);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,173 @@
|
||||||
|
package ctbrec.ui.settings;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.recorder.postprocessing.PostProcessor;
|
||||||
|
import ctbrec.recorder.postprocessing.RecordingRenamer;
|
||||||
|
import ctbrec.recorder.postprocessing.Remuxer;
|
||||||
|
import ctbrec.ui.controls.Dialogs;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ListChangeListener;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.ChoiceDialog;
|
||||||
|
import javafx.scene.control.ListView;
|
||||||
|
import javafx.scene.control.Tooltip;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.layout.GridPane;
|
||||||
|
import javafx.scene.layout.Priority;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
public class PostProcessingStepPanel extends GridPane {
|
||||||
|
|
||||||
|
private Config config;
|
||||||
|
|
||||||
|
private static final Class<?>[] POST_PROCESSOR_CLASSES = new Class<?>[] { Remuxer.class, RecordingRenamer.class };
|
||||||
|
|
||||||
|
ListView<PostProcessor> stepListView;
|
||||||
|
ObservableList<PostProcessor> stepList;
|
||||||
|
|
||||||
|
Button up;
|
||||||
|
Button down;
|
||||||
|
|
||||||
|
Button add;
|
||||||
|
Button remove;
|
||||||
|
Button edit;
|
||||||
|
|
||||||
|
public PostProcessingStepPanel(Config config) {
|
||||||
|
this.config = config;
|
||||||
|
initGui();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initGui() {
|
||||||
|
setHgap(5);
|
||||||
|
vgapProperty().bind(hgapProperty());
|
||||||
|
|
||||||
|
up = createUpButton();
|
||||||
|
down = createDownButton();
|
||||||
|
add = createAddButton();
|
||||||
|
remove = createRemoveButton();
|
||||||
|
edit = createEditButton();
|
||||||
|
VBox buttons = new VBox(5, add, edit, up, down, remove);
|
||||||
|
|
||||||
|
stepList = FXCollections.observableList(config.getSettings().postProcessors);
|
||||||
|
stepList.addListener((ListChangeListener<PostProcessor>) change -> {
|
||||||
|
try {
|
||||||
|
config.save();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Dialogs.showError(getScene(), "Couldn't save configuration", "An error occurred while saving the configuration", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
stepListView = new ListView<>(stepList);
|
||||||
|
GridPane.setHgrow(stepListView, Priority.ALWAYS);
|
||||||
|
|
||||||
|
add(stepListView, 0, 0);
|
||||||
|
add(buttons, 1, 0);
|
||||||
|
|
||||||
|
stepListView.getSelectionModel().selectedIndexProperty().addListener((obs, oldV, newV) -> {
|
||||||
|
int idx = newV.intValue();
|
||||||
|
boolean noSelection = idx == -1;
|
||||||
|
up.setDisable(noSelection || idx == 0);
|
||||||
|
down.setDisable(noSelection || idx == stepList.size() - 1);
|
||||||
|
edit.setDisable(noSelection);
|
||||||
|
remove.setDisable(noSelection);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Button createUpButton() {
|
||||||
|
Button up = createButton("\u25B4", "Move step up");
|
||||||
|
up.setOnAction(evt -> {
|
||||||
|
int idx = stepListView.getSelectionModel().getSelectedIndex();
|
||||||
|
PostProcessor selectedItem = stepListView.getSelectionModel().getSelectedItem();
|
||||||
|
stepList.remove(idx);
|
||||||
|
stepList.add(idx - 1, selectedItem);
|
||||||
|
stepListView.getSelectionModel().select(idx - 1);
|
||||||
|
});
|
||||||
|
return up;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Button createDownButton() {
|
||||||
|
Button down = createButton("\u25BE", "Move step down");
|
||||||
|
down.setOnAction(evt -> {
|
||||||
|
int idx = stepListView.getSelectionModel().getSelectedIndex();
|
||||||
|
PostProcessor selectedItem = stepListView.getSelectionModel().getSelectedItem();
|
||||||
|
stepList.remove(idx);
|
||||||
|
stepList.add(idx + 1, selectedItem);
|
||||||
|
stepListView.getSelectionModel().select(idx + 1);
|
||||||
|
});
|
||||||
|
return down;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Button createAddButton() {
|
||||||
|
Button add = createButton("+", "Add a new step");
|
||||||
|
add.setDisable(false);
|
||||||
|
add.setOnAction(evt -> {
|
||||||
|
PostProcessor[] options = createOptions();
|
||||||
|
ChoiceDialog<PostProcessor> choice = new ChoiceDialog<>(options[0], options);
|
||||||
|
choice.setTitle("New Post-Processing Step");
|
||||||
|
choice.setHeaderText("Select the new step type");
|
||||||
|
choice.setResizable(true);
|
||||||
|
choice.setWidth(600);
|
||||||
|
choice.getDialogPane().setMinWidth(400);
|
||||||
|
Stage stage = (Stage) choice.getDialogPane().getScene().getWindow();
|
||||||
|
stage.getScene().getStylesheets().addAll(getScene().getStylesheets());
|
||||||
|
InputStream icon = Dialogs.class.getResourceAsStream("/icon.png");
|
||||||
|
stage.getIcons().add(new Image(icon));
|
||||||
|
|
||||||
|
Optional<PostProcessor> result = choice.showAndWait();
|
||||||
|
result.ifPresent(pp -> PostProcessingDialogFactory.openNewDialog(pp, config, getScene(), stepList));
|
||||||
|
});
|
||||||
|
return add;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PostProcessor[] createOptions() {
|
||||||
|
try {
|
||||||
|
PostProcessor[] options = new PostProcessor[POST_PROCESSOR_CLASSES.length];
|
||||||
|
for (int i = 0; i < POST_PROCESSOR_CLASSES.length; i++) {
|
||||||
|
Class<?> cls = POST_PROCESSOR_CLASSES[i];
|
||||||
|
PostProcessor pp;
|
||||||
|
pp = (PostProcessor) cls.getDeclaredConstructor().newInstance();
|
||||||
|
options[i] = pp;
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException
|
||||||
|
| SecurityException e) {
|
||||||
|
Dialogs.showError(getScene(), "Create post-processor selection", "Error while reaing in post-processing options", e);
|
||||||
|
return new PostProcessor[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Button createRemoveButton() {
|
||||||
|
Button remove = createButton("-", "Remove selected step");
|
||||||
|
remove.setOnAction(evt -> {
|
||||||
|
PostProcessor selectedItem = stepListView.getSelectionModel().getSelectedItem();
|
||||||
|
if (selectedItem != null) {
|
||||||
|
stepList.remove(selectedItem);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return remove;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Button createEditButton() {
|
||||||
|
Button edit = createButton("\u270E", "Edit selected step");
|
||||||
|
edit.setOnAction(evt -> {
|
||||||
|
PostProcessor selectedItem = stepListView.getSelectionModel().getSelectedItem();
|
||||||
|
PostProcessingDialogFactory.openEditDialog(selectedItem, config, getScene(), stepList);
|
||||||
|
stepListView.refresh();
|
||||||
|
});
|
||||||
|
return edit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Button createButton(String text, String tooltip) {
|
||||||
|
Button b = new Button(text);
|
||||||
|
b.setTooltip(new Tooltip(tooltip));
|
||||||
|
b.setDisable(true);
|
||||||
|
b.setPrefSize(32, 32);
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package ctbrec.ui.settings;
|
||||||
|
|
||||||
|
import ctbrec.recorder.postprocessing.PostProcessor;
|
||||||
|
import ctbrec.recorder.postprocessing.Remuxer;
|
||||||
|
import ctbrec.ui.settings.api.Category;
|
||||||
|
import ctbrec.ui.settings.api.Preferences;
|
||||||
|
import ctbrec.ui.settings.api.Setting;
|
||||||
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
|
||||||
|
public class RemuxerPaneFactory extends AbstractPostProcessingPaneFactory {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Preferences doCreatePostProcessorPane(PostProcessor pp) {
|
||||||
|
SimpleStringProperty ffmpegParams = new SimpleStringProperty(null, Remuxer.FFMPEG_ARGS, pp.getConfig().getOrDefault(Remuxer.FFMPEG_ARGS, "-c:v copy -c:a copy -movflags faststart -y -f mp4"));
|
||||||
|
SimpleStringProperty fileExt = new SimpleStringProperty(null, Remuxer.FILE_EXT, pp.getConfig().getOrDefault(Remuxer.FILE_EXT, "mp4"));
|
||||||
|
properties.add(ffmpegParams);
|
||||||
|
properties.add(fileExt);
|
||||||
|
|
||||||
|
return Preferences.of(new MapPreferencesStorage(),
|
||||||
|
Category.of(pp.getName(),
|
||||||
|
Setting.of("FFmpeg parameters", ffmpegParams),
|
||||||
|
Setting.of("File extension", fileExt)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -209,7 +209,8 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
Setting.of("Post-Processing", postProcessing),
|
Setting.of("Post-Processing", postProcessing),
|
||||||
Setting.of("Threads", postProcessingThreads),
|
Setting.of("Threads", postProcessingThreads),
|
||||||
Setting.of("Delete recordings shorter than (secs)", minimumLengthInSecs, "Delete recordings, which are shorter than x seconds. 0 to disable"),
|
Setting.of("Delete recordings shorter than (secs)", minimumLengthInSecs, "Delete recordings, which are shorter than x seconds. 0 to disable"),
|
||||||
Setting.of("Remove recording after post-processing", removeRecordingAfterPp)
|
Setting.of("Remove recording after post-processing", removeRecordingAfterPp),
|
||||||
|
Setting.of("Steps", new PostProcessingStepPanel(config))
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
Category.of("Events & Actions", new ActionSettingsPanel(recorder)),
|
Category.of("Events & Actions", new ActionSettingsPanel(recorder)),
|
||||||
|
|
|
@ -2,6 +2,7 @@ package ctbrec.ui.settings.api;
|
||||||
|
|
||||||
import static java.util.Optional.*;
|
import static java.util.Optional.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
@ -21,6 +22,7 @@ import javafx.scene.control.TreeView;
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.GridPane;
|
import javafx.scene.layout.GridPane;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
|
import javafx.scene.layout.Region;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
public class Preferences {
|
public class Preferences {
|
||||||
|
@ -31,7 +33,10 @@ public class Preferences {
|
||||||
|
|
||||||
private TreeView<Category> categoryTree;
|
private TreeView<Category> categoryTree;
|
||||||
|
|
||||||
|
private PreferencesStorage preferencesStorage;
|
||||||
|
|
||||||
private Preferences(PreferencesStorage preferencesStorage, Category...categories) {
|
private Preferences(PreferencesStorage preferencesStorage, Category...categories) {
|
||||||
|
this.preferencesStorage = preferencesStorage;
|
||||||
this.categories = categories;
|
this.categories = categories;
|
||||||
for (Category category : categories) {
|
for (Category category : categories) {
|
||||||
assignPreferencesStorage(category, preferencesStorage);
|
assignPreferencesStorage(category, preferencesStorage);
|
||||||
|
@ -56,15 +61,15 @@ public class Preferences {
|
||||||
return new Preferences(preferencesStorage, categories);
|
return new Preferences(preferencesStorage, categories);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void save() {
|
public void save() throws IOException {
|
||||||
throw new RuntimeException("save not implemented");
|
preferencesStorage.save(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
Category[] getCategories() {
|
Category[] getCategories() {
|
||||||
return categories;
|
return categories;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Node getView() {
|
public Region getView(boolean withNavigation) {
|
||||||
SearchBox search = new SearchBox(true);
|
SearchBox search = new SearchBox(true);
|
||||||
search.textProperty().addListener(this::filterTree);
|
search.textProperty().addListener(this::filterTree);
|
||||||
TreeItem<Category> categoryTreeItems = createCategoryTree(categories, new TreeItem<>(), null);
|
TreeItem<Category> categoryTreeItems = createCategoryTree(categories, new TreeItem<>(), null);
|
||||||
|
@ -76,7 +81,9 @@ public class Preferences {
|
||||||
VBox.setMargin(categoryTree, new Insets(2));
|
VBox.setMargin(categoryTree, new Insets(2));
|
||||||
|
|
||||||
BorderPane main = new BorderPane();
|
BorderPane main = new BorderPane();
|
||||||
|
if (withNavigation) {
|
||||||
main.setLeft(leftSide);
|
main.setLeft(leftSide);
|
||||||
|
}
|
||||||
main.setCenter(new Label("Center"));
|
main.setCenter(new Label("Center"));
|
||||||
BorderPane.setMargin(leftSide, new Insets(2));
|
BorderPane.setMargin(leftSide, new Insets(2));
|
||||||
|
|
||||||
|
@ -92,6 +99,10 @@ public class Preferences {
|
||||||
return main;
|
return main;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Region getView() {
|
||||||
|
return getView(true);
|
||||||
|
}
|
||||||
|
|
||||||
private void filterTree(ObservableValue<? extends String> obs, String oldV, String newV) {
|
private void filterTree(ObservableValue<? extends String> obs, String oldV, String newV) {
|
||||||
String q = ofNullable(newV).orElse("").toLowerCase().trim();
|
String q = ofNullable(newV).orElse("").toLowerCase().trim();
|
||||||
TreeItem<Category> filteredCategoryTree = createCategoryTree(categories, new TreeItem<>(), q);
|
TreeItem<Category> filteredCategoryTree = createCategoryTree(categories, new TreeItem<>(), q);
|
||||||
|
@ -151,6 +162,8 @@ public class Preferences {
|
||||||
|
|
||||||
private Node createGrid(Setting[] settings) throws Exception {
|
private Node createGrid(Setting[] settings) throws Exception {
|
||||||
GridPane pane = new GridPane();
|
GridPane pane = new GridPane();
|
||||||
|
pane.setHgap(2);
|
||||||
|
pane.vgapProperty().bind(pane.hgapProperty());
|
||||||
int row = 0;
|
int row = 0;
|
||||||
for (Setting setting : settings) {
|
for (Setting setting : settings) {
|
||||||
Node node = setting.getGui();
|
Node node = setting.getGui();
|
||||||
|
@ -198,13 +211,11 @@ public class Preferences {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void visit(Category cat, Consumer<Setting> visitor) {
|
private void visit(Category cat, Consumer<Setting> visitor) {
|
||||||
if (cat.hasGroups()) {
|
|
||||||
for (Group group : cat.getGroups()) {
|
for (Group group : cat.getGroups()) {
|
||||||
for (Setting setting : group.getSettings()) {
|
for (Setting setting : group.getSettings()) {
|
||||||
visitor.accept(setting);
|
visitor.accept(setting);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (cat.hasSubCategories()) {
|
if (cat.hasSubCategories()) {
|
||||||
for (Category subcat : cat.getSubCategories()) {
|
for (Category subcat : cat.getSubCategories()) {
|
||||||
visit(subcat, visitor);
|
visit(subcat, visitor);
|
||||||
|
|
|
@ -23,7 +23,10 @@ import org.slf4j.LoggerFactory;
|
||||||
import com.squareup.moshi.JsonAdapter;
|
import com.squareup.moshi.JsonAdapter;
|
||||||
import com.squareup.moshi.Moshi;
|
import com.squareup.moshi.Moshi;
|
||||||
|
|
||||||
|
import ctbrec.io.FileJsonAdapter;
|
||||||
import ctbrec.io.ModelJsonAdapter;
|
import ctbrec.io.ModelJsonAdapter;
|
||||||
|
import ctbrec.io.PostProcessorJsonAdapter;
|
||||||
|
import ctbrec.recorder.postprocessing.PostProcessor;
|
||||||
import ctbrec.sites.Site;
|
import ctbrec.sites.Site;
|
||||||
|
|
||||||
public class Config {
|
public class Config {
|
||||||
|
@ -55,6 +58,8 @@ public class Config {
|
||||||
private void load() throws IOException {
|
private void load() throws IOException {
|
||||||
Moshi moshi = new Moshi.Builder()
|
Moshi moshi = new Moshi.Builder()
|
||||||
.add(Model.class, new ModelJsonAdapter(sites))
|
.add(Model.class, new ModelJsonAdapter(sites))
|
||||||
|
.add(PostProcessor.class, new PostProcessorJsonAdapter())
|
||||||
|
.add(File.class, new FileJsonAdapter())
|
||||||
.build();
|
.build();
|
||||||
JsonAdapter<Settings> adapter = moshi.adapter(Settings.class).lenient();
|
JsonAdapter<Settings> adapter = moshi.adapter(Settings.class).lenient();
|
||||||
File configFile = new File(configDir, filename);
|
File configFile = new File(configDir, filename);
|
||||||
|
@ -125,6 +130,8 @@ public class Config {
|
||||||
public void save() throws IOException {
|
public void save() throws IOException {
|
||||||
Moshi moshi = new Moshi.Builder()
|
Moshi moshi = new Moshi.Builder()
|
||||||
.add(Model.class, new ModelJsonAdapter())
|
.add(Model.class, new ModelJsonAdapter())
|
||||||
|
.add(PostProcessor.class, new PostProcessorJsonAdapter())
|
||||||
|
.add(File.class, new FileJsonAdapter())
|
||||||
.build();
|
.build();
|
||||||
JsonAdapter<Settings> adapter = moshi.adapter(Settings.class).indent(" ");
|
JsonAdapter<Settings> adapter = moshi.adapter(Settings.class).indent(" ");
|
||||||
String json = adapter.toJson(settings);
|
String json = adapter.toJson(settings);
|
||||||
|
|
|
@ -17,6 +17,8 @@ import java.time.LocalDateTime;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
@ -39,6 +41,8 @@ public class Recording implements Serializable {
|
||||||
private boolean singleFile = false;
|
private boolean singleFile = false;
|
||||||
private boolean pinned = false;
|
private boolean pinned = false;
|
||||||
private String note;
|
private String note;
|
||||||
|
private Set<String> associatedFiles = new HashSet<>();
|
||||||
|
private File postProcessedFile = null;
|
||||||
|
|
||||||
public enum State {
|
public enum State {
|
||||||
RECORDING("recording"),
|
RECORDING("recording"),
|
||||||
|
@ -107,6 +111,17 @@ public class Recording implements Serializable {
|
||||||
return recordingsFile;
|
return recordingsFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public File getPostProcessedFile() {
|
||||||
|
if (postProcessedFile == null) {
|
||||||
|
setPostProcessedFile(getAbsoluteFile());
|
||||||
|
}
|
||||||
|
return postProcessedFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPostProcessedFile(File postProcessedFile) {
|
||||||
|
this.postProcessedFile = postProcessedFile;
|
||||||
|
}
|
||||||
|
|
||||||
public long getSizeInByte() {
|
public long getSizeInByte() {
|
||||||
return sizeInByte;
|
return sizeInByte;
|
||||||
}
|
}
|
||||||
|
@ -278,4 +293,8 @@ public class Recording implements Serializable {
|
||||||
public boolean canBePostProcessed() {
|
public boolean canBePostProcessed() {
|
||||||
return getStatus() == FAILED || getStatus() == WAITING || getStatus() == FINISHED;
|
return getStatus() == FAILED || getStatus() == WAITING || getStatus() == FINISHED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Set<String> getAssociatedFiles() {
|
||||||
|
return associatedFiles;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import ctbrec.event.EventHandlerConfiguration;
|
import ctbrec.event.EventHandlerConfiguration;
|
||||||
|
import ctbrec.recorder.postprocessing.PostProcessor;
|
||||||
|
|
||||||
public class Settings {
|
public class Settings {
|
||||||
|
|
||||||
|
@ -94,6 +95,7 @@ public class Settings {
|
||||||
public String password = ""; // chaturbate password TODO maybe rename this onetime
|
public String password = ""; // chaturbate password TODO maybe rename this onetime
|
||||||
public String postProcessing = "";
|
public String postProcessing = "";
|
||||||
public int postProcessingThreads = 2;
|
public int postProcessingThreads = 2;
|
||||||
|
public List<PostProcessor> postProcessors = new ArrayList<>();
|
||||||
public String proxyHost;
|
public String proxyHost;
|
||||||
public String proxyPassword;
|
public String proxyPassword;
|
||||||
public String proxyPort;
|
public String proxyPort;
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
package ctbrec.io;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
public class DevNull extends OutputStream {
|
||||||
|
@Override
|
||||||
|
public void write(int b) throws IOException {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(byte[] b) throws IOException {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(byte[] b, int off, int len) throws IOException {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package ctbrec.io;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonAdapter;
|
||||||
|
import com.squareup.moshi.JsonReader;
|
||||||
|
import com.squareup.moshi.JsonWriter;
|
||||||
|
|
||||||
|
public class FileJsonAdapter extends JsonAdapter<File> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public File fromJson(JsonReader reader) throws IOException {
|
||||||
|
String path = reader.nextString();
|
||||||
|
if (path != null) {
|
||||||
|
return new File(path);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void toJson(JsonWriter writer, File value) throws IOException {
|
||||||
|
if (value != null) {
|
||||||
|
writer.value(value.getCanonicalPath());
|
||||||
|
} else {
|
||||||
|
writer.nullValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
package ctbrec.io;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonAdapter;
|
||||||
|
import com.squareup.moshi.JsonReader;
|
||||||
|
import com.squareup.moshi.JsonReader.Token;
|
||||||
|
import com.squareup.moshi.JsonWriter;
|
||||||
|
|
||||||
|
import ctbrec.recorder.postprocessing.PostProcessor;
|
||||||
|
|
||||||
|
public class PostProcessorJsonAdapter extends JsonAdapter<PostProcessor> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PostProcessor fromJson(JsonReader reader) throws IOException {
|
||||||
|
reader.beginObject();
|
||||||
|
Object type = null;
|
||||||
|
PostProcessor postProcessor = null;
|
||||||
|
while(reader.hasNext()) {
|
||||||
|
try {
|
||||||
|
Token token = reader.peek();
|
||||||
|
if(token == Token.NAME) {
|
||||||
|
String key = reader.nextName();
|
||||||
|
if(key.equals("type")) {
|
||||||
|
type = reader.readJsonValue();
|
||||||
|
Class<?> modelClass = Class.forName(type.toString());
|
||||||
|
postProcessor = (PostProcessor) modelClass.getDeclaredConstructor().newInstance();
|
||||||
|
} else if(key.equals("config")) {
|
||||||
|
reader.beginObject();
|
||||||
|
} else {
|
||||||
|
String value = reader.nextString();
|
||||||
|
postProcessor.getConfig().put(key, value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reader.skipValue();
|
||||||
|
}
|
||||||
|
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) {
|
||||||
|
throw new IOException("Couldn't instantiate post-processor class [" + type + "]", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.endObject();
|
||||||
|
reader.endObject();
|
||||||
|
|
||||||
|
return postProcessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void toJson(JsonWriter writer, PostProcessor pp) throws IOException {
|
||||||
|
writer.beginObject();
|
||||||
|
writer.name("type").value(pp.getClass().getName());
|
||||||
|
writer.name("config");
|
||||||
|
writer.beginObject();
|
||||||
|
for (Entry<String, String> entry : pp.getConfig().entrySet()) {
|
||||||
|
writer.name(entry.getKey()).value(entry.getValue());
|
||||||
|
}
|
||||||
|
writer.endObject();
|
||||||
|
writer.endObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -53,6 +53,7 @@ import ctbrec.event.NoSpaceLeftEvent;
|
||||||
import ctbrec.event.RecordingStateChangedEvent;
|
import ctbrec.event.RecordingStateChangedEvent;
|
||||||
import ctbrec.io.HttpClient;
|
import ctbrec.io.HttpClient;
|
||||||
import ctbrec.recorder.download.Download;
|
import ctbrec.recorder.download.Download;
|
||||||
|
import ctbrec.recorder.postprocessing.PostProcessor;
|
||||||
import ctbrec.sites.Site;
|
import ctbrec.sites.Site;
|
||||||
|
|
||||||
public class NextGenLocalRecorder implements Recorder {
|
public class NextGenLocalRecorder implements Recorder {
|
||||||
|
@ -161,6 +162,10 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
setRecordingStatus(recording, State.POST_PROCESSING);
|
setRecordingStatus(recording, State.POST_PROCESSING);
|
||||||
recordingManager.saveRecording(recording);
|
recordingManager.saveRecording(recording);
|
||||||
recording.postprocess();
|
recording.postprocess();
|
||||||
|
List<PostProcessor> postProcessors = config.getSettings().postProcessors;
|
||||||
|
for (PostProcessor postProcessor : postProcessors) {
|
||||||
|
postProcessor.postprocess(recording);
|
||||||
|
}
|
||||||
setRecordingStatus(recording, State.FINISHED);
|
setRecordingStatus(recording, State.FINISHED);
|
||||||
recordingManager.saveRecording(recording);
|
recordingManager.saveRecording(recording);
|
||||||
deleteIfTooShort(recording);
|
deleteIfTooShort(recording);
|
||||||
|
@ -636,6 +641,7 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void rerunPostProcessing(Recording recording) {
|
public void rerunPostProcessing(Recording recording) {
|
||||||
|
recording.setPostProcessedFile(null);
|
||||||
List<Recording> recordings = recordingManager.getAll();
|
List<Recording> recordings = recordingManager.getAll();
|
||||||
for (Recording other : recordings) {
|
for (Recording other : recordings) {
|
||||||
if(other.equals(recording)) {
|
if(other.equals(recording)) {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import ctbrec.Config;
|
||||||
import ctbrec.Model;
|
import ctbrec.Model;
|
||||||
import ctbrec.Recording;
|
import ctbrec.Recording;
|
||||||
import ctbrec.Recording.State;
|
import ctbrec.Recording.State;
|
||||||
|
import ctbrec.io.FileJsonAdapter;
|
||||||
import ctbrec.io.InstantJsonAdapter;
|
import ctbrec.io.InstantJsonAdapter;
|
||||||
import ctbrec.io.ModelJsonAdapter;
|
import ctbrec.io.ModelJsonAdapter;
|
||||||
import ctbrec.sites.Site;
|
import ctbrec.sites.Site;
|
||||||
|
@ -43,6 +44,7 @@ public class RecordingManager {
|
||||||
moshi = new Moshi.Builder()
|
moshi = new Moshi.Builder()
|
||||||
.add(Model.class, new ModelJsonAdapter(sites))
|
.add(Model.class, new ModelJsonAdapter(sites))
|
||||||
.add(Instant.class, new InstantJsonAdapter())
|
.add(Instant.class, new InstantJsonAdapter())
|
||||||
|
.add(File.class, new FileJsonAdapter())
|
||||||
.build();
|
.build();
|
||||||
adapter = moshi.adapter(Recording.class).indent(" ");
|
adapter = moshi.adapter(Recording.class).indent(" ");
|
||||||
|
|
||||||
|
@ -122,20 +124,36 @@ public class RecordingManager {
|
||||||
recording.setStatus(State.DELETING);
|
recording.setStatus(State.DELETING);
|
||||||
File recordingsDir = new File(config.getSettings().recordingsDir);
|
File recordingsDir = new File(config.getSettings().recordingsDir);
|
||||||
File path = new File(recordingsDir, recording.getPath());
|
File path = new File(recordingsDir, recording.getPath());
|
||||||
|
boolean isFile = path.isFile();
|
||||||
LOG.debug("Deleting {}", path);
|
LOG.debug("Deleting {}", path);
|
||||||
|
|
||||||
// delete the video files
|
// delete the video files
|
||||||
if (path.isFile()) {
|
if (isFile) {
|
||||||
Files.delete(path.toPath());
|
Files.delete(path.toPath());
|
||||||
deleteEmptyParents(path.getParentFile());
|
|
||||||
} else {
|
} else {
|
||||||
deleteDirectory(path);
|
deleteDirectory(path);
|
||||||
deleteEmptyParents(path);
|
}
|
||||||
|
|
||||||
|
// delete files associated with this recording
|
||||||
|
for (String associated : recording.getAssociatedFiles()) {
|
||||||
|
File f = new File(associated);
|
||||||
|
if (f.isFile()) {
|
||||||
|
Files.delete(f.toPath());
|
||||||
|
} else {
|
||||||
|
deleteDirectory(f);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete the meta data
|
// delete the meta data
|
||||||
Files.deleteIfExists(new File(recording.getMetaDataFile()).toPath());
|
Files.deleteIfExists(new File(recording.getMetaDataFile()).toPath());
|
||||||
|
|
||||||
|
// delete empty parent files
|
||||||
|
if (isFile) {
|
||||||
|
deleteEmptyParents(path.getParentFile());
|
||||||
|
} else {
|
||||||
|
deleteEmptyParents(path);
|
||||||
|
}
|
||||||
|
|
||||||
// remove from data structure
|
// remove from data structure
|
||||||
recordings.remove(recording);
|
recordings.remove(recording);
|
||||||
recording.setStatus(State.DELETED);
|
recording.setStatus(State.DELETED);
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
package ctbrec.recorder.postprocessing;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public abstract class AbstractPostProcessor implements PostProcessor {
|
||||||
|
|
||||||
|
private Map<String, String> config = new HashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, String> getConfig() {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setConfig(Map<String, String> conf) {
|
||||||
|
this.config = conf;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return getName();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package ctbrec.recorder.postprocessing;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import ctbrec.Recording;
|
||||||
|
|
||||||
|
public interface PostProcessor extends Serializable {
|
||||||
|
String getName();
|
||||||
|
|
||||||
|
void postprocess(Recording rec) throws IOException, InterruptedException;
|
||||||
|
|
||||||
|
Map<String, String> getConfig();
|
||||||
|
void setConfig(Map<String, String> conf);
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package ctbrec.recorder.postprocessing;
|
||||||
|
|
||||||
|
import ctbrec.Recording;
|
||||||
|
|
||||||
|
public class RecordingRenamer extends AbstractPostProcessor {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "rename";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postprocess(Recording rec) {
|
||||||
|
// TODO rename
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
package ctbrec.recorder.postprocessing;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import ctbrec.OS;
|
||||||
|
import ctbrec.Recording;
|
||||||
|
import ctbrec.io.StreamRedirectThread;
|
||||||
|
import ctbrec.recorder.download.ProcessExitedUncleanException;
|
||||||
|
|
||||||
|
public class Remuxer extends AbstractPostProcessor {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(Remuxer.class);
|
||||||
|
|
||||||
|
public static final String FFMPEG_ARGS = "ffmpeg.args";
|
||||||
|
public static final String FILE_EXT = "file.ext";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "remux / transcode";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postprocess(Recording rec) throws IOException, InterruptedException {
|
||||||
|
String fileExt = getConfig().get(FILE_EXT);
|
||||||
|
String[] args = getConfig().get(FFMPEG_ARGS).split(" ");
|
||||||
|
String[] argsPlusFile = new String[args.length + 3];
|
||||||
|
int i = 0;
|
||||||
|
argsPlusFile[i++] = "-i";
|
||||||
|
argsPlusFile[i++] = rec.getPostProcessedFile().getAbsolutePath();
|
||||||
|
System.arraycopy(args, 0, argsPlusFile, i, args.length);
|
||||||
|
File remuxedFile = new File(rec.getPostProcessedFile().getAbsolutePath() + '.' + fileExt);
|
||||||
|
argsPlusFile[argsPlusFile.length - 1] = remuxedFile.getAbsolutePath();
|
||||||
|
String[] cmdline = OS.getFFmpegCommand(argsPlusFile);
|
||||||
|
LOG.debug(Arrays.toString(cmdline));
|
||||||
|
Process ffmpeg = Runtime.getRuntime().exec(cmdline, new String[0], rec.getPostProcessedFile().getParentFile());
|
||||||
|
setupLogging(ffmpeg, rec);
|
||||||
|
rec.setPostProcessedFile(remuxedFile);
|
||||||
|
rec.getAssociatedFiles().add(remuxedFile.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupLogging(Process ffmpeg, Recording rec) throws IOException, InterruptedException {
|
||||||
|
int exitCode = 1;
|
||||||
|
File video = rec.getPostProcessedFile();
|
||||||
|
File ffmpegLog = new File(video.getParentFile(), video.getName() + ".ffmpeg.log");
|
||||||
|
try (FileOutputStream mergeLogStream = new FileOutputStream(ffmpegLog)) {
|
||||||
|
Thread stdout = new Thread(new StreamRedirectThread(ffmpeg.getInputStream(), mergeLogStream));
|
||||||
|
Thread stderr = new Thread(new StreamRedirectThread(ffmpeg.getErrorStream(), mergeLogStream));
|
||||||
|
stdout.start();
|
||||||
|
stderr.start();
|
||||||
|
exitCode = ffmpeg.waitFor();
|
||||||
|
LOG.debug("FFmpeg exited with code {}", exitCode);
|
||||||
|
stdout.join();
|
||||||
|
stderr.join();
|
||||||
|
mergeLogStream.flush();
|
||||||
|
}
|
||||||
|
if (exitCode != 1) {
|
||||||
|
if (ffmpegLog.exists()) {
|
||||||
|
Files.delete(ffmpegLog.toPath());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rec.getAssociatedFiles().add(ffmpegLog.getAbsolutePath());
|
||||||
|
LOG.info("FFmpeg exit code was {}. Logfile: {}", exitCode, ffmpegLog.getAbsolutePath());
|
||||||
|
throw new ProcessExitedUncleanException("FFmpeg exit code was " + exitCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
String s = getName();
|
||||||
|
if(getConfig().containsKey(FFMPEG_ARGS)) {
|
||||||
|
s += " [" + getConfig().get(FFMPEG_ARGS) + ']';
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue