forked from j62/ctbrec
1
0
Fork 0

Add first configurable PP step

This commit is contained in:
0xb00bface 2020-08-22 18:31:25 +02:00
parent 4f8e7dbca2
commit 17a32cd928
18 changed files with 634 additions and 13 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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