diff --git a/client/src/main/java/ctbrec/ui/settings/AbstractPostProcessingPaneFactory.java b/client/src/main/java/ctbrec/ui/settings/AbstractPostProcessingPaneFactory.java new file mode 100644 index 00000000..37515cd6 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/AbstractPostProcessingPaneFactory.java @@ -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> 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; + } + + } +} diff --git a/client/src/main/java/ctbrec/ui/settings/PostProcessingDialogFactory.java b/client/src/main/java/ctbrec/ui/settings/PostProcessingDialogFactory.java new file mode 100644 index 00000000..f3bb74ed --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/PostProcessingDialogFactory.java @@ -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> ppToDialogMap = new HashMap<>(); + static { + ppToDialogMap.put(Remuxer.class, RemuxerPaneFactory.class); + } + + private PostProcessingDialogFactory() { + } + + public static void openNewDialog(PostProcessor pp, Config config, Scene scene, ObservableList stepList) { + openDialog(pp, config, scene, stepList, true); + } + + public static void openEditDialog(PostProcessor pp, Config config, Scene scene, ObservableList stepList) { + openDialog(pp, config, scene, stepList, false); + } + + private static void openDialog(PostProcessor pp, Config config, Scene scene, ObservableList 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); + } +} diff --git a/client/src/main/java/ctbrec/ui/settings/PostProcessingStepPanel.java b/client/src/main/java/ctbrec/ui/settings/PostProcessingStepPanel.java new file mode 100644 index 00000000..207515e7 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/PostProcessingStepPanel.java @@ -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 stepListView; + ObservableList 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) 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 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 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; + } +} diff --git a/client/src/main/java/ctbrec/ui/settings/RemuxerPaneFactory.java b/client/src/main/java/ctbrec/ui/settings/RemuxerPaneFactory.java new file mode 100644 index 00000000..886182ad --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/RemuxerPaneFactory.java @@ -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) + ) + ); + } + +} diff --git a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java index f83a38f7..95f78da1 100644 --- a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java @@ -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)), diff --git a/client/src/main/java/ctbrec/ui/settings/api/Preferences.java b/client/src/main/java/ctbrec/ui/settings/api/Preferences.java index 9b096406..d8c9bb7d 100644 --- a/client/src/main/java/ctbrec/ui/settings/api/Preferences.java +++ b/client/src/main/java/ctbrec/ui/settings/api/Preferences.java @@ -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 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 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 obs, String oldV, String newV) { String q = ofNullable(newV).orElse("").toLowerCase().trim(); TreeItem 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 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()) { diff --git a/common/src/main/java/ctbrec/Config.java b/common/src/main/java/ctbrec/Config.java index a7caec1e..c18b3a96 100644 --- a/common/src/main/java/ctbrec/Config.java +++ b/common/src/main/java/ctbrec/Config.java @@ -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 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 adapter = moshi.adapter(Settings.class).indent(" "); String json = adapter.toJson(settings); diff --git a/common/src/main/java/ctbrec/Recording.java b/common/src/main/java/ctbrec/Recording.java index e6f60bf8..508aee27 100644 --- a/common/src/main/java/ctbrec/Recording.java +++ b/common/src/main/java/ctbrec/Recording.java @@ -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 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 getAssociatedFiles() { + return associatedFiles; + } } diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index e6c7e5fc..ba7feac0 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -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 postProcessors = new ArrayList<>(); public String proxyHost; public String proxyPassword; public String proxyPort; diff --git a/common/src/main/java/ctbrec/io/DevNull.java b/common/src/main/java/ctbrec/io/DevNull.java new file mode 100644 index 00000000..b40772b9 --- /dev/null +++ b/common/src/main/java/ctbrec/io/DevNull.java @@ -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 { + } +} diff --git a/common/src/main/java/ctbrec/io/FileJsonAdapter.java b/common/src/main/java/ctbrec/io/FileJsonAdapter.java new file mode 100644 index 00000000..e3d38733 --- /dev/null +++ b/common/src/main/java/ctbrec/io/FileJsonAdapter.java @@ -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 { + + @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(); + } + } +} diff --git a/common/src/main/java/ctbrec/io/PostProcessorJsonAdapter.java b/common/src/main/java/ctbrec/io/PostProcessorJsonAdapter.java new file mode 100644 index 00000000..d5e1f937 --- /dev/null +++ b/common/src/main/java/ctbrec/io/PostProcessorJsonAdapter.java @@ -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 { + + @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 entry : pp.getConfig().entrySet()) { + writer.name(entry.getKey()).value(entry.getValue()); + } + writer.endObject(); + writer.endObject(); + } + +} diff --git a/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java b/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java index f337c7c6..bffec23e 100644 --- a/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java @@ -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 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 recordings = recordingManager.getAll(); for (Recording other : recordings) { if(other.equals(recording)) { diff --git a/common/src/main/java/ctbrec/recorder/RecordingManager.java b/common/src/main/java/ctbrec/recorder/RecordingManager.java index a710344d..e746b011 100644 --- a/common/src/main/java/ctbrec/recorder/RecordingManager.java +++ b/common/src/main/java/ctbrec/recorder/RecordingManager.java @@ -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); diff --git a/common/src/main/java/ctbrec/recorder/postprocessing/AbstractPostProcessor.java b/common/src/main/java/ctbrec/recorder/postprocessing/AbstractPostProcessor.java new file mode 100644 index 00000000..4d26953c --- /dev/null +++ b/common/src/main/java/ctbrec/recorder/postprocessing/AbstractPostProcessor.java @@ -0,0 +1,24 @@ +package ctbrec.recorder.postprocessing; + +import java.util.HashMap; +import java.util.Map; + +public abstract class AbstractPostProcessor implements PostProcessor { + + private Map config = new HashMap<>(); + + @Override + public Map getConfig() { + return config; + } + + @Override + public void setConfig(Map conf) { + this.config = conf; + } + + @Override + public String toString() { + return getName(); + } +} diff --git a/common/src/main/java/ctbrec/recorder/postprocessing/PostProcessor.java b/common/src/main/java/ctbrec/recorder/postprocessing/PostProcessor.java new file mode 100644 index 00000000..cc366520 --- /dev/null +++ b/common/src/main/java/ctbrec/recorder/postprocessing/PostProcessor.java @@ -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 getConfig(); + void setConfig(Map conf); +} diff --git a/common/src/main/java/ctbrec/recorder/postprocessing/RecordingRenamer.java b/common/src/main/java/ctbrec/recorder/postprocessing/RecordingRenamer.java new file mode 100644 index 00000000..ed771e0d --- /dev/null +++ b/common/src/main/java/ctbrec/recorder/postprocessing/RecordingRenamer.java @@ -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 + } +} diff --git a/common/src/main/java/ctbrec/recorder/postprocessing/Remuxer.java b/common/src/main/java/ctbrec/recorder/postprocessing/Remuxer.java new file mode 100644 index 00000000..4ca87c95 --- /dev/null +++ b/common/src/main/java/ctbrec/recorder/postprocessing/Remuxer.java @@ -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; + } +}