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("Threads", postProcessingThreads),
|
||||
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)),
|
||||
|
|
|
@ -2,6 +2,7 @@ package ctbrec.ui.settings.api;
|
|||
|
||||
import static java.util.Optional.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
@ -21,6 +22,7 @@ import javafx.scene.control.TreeView;
|
|||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
public class Preferences {
|
||||
|
@ -31,7 +33,10 @@ public class Preferences {
|
|||
|
||||
private TreeView<Category> categoryTree;
|
||||
|
||||
private PreferencesStorage preferencesStorage;
|
||||
|
||||
private Preferences(PreferencesStorage preferencesStorage, Category...categories) {
|
||||
this.preferencesStorage = preferencesStorage;
|
||||
this.categories = categories;
|
||||
for (Category category : categories) {
|
||||
assignPreferencesStorage(category, preferencesStorage);
|
||||
|
@ -56,15 +61,15 @@ public class Preferences {
|
|||
return new Preferences(preferencesStorage, categories);
|
||||
}
|
||||
|
||||
public void save() {
|
||||
throw new RuntimeException("save not implemented");
|
||||
public void save() throws IOException {
|
||||
preferencesStorage.save(this);
|
||||
}
|
||||
|
||||
Category[] getCategories() {
|
||||
return categories;
|
||||
}
|
||||
|
||||
public Node getView() {
|
||||
public Region getView(boolean withNavigation) {
|
||||
SearchBox search = new SearchBox(true);
|
||||
search.textProperty().addListener(this::filterTree);
|
||||
TreeItem<Category> categoryTreeItems = createCategoryTree(categories, new TreeItem<>(), null);
|
||||
|
@ -76,7 +81,9 @@ public class Preferences {
|
|||
VBox.setMargin(categoryTree, new Insets(2));
|
||||
|
||||
BorderPane main = new BorderPane();
|
||||
main.setLeft(leftSide);
|
||||
if (withNavigation) {
|
||||
main.setLeft(leftSide);
|
||||
}
|
||||
main.setCenter(new Label("Center"));
|
||||
BorderPane.setMargin(leftSide, new Insets(2));
|
||||
|
||||
|
@ -92,6 +99,10 @@ public class Preferences {
|
|||
return main;
|
||||
}
|
||||
|
||||
public Region getView() {
|
||||
return getView(true);
|
||||
}
|
||||
|
||||
private void filterTree(ObservableValue<? extends String> obs, String oldV, String newV) {
|
||||
String q = ofNullable(newV).orElse("").toLowerCase().trim();
|
||||
TreeItem<Category> filteredCategoryTree = createCategoryTree(categories, new TreeItem<>(), q);
|
||||
|
@ -151,6 +162,8 @@ public class Preferences {
|
|||
|
||||
private Node createGrid(Setting[] settings) throws Exception {
|
||||
GridPane pane = new GridPane();
|
||||
pane.setHgap(2);
|
||||
pane.vgapProperty().bind(pane.hgapProperty());
|
||||
int row = 0;
|
||||
for (Setting setting : settings) {
|
||||
Node node = setting.getGui();
|
||||
|
@ -198,11 +211,9 @@ public class Preferences {
|
|||
}
|
||||
|
||||
private void visit(Category cat, Consumer<Setting> visitor) {
|
||||
if (cat.hasGroups()) {
|
||||
for (Group group : cat.getGroups()) {
|
||||
for (Setting setting : group.getSettings()) {
|
||||
visitor.accept(setting);
|
||||
}
|
||||
for (Group group : cat.getGroups()) {
|
||||
for (Setting setting : group.getSettings()) {
|
||||
visitor.accept(setting);
|
||||
}
|
||||
}
|
||||
if (cat.hasSubCategories()) {
|
||||
|
|
|
@ -23,7 +23,10 @@ import org.slf4j.LoggerFactory;
|
|||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.Moshi;
|
||||
|
||||
import ctbrec.io.FileJsonAdapter;
|
||||
import ctbrec.io.ModelJsonAdapter;
|
||||
import ctbrec.io.PostProcessorJsonAdapter;
|
||||
import ctbrec.recorder.postprocessing.PostProcessor;
|
||||
import ctbrec.sites.Site;
|
||||
|
||||
public class Config {
|
||||
|
@ -55,6 +58,8 @@ public class Config {
|
|||
private void load() throws IOException {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(Model.class, new ModelJsonAdapter(sites))
|
||||
.add(PostProcessor.class, new PostProcessorJsonAdapter())
|
||||
.add(File.class, new FileJsonAdapter())
|
||||
.build();
|
||||
JsonAdapter<Settings> adapter = moshi.adapter(Settings.class).lenient();
|
||||
File configFile = new File(configDir, filename);
|
||||
|
@ -125,6 +130,8 @@ public class Config {
|
|||
public void save() throws IOException {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(Model.class, new ModelJsonAdapter())
|
||||
.add(PostProcessor.class, new PostProcessorJsonAdapter())
|
||||
.add(File.class, new FileJsonAdapter())
|
||||
.build();
|
||||
JsonAdapter<Settings> adapter = moshi.adapter(Settings.class).indent(" ");
|
||||
String json = adapter.toJson(settings);
|
||||
|
|
|
@ -17,6 +17,8 @@ import java.time.LocalDateTime;
|
|||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -39,6 +41,8 @@ public class Recording implements Serializable {
|
|||
private boolean singleFile = false;
|
||||
private boolean pinned = false;
|
||||
private String note;
|
||||
private Set<String> associatedFiles = new HashSet<>();
|
||||
private File postProcessedFile = null;
|
||||
|
||||
public enum State {
|
||||
RECORDING("recording"),
|
||||
|
@ -107,6 +111,17 @@ public class Recording implements Serializable {
|
|||
return recordingsFile;
|
||||
}
|
||||
|
||||
public File getPostProcessedFile() {
|
||||
if (postProcessedFile == null) {
|
||||
setPostProcessedFile(getAbsoluteFile());
|
||||
}
|
||||
return postProcessedFile;
|
||||
}
|
||||
|
||||
public void setPostProcessedFile(File postProcessedFile) {
|
||||
this.postProcessedFile = postProcessedFile;
|
||||
}
|
||||
|
||||
public long getSizeInByte() {
|
||||
return sizeInByte;
|
||||
}
|
||||
|
@ -278,4 +293,8 @@ public class Recording implements Serializable {
|
|||
public boolean canBePostProcessed() {
|
||||
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 ctbrec.event.EventHandlerConfiguration;
|
||||
import ctbrec.recorder.postprocessing.PostProcessor;
|
||||
|
||||
public class Settings {
|
||||
|
||||
|
@ -94,6 +95,7 @@ public class Settings {
|
|||
public String password = ""; // chaturbate password TODO maybe rename this onetime
|
||||
public String postProcessing = "";
|
||||
public int postProcessingThreads = 2;
|
||||
public List<PostProcessor> postProcessors = new ArrayList<>();
|
||||
public String proxyHost;
|
||||
public String proxyPassword;
|
||||
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.io.HttpClient;
|
||||
import ctbrec.recorder.download.Download;
|
||||
import ctbrec.recorder.postprocessing.PostProcessor;
|
||||
import ctbrec.sites.Site;
|
||||
|
||||
public class NextGenLocalRecorder implements Recorder {
|
||||
|
@ -161,6 +162,10 @@ public class NextGenLocalRecorder implements Recorder {
|
|||
setRecordingStatus(recording, State.POST_PROCESSING);
|
||||
recordingManager.saveRecording(recording);
|
||||
recording.postprocess();
|
||||
List<PostProcessor> postProcessors = config.getSettings().postProcessors;
|
||||
for (PostProcessor postProcessor : postProcessors) {
|
||||
postProcessor.postprocess(recording);
|
||||
}
|
||||
setRecordingStatus(recording, State.FINISHED);
|
||||
recordingManager.saveRecording(recording);
|
||||
deleteIfTooShort(recording);
|
||||
|
@ -636,6 +641,7 @@ public class NextGenLocalRecorder implements Recorder {
|
|||
|
||||
@Override
|
||||
public void rerunPostProcessing(Recording recording) {
|
||||
recording.setPostProcessedFile(null);
|
||||
List<Recording> recordings = recordingManager.getAll();
|
||||
for (Recording other : recordings) {
|
||||
if(other.equals(recording)) {
|
||||
|
|
|
@ -25,6 +25,7 @@ import ctbrec.Config;
|
|||
import ctbrec.Model;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.Recording.State;
|
||||
import ctbrec.io.FileJsonAdapter;
|
||||
import ctbrec.io.InstantJsonAdapter;
|
||||
import ctbrec.io.ModelJsonAdapter;
|
||||
import ctbrec.sites.Site;
|
||||
|
@ -43,6 +44,7 @@ public class RecordingManager {
|
|||
moshi = new Moshi.Builder()
|
||||
.add(Model.class, new ModelJsonAdapter(sites))
|
||||
.add(Instant.class, new InstantJsonAdapter())
|
||||
.add(File.class, new FileJsonAdapter())
|
||||
.build();
|
||||
adapter = moshi.adapter(Recording.class).indent(" ");
|
||||
|
||||
|
@ -122,20 +124,36 @@ public class RecordingManager {
|
|||
recording.setStatus(State.DELETING);
|
||||
File recordingsDir = new File(config.getSettings().recordingsDir);
|
||||
File path = new File(recordingsDir, recording.getPath());
|
||||
boolean isFile = path.isFile();
|
||||
LOG.debug("Deleting {}", path);
|
||||
|
||||
// delete the video files
|
||||
if (path.isFile()) {
|
||||
if (isFile) {
|
||||
Files.delete(path.toPath());
|
||||
deleteEmptyParents(path.getParentFile());
|
||||
} else {
|
||||
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
|
||||
Files.deleteIfExists(new File(recording.getMetaDataFile()).toPath());
|
||||
|
||||
// delete empty parent files
|
||||
if (isFile) {
|
||||
deleteEmptyParents(path.getParentFile());
|
||||
} else {
|
||||
deleteEmptyParents(path);
|
||||
}
|
||||
|
||||
// remove from data structure
|
||||
recordings.remove(recording);
|
||||
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