forked from j62/ctbrec
1
0
Fork 0

Merge branch 'pp' into dev

This commit is contained in:
0xb00bface 2020-09-27 15:25:49 +02:00
commit 8b6d246732
68 changed files with 2476 additions and 470 deletions

View File

@ -1,3 +1,8 @@
3.10.0
========================
* New post-processing
* Fix: MV Live models with spaces in the name not indicated as recording
3.9.0 3.9.0
======================== ========================
* Added support for Manyvids Live. * Added support for Manyvids Live.

View File

@ -8,7 +8,7 @@
<parent> <parent>
<groupId>ctbrec</groupId> <groupId>ctbrec</groupId>
<artifactId>master</artifactId> <artifactId>master</artifactId>
<version>3.9.0</version> <version>3.10.0</version>
<relativePath>../master</relativePath> <relativePath>../master</relativePath>
</parent> </parent>

View File

@ -1,5 +1,6 @@
package ctbrec.ui; package ctbrec.ui;
import java.io.File;
import java.time.Instant; import java.time.Instant;
import ctbrec.Config; import ctbrec.Config;
@ -156,11 +157,7 @@ public class JavaFxRecording extends Recording {
setStatus(updated.getStatus()); setStatus(updated.getStatus());
setProgress(updated.getProgress()); setProgress(updated.getProgress());
setSizeInByte(updated.getSizeInByte()); setSizeInByte(updated.getSizeInByte());
} setSingleFile(updated.isSingleFile());
@Override
public String getPath() {
return delegate.getPath();
} }
@Override @Override
@ -192,6 +189,11 @@ public class JavaFxRecording extends Recording {
return delegate.isSingleFile(); return delegate.isSingleFile();
} }
@Override
public void setSingleFile(boolean singleFile) {
delegate.setSingleFile(singleFile);
}
@Override @Override
public boolean isPinned() { public boolean isPinned() {
return delegate.isPinned(); return delegate.isPinned();
@ -223,4 +225,26 @@ public class JavaFxRecording extends Recording {
public StringProperty getNoteProperty() { public StringProperty getNoteProperty() {
return notesProperty; return notesProperty;
} }
@Override
public File getAbsoluteFile() {
return delegate.getAbsoluteFile();
}
@Override
public void setAbsoluteFile(File absoluteFile) {
delegate.setAbsoluteFile(absoluteFile);
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public void setId(String id) {
delegate.setId(id);
}
} }

View File

@ -152,7 +152,7 @@ public class Player {
Config cfg = Config.getInstance(); Config cfg = Config.getInstance();
try { try {
if (cfg.getSettings().localRecording && rec != null) { if (cfg.getSettings().localRecording && rec != null) {
File file = new File(cfg.getSettings().recordingsDir, rec.getPath()); File file = rec.getAbsoluteFile();
String[] cmdline = createCmdline(file.getAbsolutePath()); String[] cmdline = createCmdline(file.getAbsolutePath());
playerProcess = rt.exec(cmdline, OS.getEnvironment(), file.getParentFile()); playerProcess = rt.exec(cmdline, OS.getEnvironment(), file.getParentFile());
} else { } else {
@ -206,7 +206,7 @@ public class Player {
private String getRemoteRecordingUrl(Recording rec, Config cfg) private String getRemoteRecordingUrl(Recording rec, Config cfg)
throws MalformedURLException, InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException { throws MalformedURLException, InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
String hlsBase = Config.getInstance().getServerUrl() + "/hls"; String hlsBase = Config.getInstance().getServerUrl() + "/hls";
String recUrl = hlsBase + rec.getPath() + (rec.isSingleFile() ? "" : "/playlist.m3u8"); String recUrl = hlsBase + '/' + rec.getId() + (rec.isSingleFile() ? "" : "/playlist.m3u8");
if (cfg.getSettings().requireAuthentication) { if (cfg.getSettings().requireAuthentication) {
URL u = new URL(recUrl); URL u = new URL(recUrl);
String path = u.getPath(); String path = u.getPath();

View File

@ -0,0 +1,194 @@
package ctbrec.ui.settings;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.recorder.postprocessing.PostProcessor;
import ctbrec.ui.controls.DirectorySelectionBox;
import ctbrec.ui.controls.ProgramSelectionBox;
import ctbrec.ui.settings.api.ExclusiveSelectionProperty;
import ctbrec.ui.settings.api.Preferences;
import ctbrec.ui.settings.api.PreferencesStorage;
import ctbrec.ui.settings.api.Setting;
import ctbrec.ui.settings.api.SimpleDirectoryProperty;
import ctbrec.ui.settings.api.SimpleFileProperty;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ListProperty;
import javafx.beans.property.LongProperty;
import javafx.beans.property.Property;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.HBox;
import javafx.util.converter.NumberStringConverter;
public abstract class AbstractPostProcessingPaneFactory {
private static final Logger LOG = LoggerFactory.getLogger(AbstractPostProcessingPaneFactory.class);
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();
LOG.debug("{}={}", key, value.toString());
pp.getConfig().put(key, value.toString());
}
}
@Override
public void load(Preferences preferences) {
// no op
}
@Override
public Node createGui(Setting setting) throws Exception {
Property<?> prop = setting.getProperty();
if (prop instanceof ExclusiveSelectionProperty) {
return createRadioGroup(setting);
} else if (prop instanceof SimpleDirectoryProperty) {
return createDirectorySelector(setting);
} else if (prop instanceof SimpleFileProperty) {
return createFileSelector(setting);
} else if (prop instanceof IntegerProperty) {
return createIntegerProperty(setting);
} else if (prop instanceof LongProperty) {
return createLongProperty(setting);
} else if (prop instanceof BooleanProperty) {
return createBooleanProperty(setting);
} else if (prop instanceof ListProperty) {
return createComboBox(setting);
} else if (prop instanceof StringProperty) {
return createStringProperty(setting);
} else {
return new Label("Unsupported Type for key " + setting.getKey() + ": " + setting.getProperty());
}
}
}
private Node createRadioGroup(Setting setting) {
ExclusiveSelectionProperty prop = (ExclusiveSelectionProperty) setting.getProperty();
ToggleGroup toggleGroup = new ToggleGroup();
RadioButton optionA = new RadioButton(prop.getOptionA());
optionA.setSelected(prop.getValue());
optionA.setToggleGroup(toggleGroup);
RadioButton optionB = new RadioButton(prop.getOptionB());
optionB.setSelected(!optionA.isSelected());
optionB.setToggleGroup(toggleGroup);
optionA.selectedProperty().bindBidirectional(prop);
HBox row = new HBox();
row.getChildren().addAll(optionA, optionB);
HBox.setMargin(optionA, new Insets(5));
HBox.setMargin(optionB, new Insets(5));
return row;
}
private Node createFileSelector(Setting setting) {
ProgramSelectionBox programSelector = new ProgramSelectionBox("");
// programSelector.fileProperty().addListener((obs, o, n) -> saveValue(() -> {
// String path = n;
// Field field = Settings.class.getField(setting.getKey());
// String oldValue = (String) field.get(settings);
// if (!Objects.equals(path, oldValue)) {
// field.set(settings, path);
// config.save();
// }
// }));
StringProperty property = (StringProperty) setting.getProperty();
programSelector.fileProperty().bindBidirectional(property);
return programSelector;
}
private Node createDirectorySelector(Setting setting) {
DirectorySelectionBox directorySelector = new DirectorySelectionBox("");
directorySelector.prefWidth(400);
// directorySelector.fileProperty().addListener((obs, o, n) -> saveValue(() -> {
// String path = n;
// Field field = Settings.class.getField(setting.getKey());
// String oldValue = (String) field.get(settings);
// if (!Objects.equals(path, oldValue)) {
// field.set(settings, path);
// config.save();
// }
// }));
StringProperty property = (StringProperty) setting.getProperty();
directorySelector.fileProperty().bindBidirectional(property);
return directorySelector;
}
@SuppressWarnings("unchecked")
private Node createStringProperty(Setting setting) {
TextField ctrl = new TextField();
ctrl.textProperty().bindBidirectional(setting.getProperty());
return ctrl;
}
@SuppressWarnings("unchecked")
private Node createIntegerProperty(Setting setting) {
TextField ctrl = new TextField();
Property<Number> prop = setting.getProperty();
ctrl.textProperty().bindBidirectional(prop, new NumberStringConverter());
return ctrl;
}
@SuppressWarnings("unchecked")
private Node createLongProperty(Setting setting) {
TextField ctrl = new TextField();
Property<Number> prop = setting.getProperty();
ctrl.textProperty().bindBidirectional(prop, new NumberStringConverter());
return ctrl;
}
private Node createBooleanProperty(Setting setting) {
CheckBox ctrl = new CheckBox();
BooleanProperty prop = (BooleanProperty) setting.getProperty();
ctrl.selectedProperty().bindBidirectional(prop);
return ctrl;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private Node createComboBox(Setting setting) throws NoSuchFieldException, IllegalAccessException {
ListProperty<?> listProp = (ListProperty<?>) setting.getProperty();
ComboBox<Object> comboBox = new ComboBox(listProp);
// Field field = Settings.class.getField(setting.getKey());
// Object value = field.get(Config.getInstance().getSettings());
// if (StringUtil.isNotBlank(value.toString())) {
// if (setting.getConverter() != null) {
// comboBox.getSelectionModel().select(setting.getConverter().convertTo(value));
// } else {
// comboBox.getSelectionModel().select(value);
// }
// }
// comboBox.valueProperty().addListener((obs, oldV, newV) -> saveValue(() -> {
// if (setting.getConverter() != null) {
// field.set(settings, setting.getConverter().convertFrom(newV));
// } else {
// field.set(settings, newV);
// }
// config.save();
// }));
return comboBox;
}
}

View File

@ -0,0 +1,24 @@
package ctbrec.ui.settings;
import ctbrec.recorder.postprocessing.DeleteTooShort;
import ctbrec.recorder.postprocessing.PostProcessor;
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 DeleteTooShortPaneFactory extends AbstractPostProcessingPaneFactory {
@Override
public Preferences doCreatePostProcessorPane(PostProcessor pp) {
SimpleStringProperty minimumLengthInSeconds = new SimpleStringProperty(null, DeleteTooShort.MIN_LEN_IN_SECS, pp.getConfig().getOrDefault(DeleteTooShort.MIN_LEN_IN_SECS, "10"));
properties.add(minimumLengthInSeconds);
return Preferences.of(new MapPreferencesStorage(),
Category.of(pp.getName(),
Setting.of("Minimum length in seconds", minimumLengthInSeconds)
)
);
}
}

View File

@ -0,0 +1,24 @@
package ctbrec.ui.settings;
import ctbrec.recorder.postprocessing.Move;
import ctbrec.recorder.postprocessing.PostProcessor;
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 MoverPaneFactory extends AbstractPostProcessingPaneFactory {
@Override
public Preferences doCreatePostProcessorPane(PostProcessor pp) {
SimpleStringProperty pathTemplate = new SimpleStringProperty(null, Move.PATH_TEMPLATE, pp.getConfig().getOrDefault(Move.PATH_TEMPLATE, Move.DEFAULT));
properties.add(pathTemplate);
return Preferences.of(new MapPreferencesStorage(),
Category.of(pp.getName(),
Setting.of("Directory", pathTemplate)
)
);
}
}

View File

@ -0,0 +1,77 @@
package ctbrec.ui.settings;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import ctbrec.Config;
import ctbrec.recorder.postprocessing.DeleteTooShort;
import ctbrec.recorder.postprocessing.Move;
import ctbrec.recorder.postprocessing.PostProcessor;
import ctbrec.recorder.postprocessing.Remux;
import ctbrec.recorder.postprocessing.Rename;
import ctbrec.recorder.postprocessing.Script;
import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.settings.api.Preferences;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.layout.Region;
public class PostProcessingDialogFactory {
static Map<Class<?>, Class<?>> ppToDialogMap = new HashMap<>();
static {
ppToDialogMap.put(Remux.class, RemuxerPaneFactory.class);
ppToDialogMap.put(Script.class, ScriptPaneFactory.class);
ppToDialogMap.put(Rename.class, RenamerPaneFactory.class);
ppToDialogMap.put(Move.class, MoverPaneFactory.class);
ppToDialogMap.put(DeleteTooShort.class, DeleteTooShortPaneFactory.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 {
Optional<Preferences> preferences = createPreferences(pp);
if(preferences.isPresent()) {
Region view = preferences.get().getView(false);
view.setMinWidth(600);
ok = Dialogs.showCustomInput(scene, "Configure " + pp.getName(), view);
if (ok) {
preferences.get().save();
if (newEntry) {
stepList.add(pp);
}
}
} else 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 Optional<Preferences> createPreferences(PostProcessor pp) throws InstantiationException, IllegalAccessException, IllegalArgumentException,
InvocationTargetException, NoSuchMethodException, SecurityException {
Class<?> paneFactoryClass = ppToDialogMap.get(pp.getClass());
if (paneFactoryClass != null) {
AbstractPostProcessingPaneFactory factory = (AbstractPostProcessingPaneFactory) paneFactoryClass.getDeclaredConstructor().newInstance();
return Optional.of(factory.createPostProcessorPane(pp));
} else {
return Optional.empty();
}
}
}

View File

@ -0,0 +1,199 @@
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.Copy;
import ctbrec.recorder.postprocessing.DeleteOriginal;
import ctbrec.recorder.postprocessing.DeleteTooShort;
import ctbrec.recorder.postprocessing.Move;
import ctbrec.recorder.postprocessing.PostProcessor;
import ctbrec.recorder.postprocessing.RemoveKeepFile;
import ctbrec.recorder.postprocessing.Remux;
import ctbrec.recorder.postprocessing.Rename;
import ctbrec.recorder.postprocessing.Script;
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<?>[] { // @formatter: off
Copy.class,
Rename.class,
Move.class,
Remux.class,
Script.class,
DeleteOriginal.class,
DeleteTooShort.class,
RemoveKeepFile.class
}; // @formatter: on
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));
saveConfig();
});
return add;
}
private void saveConfig() {
try {
config.save();
} catch (IOException e) {
Dialogs.showError("Post-Processing", "Couldn't save post-processing step", e);
}
}
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();
saveConfig();
});
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.Remux;
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, Remux.FFMPEG_ARGS, pp.getConfig().getOrDefault(Remux.FFMPEG_ARGS, "-c:v copy -c:a copy -movflags faststart -y -f mp4"));
SimpleStringProperty fileExt = new SimpleStringProperty(null, Remux.FILE_EXT, pp.getConfig().getOrDefault(Remux.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

@ -0,0 +1,24 @@
package ctbrec.ui.settings;
import ctbrec.recorder.postprocessing.PostProcessor;
import ctbrec.recorder.postprocessing.Rename;
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 RenamerPaneFactory extends AbstractPostProcessingPaneFactory {
@Override
public Preferences doCreatePostProcessorPane(PostProcessor pp) {
SimpleStringProperty fileTemplate = new SimpleStringProperty(null, Rename.FILE_NAME_TEMPLATE, pp.getConfig().getOrDefault(Rename.FILE_NAME_TEMPLATE, Rename.DEFAULT));
properties.add(fileTemplate);
return Preferences.of(new MapPreferencesStorage(),
Category.of(pp.getName(),
Setting.of("File name", fileTemplate)
)
);
}
}

View File

@ -0,0 +1,27 @@
package ctbrec.ui.settings;
import ctbrec.recorder.postprocessing.PostProcessor;
import ctbrec.recorder.postprocessing.Script;
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 ScriptPaneFactory extends AbstractPostProcessingPaneFactory {
@Override
public Preferences doCreatePostProcessorPane(PostProcessor pp) {
SimpleStringProperty script = new SimpleStringProperty(null, Script.SCRIPT_EXECUTABLE, pp.getConfig().getOrDefault(Script.SCRIPT_EXECUTABLE, "c:\\users\\johndoe\\somescript"));
SimpleStringProperty params = new SimpleStringProperty(null, Script.SCRIPT_PARAMS, pp.getConfig().getOrDefault(Script.SCRIPT_PARAMS, "${absolutePath}"));
properties.add(script);
properties.add(params);
return Preferences.of(new MapPreferencesStorage(),
Category.of(pp.getName(),
Setting.of("Script", script),
Setting.of("Parameters", params)
)
);
}
}

View File

@ -88,19 +88,18 @@ public class SettingsTab extends Tab implements TabSelectionListener {
private SimpleIntegerProperty onlineCheckIntervalInSecs; private SimpleIntegerProperty onlineCheckIntervalInSecs;
private SimpleBooleanProperty onlineCheckSkipsPausedModels; private SimpleBooleanProperty onlineCheckSkipsPausedModels;
private SimpleLongProperty leaveSpaceOnDevice; private SimpleLongProperty leaveSpaceOnDevice;
private SimpleIntegerProperty minimumLengthInSecs;
private SimpleStringProperty ffmpegParameters; private SimpleStringProperty ffmpegParameters;
private SimpleStringProperty fileExtension; private SimpleStringProperty fileExtension;
private SimpleStringProperty server; private SimpleStringProperty server;
private SimpleIntegerProperty port; private SimpleIntegerProperty port;
private SimpleStringProperty path; private SimpleStringProperty path;
private SimpleStringProperty downloadFilename;
private SimpleBooleanProperty requireAuthentication; private SimpleBooleanProperty requireAuthentication;
private SimpleBooleanProperty transportLayerSecurity; private SimpleBooleanProperty transportLayerSecurity;
private ExclusiveSelectionProperty recordLocal; private ExclusiveSelectionProperty recordLocal;
private SimpleFileProperty postProcessing;
private SimpleIntegerProperty postProcessingThreads; private SimpleIntegerProperty postProcessingThreads;
private SimpleBooleanProperty removeRecordingAfterPp;
private IgnoreList ignoreList; private IgnoreList ignoreList;
private PostProcessingStepPanel postProcessingStepPanel;
public SettingsTab(List<Site> sites, Recorder recorder) { public SettingsTab(List<Site> sites, Recorder recorder) {
this.sites = sites; this.sites = sites;
@ -137,23 +136,22 @@ public class SettingsTab extends Tab implements TabSelectionListener {
concurrentRecordings = new SimpleIntegerProperty(null, "concurrentRecordings", settings.concurrentRecordings); concurrentRecordings = new SimpleIntegerProperty(null, "concurrentRecordings", settings.concurrentRecordings);
onlineCheckIntervalInSecs = new SimpleIntegerProperty(null, "onlineCheckIntervalInSecs", settings.onlineCheckIntervalInSecs); onlineCheckIntervalInSecs = new SimpleIntegerProperty(null, "onlineCheckIntervalInSecs", settings.onlineCheckIntervalInSecs);
leaveSpaceOnDevice = new SimpleLongProperty(null, "minimumSpaceLeftInBytes", (long) new GigabytesConverter().convertTo(settings.minimumSpaceLeftInBytes)); leaveSpaceOnDevice = new SimpleLongProperty(null, "minimumSpaceLeftInBytes", (long) new GigabytesConverter().convertTo(settings.minimumSpaceLeftInBytes));
minimumLengthInSecs = new SimpleIntegerProperty(null, "minimumLengthInSeconds", settings.minimumLengthInSeconds);
ffmpegParameters = new SimpleStringProperty(null, "ffmpegMergedDownloadArgs", settings.ffmpegMergedDownloadArgs); ffmpegParameters = new SimpleStringProperty(null, "ffmpegMergedDownloadArgs", settings.ffmpegMergedDownloadArgs);
fileExtension = new SimpleStringProperty(null, "ffmpegFileSuffix", settings.ffmpegFileSuffix); fileExtension = new SimpleStringProperty(null, "ffmpegFileSuffix", settings.ffmpegFileSuffix);
server = new SimpleStringProperty(null, "httpServer", settings.httpServer); server = new SimpleStringProperty(null, "httpServer", settings.httpServer);
port = new SimpleIntegerProperty(null, "httpPort", settings.httpPort); port = new SimpleIntegerProperty(null, "httpPort", settings.httpPort);
path = new SimpleStringProperty(null, "servletContext", settings.servletContext); path = new SimpleStringProperty(null, "servletContext", settings.servletContext);
downloadFilename = new SimpleStringProperty(null, "downloadFilename", settings.downloadFilename);
requireAuthentication = new SimpleBooleanProperty(null, "requireAuthentication", settings.requireAuthentication); requireAuthentication = new SimpleBooleanProperty(null, "requireAuthentication", settings.requireAuthentication);
requireAuthentication.addListener(this::requireAuthenticationChanged); requireAuthentication.addListener(this::requireAuthenticationChanged);
transportLayerSecurity = new SimpleBooleanProperty(null, "transportLayerSecurity", settings.transportLayerSecurity); transportLayerSecurity = new SimpleBooleanProperty(null, "transportLayerSecurity", settings.transportLayerSecurity);
recordLocal = new ExclusiveSelectionProperty(null, "localRecording", settings.localRecording, "Local", "Remote"); recordLocal = new ExclusiveSelectionProperty(null, "localRecording", settings.localRecording, "Local", "Remote");
postProcessing = new SimpleFileProperty(null, "postProcessing", settings.postProcessing);
postProcessingThreads = new SimpleIntegerProperty(null, "postProcessingThreads", settings.postProcessingThreads); postProcessingThreads = new SimpleIntegerProperty(null, "postProcessingThreads", settings.postProcessingThreads);
removeRecordingAfterPp = new SimpleBooleanProperty(null, "removeRecordingAfterPostProcessing", settings.removeRecordingAfterPostProcessing);
onlineCheckSkipsPausedModels = new SimpleBooleanProperty(null, "onlineCheckSkipsPausedModels", settings.onlineCheckSkipsPausedModels); onlineCheckSkipsPausedModels = new SimpleBooleanProperty(null, "onlineCheckSkipsPausedModels", settings.onlineCheckSkipsPausedModels);
} }
private void createGui() { private void createGui() {
postProcessingStepPanel = new PostProcessingStepPanel(config);
ignoreList = new IgnoreList(sites); ignoreList = new IgnoreList(sites);
List<Category> siteCategories = new ArrayList<>(); List<Category> siteCategories = new ArrayList<>();
for (Site site : sites) { for (Site site : sites) {
@ -200,16 +198,15 @@ public class SettingsTab extends Tab implements TabSelectionListener {
Setting.of("Server", server), Setting.of("Server", server),
Setting.of("Port", port), Setting.of("Port", port),
Setting.of("Path", path, "Leave empty, if you didn't change the servletContext in the server config"), Setting.of("Path", path, "Leave empty, if you didn't change the servletContext in the server config"),
Setting.of("Download Filename", downloadFilename, "File name pattern for downloads"),
Setting.of("Require authentication", requireAuthentication), Setting.of("Require authentication", requireAuthentication),
Setting.of("Use Secure Communication (TLS)", transportLayerSecurity) Setting.of("Use Secure Communication (TLS)", transportLayerSecurity)
) )
), ),
Category.of("Post-Processing", Category.of("Post-Processing",
Group.of("Post-Processing", Group.of("Post-Processing",
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("Steps", postProcessingStepPanel)
Setting.of("Remove recording after post-processing", removeRecordingAfterPp)
) )
), ),
Category.of("Events & Actions", new ActionSettingsPanel(recorder)), Category.of("Events & Actions", new ActionSettingsPanel(recorder)),
@ -246,6 +243,9 @@ public class SettingsTab extends Tab implements TabSelectionListener {
prefs.getSetting("removeRecordingAfterPostProcessing").ifPresent(s -> bindEnabledProperty(s, recordLocal.not())); prefs.getSetting("removeRecordingAfterPostProcessing").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
prefs.getSetting("minimumLengthInSeconds").ifPresent(s -> bindEnabledProperty(s, recordLocal.not())); prefs.getSetting("minimumLengthInSeconds").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
prefs.getSetting("concurrentRecordings").ifPresent(s -> bindEnabledProperty(s, recordLocal.not())); prefs.getSetting("concurrentRecordings").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
prefs.getSetting("concurrentRecordings").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
prefs.getSetting("downloadFilename").ifPresent(s -> bindEnabledProperty(s, recordLocal));
postProcessingStepPanel.disableProperty().bind(recordLocal.not());
} }
private void bindEnabledProperty(Setting s, BooleanExpression bindTo) { private void bindEnabledProperty(Setting s, BooleanExpression bindTo) {

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

@ -0,0 +1,21 @@
package ctbrec.ui.tabs;
import java.io.IOException;
import ctbrec.Config;
import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
import ctbrec.recorder.postprocessing.AbstractPlaceholderAwarePostProcessor;
public class DownloadPostprocessor extends AbstractPlaceholderAwarePostProcessor {
@Override
public String getName() {
return "download renamer";
}
@Override
public void postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException {
// nothing really to do in here, we just inherit from AbstractPlaceholderAwarePostProcessor to use fillInPlaceHolders
}
}

View File

@ -228,7 +228,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
@Override @Override
public String get() { public String get() {
String modelNotes = Config.getInstance().getSettings().modelNotes.getOrDefault(m.getUrl(), ""); String modelNotes = Config.getInstance().getModelNotes(m);
return modelNotes; return modelNotes;
} }
}; };

View File

@ -559,9 +559,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
} }
private void onOpenDirectory(JavaFxRecording first) { private void onOpenDirectory(JavaFxRecording first) {
String recordingsDir = Config.getInstance().getSettings().recordingsDir; File tsFile = first.getAbsoluteFile();
String path = first.getPath();
File tsFile = new File(recordingsDir, path);
new Thread(() -> DesktopIntegration.open(tsFile.getParent())).start(); new Thread(() -> DesktopIntegration.open(tsFile.getParent())).start();
} }
@ -579,19 +577,19 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
} }
private void download(Recording recording) { private void download(Recording recording) {
LOG.debug("Path {}", recording.getPath()); LOG.debug("Path {}", recording.getAbsoluteFile());
String filename = proposeTargetFilename(recording); String filename = proposeTargetFilename(recording);
FileChooser chooser = new FileChooser(); FileChooser chooser = new FileChooser();
chooser.setInitialFileName(filename); chooser.setInitialFileName(filename);
if(config.getSettings().lastDownloadDir != null && !config.getSettings().lastDownloadDir.equals("")) { if (config.getSettings().lastDownloadDir != null && !config.getSettings().lastDownloadDir.equals("")) {
File dir = new File(config.getSettings().lastDownloadDir); File dir = new File(config.getSettings().lastDownloadDir);
while(!dir.exists()) { while (!dir.exists()) {
dir = dir.getParentFile(); dir = dir.getParentFile();
} }
chooser.setInitialDirectory(dir); chooser.setInitialDirectory(dir);
} }
File target = chooser.showSaveDialog(null); File target = chooser.showSaveDialog(null);
if(target != null) { if (target != null) {
config.getSettings().lastDownloadDir = target.getParent(); config.getSettings().lastDownloadDir = target.getParent();
startDownloadThread(target, recording); startDownloadThread(target, recording);
recording.setStatus(DOWNLOADING); recording.setStatus(DOWNLOADING);
@ -600,12 +598,12 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
} }
private String proposeTargetFilename(Recording recording) { private String proposeTargetFilename(Recording recording) {
String path = recording.getPath().substring(1);
if(recording.isSingleFile()) { if(recording.isSingleFile()) {
return new File(path).getName(); return recording.getAbsoluteFile().getName();
} else { } else {
String downloadFilename = config.getSettings().downloadFilename;
String fileSuffix = config.getSettings().ffmpegFileSuffix; String fileSuffix = config.getSettings().ffmpegFileSuffix;
String filename = path.replace("/", "-").replace(".mp4", "") + '.' + fileSuffix; String filename = new DownloadPostprocessor().fillInPlaceHolders(downloadFilename, recording, config) + '.' + fileSuffix;
return filename; return filename;
} }
} }
@ -615,11 +613,11 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
try { try {
String hlsBase = config.getServerUrl() + "/hls"; String hlsBase = config.getServerUrl() + "/hls";
if (recording.isSingleFile()) { if (recording.isSingleFile()) {
URL url = new URL(hlsBase + recording.getPath()); URL url = new URL(hlsBase + '/' + recording.getId());
FileDownload download = new FileDownload(CamrecApplication.httpClient, createDownloadListener(recording)); FileDownload download = new FileDownload(CamrecApplication.httpClient, createDownloadListener(recording));
download.start(url, target); download.start(url, target);
} else { } else {
URL url = new URL(hlsBase + recording.getPath() + "/playlist.m3u8"); URL url = new URL(hlsBase + '/' + recording.getId() + "/playlist.m3u8");
MergedFfmpegHlsDownload download = new MergedFfmpegHlsDownload(CamrecApplication.httpClient); MergedFfmpegHlsDownload download = new MergedFfmpegHlsDownload(CamrecApplication.httpClient);
download.init(config, recording.getModel(), Instant.now()); download.init(config, recording.getModel(), Instant.now());
LOG.info("Downloading {}", url); LOG.info("Downloading {}", url);
@ -641,7 +639,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
} }
}); });
t.setDaemon(true); t.setDaemon(true);
t.setName("Download Thread " + recording.getPath()); t.setName("Download Thread " + recording.getAbsoluteFile().toString());
t.start(); t.start();
} }
@ -650,7 +648,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
if (progress == 100) { if (progress == 100) {
recording.setStatus(FINISHED); recording.setStatus(FINISHED);
recording.setProgress(-1); recording.setProgress(-1);
LOG.debug("Download finished for recording {}", recording.getPath()); LOG.debug("Download finished for recording {} - {}", recording.getId(), recording.getAbsoluteFile());
} else { } else {
recording.setStatus(DOWNLOADING); recording.setStatus(DOWNLOADING);
recording.setProgress(progress); recording.setProgress(progress);

View File

@ -8,7 +8,7 @@
<parent> <parent>
<groupId>ctbrec</groupId> <groupId>ctbrec</groupId>
<artifactId>master</artifactId> <artifactId>master</artifactId>
<version>3.9.0</version> <version>3.10.0</version>
<relativePath>../master</relativePath> <relativePath>../master</relativePath>
</parent> </parent>
@ -50,6 +50,10 @@
<groupId>com.google.guava</groupId> <groupId>com.google.guava</groupId>
<artifactId>guava</artifactId> <artifactId>guava</artifactId>
</dependency> </dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.openjfx</groupId> <groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId> <artifactId>javafx-controls</artifactId>
@ -71,6 +75,11 @@
<artifactId>junit</artifactId> <artifactId>junit</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>javax.xml.bind</groupId> <groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId> <artifactId>jaxb-api</artifactId>

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);
@ -186,4 +193,8 @@ public class Config {
} }
return context; return context;
} }
public String getModelNotes(Model m) {
return Config.getInstance().getSettings().modelNotes.getOrDefault(m.getUrl(), "");
}
} }

View File

@ -3,7 +3,7 @@ package ctbrec;
public class NotImplementedExcetion extends RuntimeException { public class NotImplementedExcetion extends RuntimeException {
public NotImplementedExcetion() { public NotImplementedExcetion() {
super(); super("Not implemented");
} }
public NotImplementedExcetion(String mesg) { public NotImplementedExcetion(String mesg) {

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;
@ -28,6 +30,7 @@ import ctbrec.recorder.download.Download;
public class Recording implements Serializable { public class Recording implements Serializable {
private static final transient Logger LOG = LoggerFactory.getLogger(Recording.class); private static final transient Logger LOG = LoggerFactory.getLogger(Recording.class);
private String id;
private Model model; private Model model;
private transient Download download; private transient Download download;
private Instant startDate; private Instant startDate;
@ -39,6 +42,9 @@ 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 absoluteFile = null;
private File postProcessedFile = null;
public enum State { public enum State {
RECORDING("recording"), RECORDING("recording"),
@ -64,6 +70,14 @@ public class Recording implements Serializable {
} }
} }
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Instant getStartDate() { public Instant getStartDate() {
return startDate; return startDate;
} }
@ -93,18 +107,38 @@ public class Recording implements Serializable {
this.progress = progress; this.progress = progress;
} }
public String getPath() { // public String getPath() {
return path; // return path;
} // }
public void setPath(String path) { public void setPath(String path) {
this.path = path; this.path = path;
} }
public File getAbsoluteFile() { public File getAbsoluteFile() {
if (absoluteFile == null) {
String recordingsDir = Config.getInstance().getSettings().recordingsDir; String recordingsDir = Config.getInstance().getSettings().recordingsDir;
File recordingsFile = new File(recordingsDir, getPath()); File recordingsFile = new File(recordingsDir, path);
return recordingsFile; absoluteFile = recordingsFile;
return absoluteFile;
} else {
return absoluteFile;
}
}
public void setAbsoluteFile(File absoluteFile) {
this.absoluteFile = absoluteFile;
}
public File getPostProcessedFile() {
if (postProcessedFile == null) {
setPostProcessedFile(getAbsoluteFile());
}
return postProcessedFile;
}
public void setPostProcessedFile(File postProcessedFile) {
this.postProcessedFile = postProcessedFile;
} }
public long getSizeInByte() { public long getSizeInByte() {
@ -186,7 +220,7 @@ public class Recording implements Serializable {
int result = 1; int result = 1;
result = prime * result + ((getStartDate() == null) ? 0 : (int) (getStartDate().toEpochMilli() ^ (getStartDate().toEpochMilli() >>> 32))); result = prime * result + ((getStartDate() == null) ? 0 : (int) (getStartDate().toEpochMilli() ^ (getStartDate().toEpochMilli() >>> 32)));
result = prime * result + ((model == null) ? 0 : model.hashCode()); result = prime * result + ((model == null) ? 0 : model.hashCode());
result = prime * result + ((path == null) ? 0 : path.hashCode()); result = prime * result + ((getAbsoluteFile() == null) ? 0 : getAbsoluteFile().hashCode());
return result; return result;
} }
@ -207,11 +241,7 @@ public class Recording implements Serializable {
} else if (!getModel().equals(other.getModel())) { } else if (!getModel().equals(other.getModel())) {
return false; return false;
} }
if (getPath() == null) { if (!getAbsoluteFile().equals(other.getAbsoluteFile())) {
if (other.getPath() != null) {
return false;
}
} else if (!getPath().equals(other.getPath())) {
return false; return false;
} }
if (getStartDate() == null) { if (getStartDate() == null) {
@ -232,7 +262,7 @@ public class Recording implements Serializable {
} }
private long getSize() { private long getSize() {
File rec = new File(Config.getInstance().getSettings().recordingsDir, getPath()); File rec = getAbsoluteFile();
if (rec.isDirectory()) { if (rec.isDirectory()) {
return getDirectorySize(rec); return getDirectorySize(rec);
} else { } else {
@ -278,4 +308,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 {
@ -47,6 +48,7 @@ public class Settings {
public int concurrentRecordings = 0; public int concurrentRecordings = 0;
public boolean determineResolution = false; public boolean determineResolution = false;
public List<String> disabledSites = new ArrayList<>(); public List<String> disabledSites = new ArrayList<>();
public String downloadFilename = "${modelSanitizedName}-${localDateTime}";
public List<EventHandlerConfiguration> eventHandlers = new ArrayList<>(); public List<EventHandlerConfiguration> eventHandlers = new ArrayList<>();
public String fc2livePassword = ""; public String fc2livePassword = "";
public String fc2liveUsername = ""; public String fc2liveUsername = "";
@ -83,7 +85,6 @@ public class Settings {
public String mfcModelsTableSortType = ""; public String mfcModelsTableSortType = "";
public String mfcPassword = ""; public String mfcPassword = "";
public String mfcUsername = ""; public String mfcUsername = "";
public int minimumLengthInSeconds = 0;
public long minimumSpaceLeftInBytes = 0; public long minimumSpaceLeftInBytes = 0;
public Map<String, String> modelNotes = new HashMap<>(); public Map<String, String> modelNotes = new HashMap<>();
public List<Model> models = new ArrayList<>(); public List<Model> models = new ArrayList<>();
@ -92,8 +93,8 @@ public class Settings {
public boolean onlineCheckSkipsPausedModels = false; public boolean onlineCheckSkipsPausedModels = false;
public int overviewUpdateIntervalInSecs = 10; public int overviewUpdateIntervalInSecs = 10;
public String password = ""; // chaturbate password TODO maybe rename this onetime public String password = ""; // chaturbate password TODO maybe rename this onetime
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;
@ -110,7 +111,6 @@ public class Settings {
public String recordingsSortColumn = ""; public String recordingsSortColumn = "";
public String recordingsSortType = ""; public String recordingsSortType = "";
public boolean recordSingleFile = false; public boolean recordSingleFile = false;
public boolean removeRecordingAfterPostProcessing = false;
public boolean requireAuthentication = false; public boolean requireAuthentication = false;
public String servletContext = ""; public String servletContext = "";
public boolean showPlayerStarting = false; public boolean showPlayerStarting = false;

View File

@ -60,4 +60,14 @@ public class StringUtil {
} }
return hex; return hex;
} }
// @formatter:off
public static String sanitize(String input) {
return input
.replace(' ', '_')
.replace('\\', '_')
.replace('/', '_')
.replace('\'', '_')
.replace('"', '_');
} // @formatter:on
} }

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,53 @@
package ctbrec.io;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
public class IoUtils {
private static final Logger LOG = LoggerFactory.getLogger(IoUtils.class);
private IoUtils() {}
public static void deleteEmptyParents(File parent) throws IOException {
File recDir = new File(Config.getInstance().getSettings().recordingsDir);
while (parent != null && (parent.list() != null && parent.list().length == 0 || !parent.exists()) ) {
if (parent.equals(recDir)) {
return;
}
if(parent.exists()) {
LOG.debug("Deleting empty directory {}", parent.getAbsolutePath());
Files.delete(parent.toPath());
}
parent = parent.getParentFile();
}
}
public static void deleteDirectory(File directory) throws IOException {
if (!directory.exists()) {
return;
}
File[] files = directory.listFiles();
boolean deletedAllFiles = true;
for (File file : files) {
try {
LOG.trace("Deleting {}", file.getAbsolutePath());
Files.delete(file.toPath());
} catch (Exception e) {
deletedAllFiles = false;
LOG.debug("Couldn't delete {}", file, e);
}
}
if (!deletedAllFiles) {
throw new IOException("Couldn't delete all files in " + directory);
}
}
}

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

@ -9,7 +9,6 @@ import java.nio.file.FileStore;
import java.nio.file.Files; import java.nio.file.Files;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.ArrayList; import java.util.ArrayList;
@ -53,6 +52,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,13 +161,14 @@ 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) {
LOG.debug("Running post-processor: {}", postProcessor.getName());
postProcessor.postprocess(recording, recordingManager, config);
}
setRecordingStatus(recording, State.FINISHED); setRecordingStatus(recording, State.FINISHED);
recordingManager.saveRecording(recording); recordingManager.saveRecording(recording);
deleteIfTooShort(recording);
LOG.info("Post-processing finished for {}", recording.getModel().getName()); LOG.info("Post-processing finished for {}", recording.getModel().getName());
if (config.getSettings().removeRecordingAfterPostProcessing) {
recordingManager.remove(recording);
}
} catch (Exception e) { } catch (Exception e) {
if (e instanceof InterruptedException) { // NOSONAR if (e instanceof InterruptedException) { // NOSONAR
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
@ -276,8 +277,11 @@ public class NextGenLocalRecorder implements Recorder {
private Recording createRecording(Download download) throws IOException { private Recording createRecording(Download download) throws IOException {
Model model = download.getModel(); Model model = download.getModel();
Recording rec = new Recording(); Recording rec = new Recording();
rec.setId(UUID.randomUUID().toString());
rec.setDownload(download); rec.setDownload(download);
rec.setPath(download.getPath(model).replaceAll("\\\\", "/")); String recordingFile = download.getPath(model).replaceAll("\\\\", "/");
File absoluteFile = new File(config.getSettings().recordingsDir, recordingFile);
rec.setAbsoluteFile(absoluteFile);
rec.setModel(model); rec.setModel(model);
rec.setStartDate(download.getStartTime()); rec.setStartDate(download.getStartTime());
rec.setSingleFile(download.isSingleFile()); rec.setSingleFile(download.isSingleFile());
@ -298,22 +302,6 @@ public class NextGenLocalRecorder implements Recorder {
} }
} }
private boolean deleteIfTooShort(Recording rec) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
Duration minimumLengthInSeconds = Duration.ofSeconds(Config.getInstance().getSettings().minimumLengthInSeconds);
if (minimumLengthInSeconds.getSeconds() <= 0) {
return false;
}
Duration recordingLength = rec.getLength();
if (recordingLength.compareTo(minimumLengthInSeconds) < 0) {
LOG.info("Deleting too short recording {} [{} < {}]", rec, recordingLength, minimumLengthInSeconds);
delete(rec);
return true;
}
return false;
}
@Override @Override
public void stopRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException { public void stopRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
recorderLock.lock(); recorderLock.lock();
@ -636,12 +624,14 @@ 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)) {
Download download = other.getModel().createDownload(); Download download = other.getModel().createDownload();
download.init(Config.getInstance(), other.getModel(), other.getStartDate()); download.init(Config.getInstance(), other.getModel(), other.getStartDate());
other.setDownload(download); other.setDownload(download);
other.setPostProcessedFile(null);
submitPostProcessingJob(other); submitPostProcessingJob(other);
return; return;
} }

View File

@ -139,7 +139,7 @@ public class OnlineMonitor extends Thread {
} }
private void suspendUntilNextIteration(List<Model> models, Duration timeCheckTook) { private void suspendUntilNextIteration(List<Model> models, Duration timeCheckTook) {
LOG.trace("Online check for {} models took {} seconds", models.size(), timeCheckTook.getSeconds()); LOG.debug("Online check for {} models took {} seconds", models.size(), timeCheckTook.getSeconds());
long sleepTime = config.getSettings().onlineCheckIntervalInSecs; long sleepTime = config.getSettings().onlineCheckIntervalInSecs;
if(timeCheckTook.getSeconds() < sleepTime) { if(timeCheckTook.getSeconds() < sleepTime) {
try { try {

View File

@ -1,236 +0,0 @@
package ctbrec.recorder;
import static java.nio.file.StandardWatchEventKinds.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.ClosedWatchServiceException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Recording;
public class RecordingFileMonitor {
private static final transient Logger LOG = LoggerFactory.getLogger(RecordingFileMonitor.class);
private WatchService watcher;
private Map<WatchKey, Path> keys;
private boolean running = true;
private RecordingManager manager;
public RecordingFileMonitor(RecordingManager manager) throws IOException {
this.manager = manager;
this.watcher = FileSystems.getDefault().newWatchService();
this.keys = new HashMap<>();
registerAll(new File(Config.getInstance().getSettings().recordingsDir).toPath());
}
void processEvents() {
while (running) {
// wait for key to be signalled
WatchKey key;
try {
key = watcher.take();
} catch (InterruptedException | ClosedWatchServiceException x) {
return;
}
Path dir = keys.get(key);
if (dir == null) {
LOG.error("WatchKey not recognized!!");
continue;
}
List<WatchEvent<?>> events = key.pollEvents();
LOG.debug("Size: {}", events.size());
if (isRenameProcess(events)) {
handleRename(dir, events);
} else {
for (WatchEvent<?> event : events) {
WatchEvent.Kind<?> kind = event.kind();
// TBD - provide example of how OVERFLOW event is handled
if (kind == OVERFLOW) {
continue;
}
// Context for directory entry event is the file name of entry
WatchEvent<Path> ev = cast(event);
Path name = ev.context();
Path child = dir.resolve(name);
if(Files.isRegularFile(child)) {
if (kind == ENTRY_CREATE) {
handleFileCreation(child);
} else if (kind == ENTRY_DELETE) {
handleFileDeletion(child);
}
} else {
if (kind == ENTRY_CREATE) {
handleDirCreation(child);
} else if (kind == ENTRY_DELETE) {
handleDirDeletion(child);
}
}
}
}
// reset key and remove from set if directory no longer accessible
boolean valid = key.reset();
if (!valid) {
keys.remove(key);
// all directories are inaccessible
if (keys.isEmpty()) {
break;
}
}
}
}
private void handleRename(Path dir, List<WatchEvent<?>> events) {
WatchEvent<Path> deleteEvent = cast(events.get(0));
WatchEvent<Path> createEvent = cast(events.get(1));
Path from = dir.resolve(deleteEvent.context());
Path to = dir.resolve(createEvent.context());
LOG.debug("{} -> {}", from, to);
List<Recording> affectedRecordings = getAffectedRecordings(from);
adjustPaths(affectedRecordings, from, to);
if (Files.isDirectory(to, LinkOption.NOFOLLOW_LINKS)) {
unregister(from);
try {
registerAll(to);
} catch (IOException e) {
e.printStackTrace();
}
}
}
private List<Recording> getAffectedRecordings(Path from) {
String f = from.toAbsolutePath().toString();
List<Recording> affected = new ArrayList<>();
for (Recording rec : manager.getAll()) {
String r = rec.getAbsoluteFile().getAbsolutePath();
if (r.startsWith(f)) {
affected.add(rec);
}
}
return affected;
}
private void adjustPaths(List<Recording> affectedRecordings, Path from, Path to) {
for (Recording rec : affectedRecordings) {
String oldPath = rec.getAbsoluteFile().getAbsolutePath();
String newPath = oldPath.replace(from.toString(), to.toString());
String recordingsDir = Config.getInstance().getSettings().recordingsDir;
String relativePath = newPath.replaceFirst(Pattern.quote(recordingsDir), "");
LOG.debug("Recording path has changed {} -> {}", rec.getPath(), relativePath);
rec.setPath(relativePath);
try {
manager.saveRecording(rec);
} catch (IOException e) {
LOG.error("Couldn't update recording path in meta data file", e);
}
}
}
private void handleFileCreation(Path child) {
LOG.trace("File created {}", child);
}
private void handleFileDeletion(Path child) {
LOG.trace("File deleted {}", child);
}
private void handleDirCreation(Path dir) {
try {
registerAll(dir);
LOG.trace("Directory added {}", dir);
} catch (IOException x) {
// ignore to keep sample readbale
}
}
private void handleDirDeletion(Path dir) {
// TODO unregister key ?!?
// only delete directories, which have actually been deleted
if(Files.notExists(dir, LinkOption.NOFOLLOW_LINKS)) {
LOG.trace("Directory Deleted {}", dir);
}
}
private boolean isRenameProcess(List<WatchEvent<?>> events) {
if(events.size() == 2) {
boolean deleteFirst = events.get(0).kind() == ENTRY_DELETE;
boolean createSecond = events.get(1).kind() == ENTRY_CREATE;
return deleteFirst && createSecond;
} else {
return false;
}
}
/**
* Register the given directory, and all its sub-directories, with the
* WatchService.
*/
private void registerAll(final Path start) throws IOException {
// register directory and sub-directories
Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
register(dir);
return FileVisitResult.CONTINUE;
}
});
}
/**
* Register the given directory with the WatchService
*/
void register(Path dir) {
try {
WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE);
keys.put(key, dir);
LOG.debug("Monitor {}", dir);
} catch(IOException e) {
LOG.warn("Couldn't register directory monitor for directory {}", dir, e);
}
}
public void unregister(Path path) {
}
@SuppressWarnings("unchecked")
static <T> WatchEvent<T> cast(WatchEvent<?> event) {
return (WatchEvent<T>) event;
}
public void addDirectory(Path dir) throws IOException {
LOG.info("Adding monitor for {}", dir);
registerAll(dir);
}
public void stop() throws IOException {
running = false;
watcher.close();
}
}

View File

@ -1,6 +1,7 @@
package ctbrec.recorder; package ctbrec.recorder;
import static ctbrec.Recording.State.*; import static ctbrec.Recording.State.*;
import static ctbrec.io.IoUtils.*;
import static java.nio.charset.StandardCharsets.*; import static java.nio.charset.StandardCharsets.*;
import static java.nio.file.StandardOpenOption.*; import static java.nio.file.StandardOpenOption.*;
@ -13,6 +14,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.Objects; import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -25,6 +27,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 +46,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(" ");
@ -50,6 +54,10 @@ public class RecordingManager {
} }
public void add(Recording rec) throws IOException { public void add(Recording rec) throws IOException {
File recordingsMetaDir = getDir();
String filename = rec.toString() + ".json";
File recordingMetaData = new File(recordingsMetaDir, filename);
rec.setMetaDataFile(recordingMetaData.getCanonicalPath());
saveRecording(rec); saveRecording(rec);
recordingsLock.lock(); recordingsLock.lock();
try { try {
@ -60,14 +68,14 @@ public class RecordingManager {
} }
public void saveRecording(Recording rec) throws IOException { public void saveRecording(Recording rec) throws IOException {
if (rec.getMetaDataFile() != null) {
File recordingMetaData = new File(rec.getMetaDataFile());
String json = adapter.toJson(rec); String json = adapter.toJson(rec);
File recordingsMetaDir = getDir();
String filename = rec.toString() + ".json";
File recordingMetaData = new File(recordingsMetaDir, filename);
rec.setMetaDataFile(recordingMetaData.getAbsolutePath()); rec.setMetaDataFile(recordingMetaData.getAbsolutePath());
Files.createDirectories(recordingsMetaDir.toPath()); Files.createDirectories(recordingMetaData.getParentFile().toPath());
Files.write(recordingMetaData.toPath(), json.getBytes(UTF_8), CREATE, WRITE, TRUNCATE_EXISTING); Files.write(recordingMetaData.toPath(), json.getBytes(UTF_8), CREATE, WRITE, TRUNCATE_EXISTING);
} }
}
private void loadRecordings() throws IOException { private void loadRecordings() throws IOException {
File recordingsMetaDir = getDir(); File recordingsMetaDir = getDir();
@ -80,6 +88,10 @@ public class RecordingManager {
if (recording.getStatus() == RECORDING || recording.getStatus() == GENERATING_PLAYLIST || recording.getStatus() == POST_PROCESSING) { if (recording.getStatus() == RECORDING || recording.getStatus() == GENERATING_PLAYLIST || recording.getStatus() == POST_PROCESSING) {
recording.setStatus(WAITING); recording.setStatus(WAITING);
} }
if (recording.getId() == null) {
recording.setId(UUID.randomUUID().toString());
saveRecording(recording);
}
if (recordingExists(recording)) { if (recordingExists(recording)) {
recordings.add(recording); recordings.add(recording);
} else { } else {
@ -94,8 +106,7 @@ public class RecordingManager {
} }
private boolean recordingExists(Recording recording) { private boolean recordingExists(Recording recording) {
File rec = new File(config.getSettings().recordingsDir, recording.getPath()); return recording.getAbsoluteFile().exists();
return rec.exists();
} }
private File getDir() { private File getDir() {
@ -120,22 +131,39 @@ public class RecordingManager {
recording = recordings.get(idx); recording = recordings.get(idx);
recording.setStatus(State.DELETING); recording.setStatus(State.DELETING);
File recordingsDir = new File(config.getSettings().recordingsDir); File path = recording.getAbsoluteFile();
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());
deleteEmptyParents(f.getParentFile());
} else {
deleteDirectory(f);
deleteEmptyParents(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);
@ -154,8 +182,7 @@ public class RecordingManager {
try { try {
int idx = recordings.indexOf(recording); int idx = recordings.indexOf(recording);
recording = recordings.get(idx); recording = recordings.get(idx);
File recordingsDir = new File(config.getSettings().recordingsDir); File path = recording.getAbsoluteFile();
File path = new File(recordingsDir, recording.getPath());
deleteEmptyParents(path.getParentFile()); deleteEmptyParents(path.getParentFile());
// delete the meta data // delete the meta data
Files.deleteIfExists(new File(recording.getMetaDataFile()).toPath()); Files.deleteIfExists(new File(recording.getMetaDataFile()).toPath());
@ -182,40 +209,6 @@ public class RecordingManager {
} }
} }
public static void deleteEmptyParents(File parent) throws IOException {
File recDir = new File(Config.getInstance().getSettings().recordingsDir);
while (parent != null && parent.list() != null && parent.list().length == 0) {
if (parent.equals(recDir)) {
return;
}
LOG.debug("Deleting empty directory {}", parent.getAbsolutePath());
Files.delete(parent.toPath());
parent = parent.getParentFile();
}
}
private void deleteDirectory(File directory) throws IOException {
if (!directory.exists()) {
return;
}
File[] files = directory.listFiles();
boolean deletedAllFiles = true;
for (File file : files) {
try {
LOG.trace("Deleting {}", file.getAbsolutePath());
Files.delete(file.toPath());
} catch (Exception e) {
deletedAllFiles = false;
LOG.debug("Couldn't delete {}", file, e);
}
}
if (!deletedAllFiles) {
throw new IOException("Couldn't delete all files in " + directory);
}
}
public void pin(Recording recording) throws IOException { public void pin(Recording recording) throws IOException {
recordingsLock.lock(); recordingsLock.lock();
try { try {

View File

@ -27,6 +27,7 @@ import ctbrec.event.EventBusHolder;
import ctbrec.event.NoSpaceLeftEvent; import ctbrec.event.NoSpaceLeftEvent;
import ctbrec.event.RecordingStateChangedEvent; import ctbrec.event.RecordingStateChangedEvent;
import ctbrec.io.BandwidthMeter; import ctbrec.io.BandwidthMeter;
import ctbrec.io.FileJsonAdapter;
import ctbrec.io.HttpClient; import ctbrec.io.HttpClient;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.io.InstantJsonAdapter; import ctbrec.io.InstantJsonAdapter;
@ -45,7 +46,11 @@ public class RemoteRecorder implements Recorder {
private static final Logger LOG = LoggerFactory.getLogger(RemoteRecorder.class); private static final Logger LOG = LoggerFactory.getLogger(RemoteRecorder.class);
public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
private Moshi moshi = new Moshi.Builder().add(Instant.class, new InstantJsonAdapter()).add(Model.class, new ModelJsonAdapter()).build(); private Moshi moshi = new Moshi.Builder()
.add(Instant.class, new InstantJsonAdapter())
.add(Model.class, new ModelJsonAdapter())
.add(File.class, new FileJsonAdapter())
.build();
private JsonAdapter<ModelListResponse> modelListResponseAdapter = moshi.adapter(ModelListResponse.class); private JsonAdapter<ModelListResponse> modelListResponseAdapter = moshi.adapter(ModelListResponse.class);
private JsonAdapter<RecordingListResponse> recordingListResponseAdapter = moshi.adapter(RecordingListResponse.class); private JsonAdapter<RecordingListResponse> recordingListResponseAdapter = moshi.adapter(RecordingListResponse.class);
private JsonAdapter<ModelRequest> modelRequestAdapter = moshi.adapter(ModelRequest.class); private JsonAdapter<ModelRequest> modelRequestAdapter = moshi.adapter(ModelRequest.class);
@ -325,7 +330,7 @@ public class RemoteRecorder implements Recorder {
int idx = newRecordings.indexOf(recording); int idx = newRecordings.indexOf(recording);
Recording newRecording = newRecordings.get(idx); Recording newRecording = newRecordings.get(idx);
if (newRecording.getStatus() != recording.getStatus()) { if (newRecording.getStatus() != recording.getStatus()) {
File file = new File(recording.getPath()); File file = recording.getAbsoluteFile();
RecordingStateChangedEvent evt = new RecordingStateChangedEvent(file, newRecording.getStatus(), recording.getModel(), RecordingStateChangedEvent evt = new RecordingStateChangedEvent(file, newRecording.getStatus(), recording.getModel(),
recording.getStartDate()); recording.getStartDate());
EventBusHolder.BUS.post(evt); EventBusHolder.BUS.post(evt);
@ -337,7 +342,7 @@ public class RemoteRecorder implements Recorder {
justStarted.removeAll(recordings); justStarted.removeAll(recordings);
for (Recording recording : justStarted) { for (Recording recording : justStarted) {
if (recording.getStatus() == Recording.State.RECORDING) { if (recording.getStatus() == Recording.State.RECORDING) {
File file = new File(recording.getPath()); File file = recording.getAbsoluteFile();
RecordingStateChangedEvent evt = new RecordingStateChangedEvent(file, recording.getStatus(), recording.getModel(), RecordingStateChangedEvent evt = new RecordingStateChangedEvent(file, recording.getStatus(), recording.getModel(),
recording.getStartDate()); recording.getStartDate());
EventBusHolder.BUS.post(evt); EventBusHolder.BUS.post(evt);
@ -345,6 +350,19 @@ public class RemoteRecorder implements Recorder {
} }
recordings = newRecordings; recordings = newRecordings;
// assign a site to the model
for (Site site : sites) {
for (Recording recording : recordings) {
Model m = recording.getModel();
if (m.getSite() == null) {
if (site.isSiteForModel(m)) {
m.setSite(site);
continue;
}
}
}
}
} else { } else {
LOG.error(SERVER_RETURNED_ERROR, resp.status, resp.msg); LOG.error(SERVER_RETURNED_ERROR, resp.status, resp.msg);
} }

View File

@ -1,59 +1,11 @@
package ctbrec.recorder.download; package ctbrec.recorder.download;
import java.io.File;
import java.io.IOException;
import java.time.Instant; import java.time.Instant;
import java.util.Arrays;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.OS;
import ctbrec.Recording;
import ctbrec.io.StreamRedirectThread;
public abstract class AbstractDownload implements Download { public abstract class AbstractDownload implements Download {
private static final Logger LOG = LoggerFactory.getLogger(AbstractDownload.class);
protected Instant startTime; protected Instant startTime;
protected void runPostProcessingScript(Recording recording) throws IOException, InterruptedException {
String postProcessing = Config.getInstance().getSettings().postProcessing;
if (postProcessing != null && !postProcessing.isEmpty()) {
File target = recording.getAbsoluteFile();
Runtime rt = Runtime.getRuntime();
String[] args = new String[] {
postProcessing,
target.getParentFile().getAbsolutePath(),
target.getAbsolutePath(),
getModel().getName(),
getModel().getSite().getName(),
Long.toString(recording.getStartDate().getEpochSecond())
};
if(LOG.isDebugEnabled()) {
LOG.debug("Running {}", Arrays.toString(args));
}
Process process = rt.exec(args, OS.getEnvironment());
// TODO maybe write these to a separate log file, e.g. recname.ts.pp.log
Thread std = new Thread(new StreamRedirectThread(process.getInputStream(), System.out));
std.setName("Process stdout pipe");
std.setDaemon(true);
std.start();
Thread err = new Thread(new StreamRedirectThread(process.getErrorStream(), System.err));
err.setName("Process stderr pipe");
err.setDaemon(true);
err.start();
int exitCode = process.waitFor();
LOG.debug("Process finished with exit code {}", exitCode);
if (exitCode != 0) {
throw new ProcessExitedUncleanException("Post-Processing finished with exit code " + exitCode);
}
}
}
@Override @Override
public Instant getStartTime() { public Instant getStartTime() {
return startTime; return startTime;

View File

@ -391,13 +391,13 @@ public class DashDownload extends AbstractDownload {
try { try {
Thread.currentThread().setName("PP " + model.getName()); Thread.currentThread().setName("PP " + model.getName());
recording.setStatus(POST_PROCESSING); recording.setStatus(POST_PROCESSING);
String path = recording.getPath(); // FIXME this was recording.getPath() before and is currently not working. This has to be fixed once DASH is used for a download again
String path = recording.getAbsoluteFile().getAbsolutePath();
File dir = new File(Config.getInstance().getSettings().recordingsDir, path); File dir = new File(Config.getInstance().getSettings().recordingsDir, path);
File file = new File(dir.getParentFile(), dir.getName().substring(0, dir.getName().length() - 5)); File file = new File(dir.getParentFile(), dir.getName().substring(0, dir.getName().length() - 5));
new FfmpegMuxer(dir, file); new FfmpegMuxer(dir, file);
targetFile = file; targetFile = file;
recording.setPath(path.substring(0, path.length() - 5)); recording.setPath(path.substring(0, path.length() - 5));
runPostProcessingScript(recording);
} catch (Exception e) { } catch (Exception e) {
throw new PostProcessingException(e); throw new PostProcessingException(e);
} }

View File

@ -125,12 +125,6 @@ public class FFmpegDownload extends AbstractHlsDownload {
@Override @Override
public void postprocess(Recording recording) { public void postprocess(Recording recording) {
Thread.currentThread().setName("PP " + model.getName());
try {
runPostProcessingScript(recording);
} catch (Exception e) {
throw new PostProcessingException(e);
}
} }
@Override @Override

View File

@ -215,7 +215,6 @@ public class HlsDownload extends AbstractHlsDownload {
try { try {
generatePlaylist(recording); generatePlaylist(recording);
recording.setStatusWithEvent(State.POST_PROCESSING); recording.setStatusWithEvent(State.POST_PROCESSING);
runPostProcessingScript(recording);
} catch (Exception e) { } catch (Exception e) {
throw new PostProcessingException(e); throw new PostProcessingException(e);
} }

View File

@ -492,12 +492,6 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
@Override @Override
public void postprocess(Recording recording) { public void postprocess(Recording recording) {
Thread.currentThread().setName("PP " + model.getName());
try {
runPostProcessingScript(recording);
} catch (Exception e) {
throw new PostProcessingException(e);
}
} }
public void downloadFinishedRecording(String segmentPlaylistUri, File target, ProgressListener progressListener, long sizeInBytes) throws Exception { public void downloadFinishedRecording(String segmentPlaylistUri, File target, ProgressListener progressListener, long sizeInBytes) throws Exception {

View File

@ -0,0 +1,104 @@
package ctbrec.recorder.postprocessing;
import static ctbrec.StringUtil.*;
import static java.util.Optional.*;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import ctbrec.Config;
import ctbrec.Recording;
import ctbrec.sites.Site;
public abstract class AbstractPlaceholderAwarePostProcessor extends AbstractPostProcessor {
public static final String[] PLACE_HOLDERS = {
"${modelName}",
"${modelDisplayName}",
"${modelSanitizedName}",
"${siteName}",
"${siteSanitizedName}",
"${utcDateTime}",
"${localDateTime}",
"${epochSecond}",
"${fileSuffix}",
"${modelNotes}",
"${recordingNotes}",
"${recordingsDir}",
"${absolutePath}",
"${absoluteParentPath}"
};
public String fillInPlaceHolders(String input, Recording rec, Config config) {
// @formatter:off
String output = input
.replace("${modelName}", ofNullable(rec.getModel().getName()).orElse("modelName"))
.replace("${modelDisplayName}", ofNullable(rec.getModel().getDisplayName()).orElse("displayName"))
.replace("${modelSanitizedName}", ofNullable(rec.getModel().getSanitizedNamed()).orElse("sanitizedName"))
.replace("${siteName}", ofNullable(rec.getModel().getSite()).map(Site::getName).orElse("site"))
.replace("${siteSanitizedName}", getSanitizedSiteName(rec))
.replace("${fileSuffix}", getFileSuffix(rec))
.replace("${epochSecond}", Long.toString(rec.getStartDate().getEpochSecond()))
.replace("${modelNotes}", sanitize(config.getModelNotes(rec.getModel())))
.replace("${recordingNotes}", getSanitizedRecordingNotes(rec))
.replace("${recordingsDir}", config.getSettings().recordingsDir)
.replace("${absolutePath}", rec.getPostProcessedFile().getAbsolutePath())
.replace("${absoluteParentPath}", rec.getPostProcessedFile().getParentFile().getAbsolutePath())
;
output = replaceUtcDateTime(rec, output);
output = replaceLocalDateTime(rec, output);
return output;
// @formatter:on
}
private String replaceUtcDateTime(Recording rec, String filename) {
return replaceDateTime(rec, filename, "utcDateTime", ZoneOffset.UTC);
}
private String replaceLocalDateTime(Recording rec, String filename) {
return replaceDateTime(rec, filename, "localDateTime", ZoneId.systemDefault());
}
private String replaceDateTime(Recording rec, String filename, String placeHolder, ZoneId zone) {
String pattern = "yyyy-MM-dd_HH-mm-ss";
Matcher m = Pattern.compile("\\$\\{" + placeHolder + "(?:\\((.*?)\\))?\\}").matcher(filename);
if (m.find()) {
String p = m.group(1);
if (p != null) {
pattern = p;
}
}
String formattedDate = getDateTime(rec, pattern, zone);
return m.replaceAll(formattedDate);
}
private String getDateTime(Recording rec, String pattern, ZoneId zone) {
return DateTimeFormatter.ofPattern(pattern)
.withLocale(Locale.getDefault())
.withZone(zone)
.format(rec.getStartDate());
}
private CharSequence getFileSuffix(Recording rec) {
if(rec.isSingleFile()) {
String filename = rec.getPostProcessedFile().getName();
return filename.substring(filename.lastIndexOf('.') + 1);
} else {
return "";
}
}
private CharSequence getSanitizedSiteName(Recording rec) {
return sanitize(ofNullable(rec.getModel().getSite()).map(Site::getName).orElse(""));
}
private CharSequence getSanitizedRecordingNotes(Recording rec) {
return sanitize(ofNullable(rec.getNote()).orElse(""));
}
}

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,49 @@
package ctbrec.recorder.postprocessing;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
public class Copy extends AbstractPostProcessor {
private static final transient Logger LOG = LoggerFactory.getLogger(Copy.class);
@Override
public String getName() {
return "create a copy";
}
@Override
public void postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException {
File orig = rec.getPostProcessedFile();
String copyFilename = getFilenameForCopy(orig);
File copy = new File(orig.getParentFile(), copyFilename);
LOG.info("Creating a copy {}", copy);
if (orig.isFile()) {
Files.copy(rec.getPostProcessedFile().toPath(), copy.toPath());
} else {
FileUtils.copyDirectory(orig, copy, true);
}
rec.setPostProcessedFile(copy);
rec.getAssociatedFiles().add(copy.getCanonicalPath());
}
private String getFilenameForCopy(File orig) {
String filename = orig.getName();
if (orig.isFile()) {
String name = filename.substring(0, filename.lastIndexOf('.'));
String ext = filename.substring(filename.lastIndexOf('.') + 1);
return name + "_copy." + ext;
} else {
return filename + "_copy";
}
}
}

View File

@ -0,0 +1,21 @@
package ctbrec.recorder.postprocessing;
import java.io.IOException;
import ctbrec.Config;
import ctbrec.NotImplementedExcetion;
import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
public class CreateContactSheet extends AbstractPlaceholderAwarePostProcessor {
@Override
public String getName() {
return "create contact sheet";
}
@Override
public void postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException {
throw new NotImplementedExcetion();
}
}

View File

@ -0,0 +1,21 @@
package ctbrec.recorder.postprocessing;
import java.io.IOException;
import ctbrec.Config;
import ctbrec.NotImplementedExcetion;
import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
public class CreateTimelineThumbs extends AbstractPlaceholderAwarePostProcessor {
@Override
public String getName() {
return "create timeline thumbnails";
}
@Override
public void postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException {
throw new NotImplementedExcetion();
}
}

View File

@ -0,0 +1,31 @@
package ctbrec.recorder.postprocessing;
import static ctbrec.io.IoUtils.*;
import java.io.IOException;
import java.nio.file.Files;
import ctbrec.Config;
import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
public class DeleteOriginal extends AbstractPostProcessor {
@Override
public String getName() {
return "delete original";
}
@Override
public void postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException {
if (rec.getAbsoluteFile().isFile()) {
Files.deleteIfExists(rec.getAbsoluteFile().toPath());
deleteEmptyParents(rec.getAbsoluteFile().getParentFile());
} else {
deleteDirectory(rec.getAbsoluteFile());
deleteEmptyParents(rec.getAbsoluteFile());
}
rec.setAbsoluteFile(rec.getPostProcessedFile());
rec.getAssociatedFiles().remove(rec.getAbsoluteFile().getCanonicalPath());
}
}

View File

@ -0,0 +1,34 @@
package ctbrec.recorder.postprocessing;
import java.io.IOException;
import java.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
public class DeleteTooShort extends AbstractPostProcessor {
private static final transient Logger LOG = LoggerFactory.getLogger(DeleteTooShort.class);
public static final String MIN_LEN_IN_SECS = "minimumLengthInSeconds";
@Override
public String getName() {
return "delete too short";
}
@Override
public void postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException {
Duration minimumLengthInSeconds = Duration.ofSeconds(Integer.parseInt(getConfig().getOrDefault(MIN_LEN_IN_SECS, "0")));
if (minimumLengthInSeconds.getSeconds() > 0) {
Duration recordingLength = rec.getLength();
if (recordingLength.compareTo(minimumLengthInSeconds) < 0) {
LOG.info("Deleting too short recording {} [{} < {}]", rec, recordingLength, minimumLengthInSeconds);
recordingManager.delete(rec);
}
}
}
}

View File

@ -0,0 +1,61 @@
package ctbrec.recorder.postprocessing;
import static ctbrec.io.IoUtils.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
public class Move extends AbstractPlaceholderAwarePostProcessor {
private static final Logger LOG = LoggerFactory.getLogger(Rename.class);
public static final String PATH_TEMPLATE = "path.template";
public static final String DEFAULT = "${modelSanitizedName}" + File.separatorChar + "${localDateTime}";
@Override
public String getName() {
return "move";
}
@Override
public void postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException {
String pathTemplate = getConfig().getOrDefault(PATH_TEMPLATE, DEFAULT);
String path = fillInPlaceHolders(pathTemplate, rec, config);
File src = rec.getPostProcessedFile();
boolean isFile = src.isFile();
File target = new File(path, src.getName());
if (Objects.equals(src, target)) {
return;
}
LOG.info("Moving {} to {}", src.getName(), target.getParentFile().getCanonicalPath());
Files.createDirectories(target.getParentFile().toPath());
Files.move(rec.getPostProcessedFile().toPath(), target.toPath());
rec.setPostProcessedFile(target);
if (Objects.equals(src, rec.getAbsoluteFile())) {
rec.setAbsoluteFile(target);
}
rec.getAssociatedFiles().remove(src.getCanonicalPath());
rec.getAssociatedFiles().add(target.getCanonicalPath());
if (isFile) {
deleteEmptyParents(src.getParentFile());
} else {
deleteEmptyParents(src);
}
}
@Override
public String toString() {
String s = getName();
if (getConfig().containsKey(PATH_TEMPLATE)) {
s += " [" + getConfig().get(PATH_TEMPLATE) + ']';
}
return s;
}
}

View File

@ -0,0 +1,18 @@
package ctbrec.recorder.postprocessing;
import java.io.IOException;
import java.io.Serializable;
import java.util.Map;
import ctbrec.Config;
import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
public interface PostProcessor extends Serializable {
String getName();
void postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException;
Map<String, String> getConfig();
void setConfig(Map<String, String> conf);
}

View File

@ -0,0 +1,21 @@
package ctbrec.recorder.postprocessing;
import java.io.IOException;
import ctbrec.Config;
import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
public class RemoveKeepFile extends AbstractPostProcessor {
@Override
public String getName() {
return "remove recording, but keep the files";
}
@Override
public void postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException {
recordingManager.remove(rec);
rec.setMetaDataFile(null);
}
}

View File

@ -0,0 +1,107 @@
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 java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.OS;
import ctbrec.Recording;
import ctbrec.io.IoUtils;
import ctbrec.io.StreamRedirectThread;
import ctbrec.recorder.RecordingManager;
import ctbrec.recorder.download.ProcessExitedUncleanException;
public class Remux extends AbstractPostProcessor {
private static final Logger LOG = LoggerFactory.getLogger(Remux.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, RecordingManager recordingManager, Config config) throws IOException, InterruptedException {
String fileExt = getConfig().get(FILE_EXT);
String[] args = getConfig().get(FFMPEG_ARGS).split(" ");
String[] argsPlusFile = new String[args.length + 3];
File inputFile = rec.getPostProcessedFile();
if (inputFile.isDirectory()) {
inputFile = new File(inputFile, "playlist.m3u8");
}
int i = 0;
argsPlusFile[i++] = "-i";
argsPlusFile[i++] = inputFile.getCanonicalPath();
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.info(Arrays.toString(cmdline));
Process ffmpeg = Runtime.getRuntime().exec(cmdline, new String[0], rec.getPostProcessedFile().getParentFile());
setupLogging(ffmpeg, rec);
rec.setPostProcessedFile(remuxedFile);
if (inputFile.getName().equals("playlist.m3u8")) {
IoUtils.deleteDirectory(inputFile.getParentFile());
if (Objects.equals(inputFile.getParentFile(), rec.getAbsoluteFile())) {
rec.setAbsoluteFile(remuxedFile);
}
} else {
Files.deleteIfExists(inputFile.toPath());
if (Objects.equals(inputFile, rec.getAbsoluteFile())) {
rec.setAbsoluteFile(remuxedFile);
}
}
rec.setSingleFile(true);
rec.setSizeInByte(remuxedFile.length());
IoUtils.deleteEmptyParents(inputFile.getParentFile());
rec.getAssociatedFiles().remove(inputFile.getCanonicalPath());
rec.getAssociatedFiles().add(remuxedFile.getCanonicalPath());
}
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");
rec.getAssociatedFiles().add(ffmpegLog.getCanonicalPath());
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());
rec.getAssociatedFiles().remove(ffmpegLog.getCanonicalPath());
}
} 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;
}
}

View File

@ -0,0 +1,54 @@
package ctbrec.recorder.postprocessing;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
public class Rename extends AbstractPlaceholderAwarePostProcessor {
private static final Logger LOG = LoggerFactory.getLogger(Rename.class);
public static final String FILE_NAME_TEMPLATE = "filename.template";
public static final String DEFAULT = "${modelSanitizedName}_${localDateTime}.${fileSuffix}";
public static final String DEFAULT_DIR = "${modelSanitizedName}_${localDateTime}";
@Override
public String getName() {
return "rename";
}
@Override
public void postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException {
String defaultTemplate = rec.isSingleFile() ? DEFAULT : DEFAULT_DIR;
String filenameTemplate = getConfig().getOrDefault(FILE_NAME_TEMPLATE, defaultTemplate);
String filename = fillInPlaceHolders(filenameTemplate, rec, config);
File src = rec.getPostProcessedFile();
File target = new File(src.getParentFile(), filename);
if (Objects.equals(src, target)) {
return;
}
LOG.info("Renaming {} to {}", src.getName(), target.getName());
Files.move(rec.getPostProcessedFile().toPath(), target.toPath());
rec.setPostProcessedFile(target);
if (Objects.equals(src, rec.getAbsoluteFile())) {
rec.setAbsoluteFile(target);
}
rec.getAssociatedFiles().remove(src.getCanonicalPath());
rec.getAssociatedFiles().add(target.getCanonicalPath());
}
@Override
public String toString() {
String s = getName();
if (getConfig().containsKey(FILE_NAME_TEMPLATE)) {
s += " [" + getConfig().get(FILE_NAME_TEMPLATE) + ']';
}
return s;
}
}

View File

@ -0,0 +1,73 @@
package ctbrec.recorder.postprocessing;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.OS;
import ctbrec.Recording;
import ctbrec.io.StreamRedirectThread;
import ctbrec.recorder.RecordingManager;
import ctbrec.recorder.download.ProcessExitedUncleanException;
public class Script extends AbstractPlaceholderAwarePostProcessor {
private static final Logger LOG = LoggerFactory.getLogger(Script.class);
public static final String SCRIPT_EXECUTABLE = "script.executable";
public static final String SCRIPT_PARAMS = "script.params";
@Override
public String getName() {
return "execute script";
}
@Override
public void postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException {
List<String> cmdline = buildCommandLine(rec, config);
Runtime rt = Runtime.getRuntime();
String[] args = cmdline.toArray(new String[0]);
if (LOG.isDebugEnabled()) {
LOG.debug("Running {}", Arrays.toString(args));
}
Process process = rt.exec(args, OS.getEnvironment());
startLogging(process);
int exitCode = process.waitFor();
LOG.debug("Process finished with exit code {}", exitCode);
if (exitCode != 0) {
throw new ProcessExitedUncleanException("Script finished with exit code " + exitCode);
}
}
private List<String> buildCommandLine(Recording rec, Config config) throws IOException {
String script = getConfig().getOrDefault(SCRIPT_EXECUTABLE, "somescript");
String params = getConfig().getOrDefault(SCRIPT_PARAMS, "${absolutePath}");
List<String> cmdline = new ArrayList<>();
cmdline.add(script);
String replacedParams = fillInPlaceHolders(params, rec, config);
Arrays.stream(replacedParams.split(" ")).forEach(cmdline::add);
return cmdline;
}
private void startLogging(Process process) {
// TODO maybe write these to a separate log file, e.g. recname.ts.script.log
Thread std = new Thread(new StreamRedirectThread(process.getInputStream(), System.out));
std.setName("Process stdout pipe");
std.setDaemon(true);
std.start();
Thread err = new Thread(new StreamRedirectThread(process.getErrorStream(), System.err));
err.setName("Process stderr pipe");
err.setDaemon(true);
err.start();
}
@Override
public String toString() {
return (getName() + " " + getConfig().getOrDefault(Script.SCRIPT_EXECUTABLE, "")).trim();
}
}

View File

@ -0,0 +1,37 @@
package ctbrec.recorder.postprocessing;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.NotImplementedExcetion;
import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
public class Webhook extends AbstractPlaceholderAwarePostProcessor {
private static final Logger LOG = LoggerFactory.getLogger(Webhook.class);
public static final String URL = "webhook.url";
public static final String HEADERS = "webhook.headers";
public static final String METHOD = "webhook.method";
public static final String DATA = "webhook.data";
public static final String SECRET = "webhook.secret";
@Override
public String getName() {
return "webhook";
}
@Override
public void postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException {
throw new NotImplementedExcetion();
}
@Override
public String toString() {
return (getName() + " " + getConfig().getOrDefault(Webhook.URL, "")).trim();
}
}

View File

@ -0,0 +1,125 @@
package ctbrec.recorder.postprocessing;
import static org.junit.Assert.*;
import java.io.IOException;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import org.junit.Before;
import org.junit.Test;
import ctbrec.Config;
import ctbrec.Recording;
public class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest {
Recording rec;
Config config;
Move placeHolderAwarePp;
@Override
@Before
public void setup() throws IOException {
super.setup();
rec = new Recording();
rec.setModel(mockModel());
rec.setAbsoluteFile(original);
rec.setPostProcessedFile(postProcessed);
rec.setStartDate(now);
rec.setSingleFile(true);
config = mockConfig();
placeHolderAwarePp = new Move();
}
@Test
public void testModelNameReplacement() {
String input = "asdf_${modelName}_asdf";
assertEquals("asdf_Mockita Boobilicious_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
input = "asdf_${modelDisplayName}_asdf";
assertEquals("asdf_Mockita Boobilicious_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
input = "asdf_${modelSanitizedName}_asdf";
assertEquals("asdf_Mockita_Boobilicious_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
}
@Test
public void testSiteNameReplacement() {
String input = "asdf_${siteName}_asdf";
assertEquals("asdf_Chaturbate_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
input = "asdf_${siteSanitizedName}_asdf";
assertEquals("asdf_Chaturbate_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
}
@Test
public void testUtcTimeReplacement() {
String date = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")
.withLocale(Locale.US)
.withZone(ZoneOffset.UTC)
.format(rec.getStartDate());
String input = "asdf_${utcDateTime}_asdf";
assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
date = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")
.withLocale(Locale.US)
.withZone(ZoneOffset.UTC)
.format(rec.getStartDate());
input = "asdf_${utcDateTime(yyyyMMdd-HHmmss)}_asdf";
assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
}
@Test
public void testLocalTimeReplacement() {
String date = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")
.withLocale(Locale.US)
.withZone(ZoneId.systemDefault())
.format(rec.getStartDate());
String input = "asdf_${localDateTime}_asdf";
assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
date = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")
.withLocale(Locale.US)
.withZone(ZoneId.systemDefault())
.format(rec.getStartDate());
input = "asdf_${localDateTime(yyyyMMdd-HHmmss)}_asdf";
assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
}
@Test
public void testEpochReplacement() {
long epoch = now.toEpochMilli() / 1000;
String input = "asdf_${epochSecond}_asdf";
assertEquals("asdf_" + epoch + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
}
@Test
public void testFileSuffixReplacement() {
String input = "asdf_${fileSuffix}_asdf";
assertEquals("asdf_ts_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
}
@Test
public void testRecordingsDirReplacement() {
String input = "asdf_${recordingsDir}_asdf";
assertEquals("asdf_" + recDir.toString() + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
}
@Test
public void testAbsolutePathReplacement() {
String input = "asdf_${absolutePath}_asdf";
assertEquals("asdf_" + postProcessed.getAbsolutePath().toString() + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
}
@Test
public void testAbsoluteParentPathReplacement() {
String input = "asdf_${absoluteParentPath}_asdf";
assertEquals("asdf_" + postProcessed.getParentFile().toString() + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
}
@Test
public void testModelNotesReplacement() {
String input = "asdf_${modelNotes}_asdf";
assertEquals("asdf_tag,_foo,_bar_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
}
}

View File

@ -0,0 +1,84 @@
package ctbrec.recorder.postprocessing;
import static java.nio.charset.StandardCharsets.*;
import static java.nio.file.StandardOpenOption.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import org.apache.commons.io.FileUtils;
import org.junit.After;
import org.junit.Before;
import org.mockito.MockedStatic;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.Settings;
import ctbrec.recorder.RecordingManager;
import ctbrec.sites.Site;
import ctbrec.sites.chaturbate.Chaturbate;
public abstract class AbstractPpTest {
Path baseDir;
Path recDir;
File original;
File postProcessed;
File originalDir;
File postProcessedDir;
Instant now = Instant.now();
RecordingManager recordingManager;
MockedStatic<Config> configStatic;
@Before
public void setup() throws IOException {
baseDir = Files.createTempDirectory("ctbrec_test_");
recDir = baseDir.resolve("recordings");
original = new File(recDir.toFile(), "original.ts");
postProcessed = new File(recDir.toFile(), "postProcessed.ts");
originalDir = new File(recDir.toFile(), "original");
postProcessedDir = new File(recDir.toFile(), "postProcessed");
Files.createDirectories(original.getParentFile().toPath());
Files.write(original.toPath(), "foobar".getBytes(UTF_8), CREATE_NEW, WRITE, TRUNCATE_EXISTING);
Files.write(postProcessed.toPath(), "foobar".getBytes(UTF_8), CREATE_NEW, WRITE, TRUNCATE_EXISTING);
Files.createDirectories(originalDir.toPath());
FileUtils.touch(new File(originalDir, "playlist.m3u8"));
}
@After
public void teardown() throws IOException {
FileUtils.deleteDirectory(baseDir.toFile());
if (configStatic != null) {
configStatic.close();
configStatic = null;
}
}
Config mockConfig() {
Config config = mock(Config.class);
when(config.getSettings()).thenReturn(mockSettings());
when(config.getModelNotes(any())).thenReturn("tag, foo, bar");
when(config.getConfigDir()).thenReturn(new File(baseDir.toFile(), "config"));
configStatic = mockStatic(Config.class);
configStatic.when(Config::getInstance).thenReturn(config);
return config;
}
Model mockModel() {
Site site = new Chaturbate();
Model model = site.createModel("Mockita Boobilicious");
model.setDisplayName("Mockita Boobilicious");
return model;
}
Settings mockSettings() {
Settings settings = new Settings();
settings.recordingsDir = recDir.toString();
return settings;
}
}

View File

@ -0,0 +1,50 @@
package ctbrec.recorder.postprocessing;
import static org.junit.Assert.*;
import java.io.IOException;
import org.junit.Test;
import ctbrec.Config;
import ctbrec.Recording;
public class CopyTest extends AbstractPpTest {
@Test
public void testCopySingleFile() throws IOException, InterruptedException {
Config config = mockConfig();
Recording rec = new Recording();
rec.setModel(mockModel());
rec.setAbsoluteFile(original);
rec.setStartDate(now);
rec.setSingleFile(false);
Copy pp = new Copy();
pp.postprocess(rec, recordingManager, config);
assertNotEquals(rec.getAbsoluteFile(), rec.getPostProcessedFile());
assertTrue(original.exists());
assertTrue(rec.getPostProcessedFile().exists());
}
@Test
public void testCopyDirectory() throws IOException, InterruptedException {
Config config = mockConfig();
Recording rec = new Recording();
rec.setModel(mockModel());
rec.setAbsoluteFile(originalDir);
rec.setStartDate(now);
rec.setSingleFile(false);
Copy pp = new Copy();
pp.postprocess(rec, recordingManager, config);
assertNotEquals(rec.getAbsoluteFile(), rec.getPostProcessedFile());
assertTrue(originalDir.exists());
assertTrue(rec.getPostProcessedFile().exists());
}
@Test
public void testGetName() {
assertEquals("create a copy", new Copy().getName());
}
}

View File

@ -0,0 +1,57 @@
package ctbrec.recorder.postprocessing;
import static org.junit.Assert.*;
import java.io.IOException;
import java.nio.file.Files;
import org.junit.Test;
import ctbrec.Config;
import ctbrec.Recording;
public class DeleteOriginalTest extends AbstractPpTest {
@Test
public void testPostProcessWithSingleFile() throws IOException, InterruptedException {
Recording rec = new Recording();
rec.setModel(mockModel());
rec.setAbsoluteFile(original);
rec.setPostProcessedFile(postProcessed);
rec.setStartDate(now);
rec.setSingleFile(true);
Config config = mockConfig();
DeleteOriginal pp = new DeleteOriginal();
pp.postprocess(rec, null, config);
assertEquals(postProcessed, rec.getAbsoluteFile());
assertTrue(rec.getAbsoluteFile().exists());
assertFalse(original.exists());
}
@Test
public void testPostProcessWithDirectory() throws IOException, InterruptedException {
Recording rec = new Recording();
rec.setModel(mockModel());
rec.setAbsoluteFile(originalDir);
rec.setPostProcessedFile(postProcessedDir);
rec.setStartDate(now);
rec.setSingleFile(true);
Config config = mockConfig();
Files.createDirectories(postProcessedDir.toPath());
DeleteOriginal pp = new DeleteOriginal();
pp.postprocess(rec, null, config);
assertEquals(postProcessedDir, rec.getAbsoluteFile());
assertTrue(rec.getAbsoluteFile().exists());
assertFalse(originalDir.exists());
}
@Test
public void testGetName() {
assertEquals("delete original", new DeleteOriginal().getName());
}
}

View File

@ -0,0 +1,98 @@
package ctbrec.recorder.postprocessing;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.io.File;
import java.io.IOException;
import java.time.Duration;
import java.util.Collections;
import org.junit.Test;
import ctbrec.Config;
import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
import ctbrec.recorder.download.Download;
public class DeleteTooShortTest extends AbstractPpTest {
@Test
public void tooShortSingleFileRecShouldBeDeleted() throws IOException, InterruptedException {
testProcess(original);
}
@Test
public void tooShortDirectoryRecShouldBeDeleted() throws IOException, InterruptedException {
testProcess(originalDir);
}
private void testProcess(File original) throws IOException {
Recording rec = createRec(original);
Config config = mockConfig();
RecordingManager recordingManager = new RecordingManager(config, Collections.emptyList());
recordingManager.add(rec);
assertEquals(1, recordingManager.getAll().size());
DeleteTooShort pp = new DeleteTooShort();
pp.getConfig().put(DeleteTooShort.MIN_LEN_IN_SECS, "10");
pp.postprocess(rec, recordingManager, config);
assertFalse(rec.getAbsoluteFile().exists());
assertFalse(original.exists());
assertEquals(0, recordingManager.getAll().size());
}
@Test
public void testGetName() {
assertEquals("delete too short", new DeleteTooShort().getName());
}
@Test
public void testDisabledWithSingleFile() throws IOException, InterruptedException {
Recording rec = createRec(original);
Config config = mockConfig();
RecordingManager recordingManager = new RecordingManager(config, Collections.emptyList());
recordingManager.add(rec);
assertEquals(1, recordingManager.getAll().size());
DeleteTooShort pp = new DeleteTooShort();
pp.getConfig().put(DeleteTooShort.MIN_LEN_IN_SECS, "0");
pp.postprocess(rec, recordingManager, config);
assertTrue(rec.getAbsoluteFile().exists());
assertTrue(original.exists());
assertEquals(1, recordingManager.getAll().size());
}
@Test
public void longEnoughVideoShouldStay() throws IOException, InterruptedException {
Recording rec = createRec(original);
Config config = mockConfig();
RecordingManager recordingManager = new RecordingManager(config, Collections.emptyList());
recordingManager.add(rec);
assertEquals(1, recordingManager.getAll().size());
DeleteTooShort pp = new DeleteTooShort();
pp.getConfig().put(DeleteTooShort.MIN_LEN_IN_SECS, "1");
pp.postprocess(rec, recordingManager, config);
assertTrue(rec.getAbsoluteFile().exists());
assertTrue(original.exists());
assertEquals(1, recordingManager.getAll().size());
}
private Recording createRec(File original) {
Download download = mock(Download.class);
when(download.getLength()).thenReturn(Duration.ofSeconds(5));
Recording rec = new Recording();
rec.setModel(mockModel());
rec.setAbsoluteFile(original);
rec.setPostProcessedFile(original);
rec.setStartDate(now);
rec.setSingleFile(true);
rec.setDownload(download);
return rec;
}
}

View File

@ -0,0 +1,55 @@
package ctbrec.recorder.postprocessing;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.junit.Test;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.Recording;
public class MoveDirectoryTest extends AbstractPpTest {
@Test
public void testOriginalFileReplacement() throws IOException {
Config config = mockConfig();
Recording rec = new Recording();
rec.setModel(mockModel());
rec.setAbsoluteFile(originalDir);
rec.setStartDate(now);
rec.setSingleFile(false);
Move pp = new Move();
pp.getConfig().put(Move.PATH_TEMPLATE, new File(baseDir.toFile(), Move.DEFAULT).getAbsolutePath());
pp.postprocess(rec, recordingManager, config);
Matcher m = Pattern.compile(baseDir.toString() + "/Mockita_Boobilicious/\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}/original").matcher(rec.getAbsoluteFile().getCanonicalPath());
assertTrue(m.matches());
assertEquals(rec.getAbsoluteFile(), rec.getPostProcessedFile());
assertNotEquals(rec.getAbsoluteFile(), original);
}
@Test
public void absoluteFileShouldKeepBeingOriginalIfFilesDiffer() throws IOException {
Model model = mockModel();
Recording rec = mock(Recording.class);
when(rec.getModel()).thenReturn(model);
when(rec.getAbsoluteFile()).thenReturn(originalDir);
when(rec.getPostProcessedFile()).thenReturn(postProcessedDir);
when(rec.getStartDate()).thenReturn(now);
doThrow(new RuntimeException("Unexpected call of setAbsoluteFile")).when(rec).setAbsoluteFile(any());
Files.createDirectories(postProcessedDir.toPath());
Move pp = new Move();
Config config = mockConfig();
pp.getConfig().put(Move.PATH_TEMPLATE, new File(baseDir.toFile(), Move.DEFAULT).getAbsolutePath());
pp.postprocess(rec, recordingManager, config);
}
}

View File

@ -0,0 +1,77 @@
package ctbrec.recorder.postprocessing;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.io.File;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.junit.Test;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.Recording;
public class MoveSingleFileTest extends AbstractPpTest {
@Test
public void testOriginalFileReplacement() throws IOException {
Config config = mockConfig();
Recording rec = new Recording();
rec.setModel(mockModel());
rec.setAbsoluteFile(original);
rec.setStartDate(now);
rec.setSingleFile(true);
Move pp = new Move();
pp.getConfig().put(Move.PATH_TEMPLATE, new File(baseDir.toFile(), Move.DEFAULT).getAbsolutePath());
pp.postprocess(rec, recordingManager, config);
Matcher m = Pattern.compile(baseDir.toFile() + "/Mockita_Boobilicious/\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}/original\\.ts").matcher(rec.getAbsoluteFile().toString());
assertTrue(m.matches());
assertEquals(rec.getAbsoluteFile(), rec.getPostProcessedFile());
assertNotEquals(rec.getAbsoluteFile(), original);
}
@Test
public void testEarlyExit() throws IOException {
Model model = mockModel();
Recording rec = mock(Recording.class);
when(rec.getModel()).thenReturn(model);
when(rec.getAbsoluteFile()).thenReturn(original);
when(rec.getPostProcessedFile()).thenReturn(original);
when(rec.getStartDate()).thenReturn(now);
doThrow(new RuntimeException("Unexpected call of setAbsoluteFile")).when(rec).setAbsoluteFile(any());
Move pp = new Move();
Config config = mockConfig();
pp.getConfig().put(Move.PATH_TEMPLATE, original.getParentFile().getCanonicalPath());
pp.postprocess(rec, recordingManager, config);
}
@Test
public void absoluteFileShouldKeepBeingOriginalIfFilesDiffer() throws IOException {
Model model = mockModel();
Recording rec = mock(Recording.class);
when(rec.getModel()).thenReturn(model);
when(rec.getAbsoluteFile()).thenReturn(original);
when(rec.getPostProcessedFile()).thenReturn(postProcessed);
when(rec.getStartDate()).thenReturn(now);
doThrow(new RuntimeException("Unexpected call of setAbsoluteFile")).when(rec).setAbsoluteFile(any());
Move pp = new Move();
Config config = mockConfig();
pp.getConfig().put(Move.PATH_TEMPLATE, new File(baseDir.toFile(), Move.DEFAULT).getAbsolutePath());
pp.postprocess(rec, recordingManager, config);
}
@Test
public void testToString() {
Move pp = new Move();
assertEquals("move", pp.toString());
pp.getConfig().put(Move.PATH_TEMPLATE, Move.DEFAULT);
assertEquals("move ["+Move.DEFAULT+"]", pp.toString());
}
}

View File

@ -0,0 +1,42 @@
package ctbrec.recorder.postprocessing;
import static org.junit.Assert.*;
import java.io.IOException;
import java.util.Collections;
import org.junit.Test;
import ctbrec.Config;
import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
public class RemoveKeepFileTest extends AbstractPpTest {
@Test
public void testPostProcessWithSingleFile() throws IOException, InterruptedException {
Recording rec = new Recording();
rec.setModel(mockModel());
rec.setAbsoluteFile(original);
rec.setPostProcessedFile(postProcessed);
rec.setStartDate(now);
rec.setSingleFile(true);
Config config = mockConfig();
RecordingManager rm = new RecordingManager(config, Collections.emptyList());
rm.add(rec);
assertTrue(rm.getAll().size() == 1);
RemoveKeepFile pp = new RemoveKeepFile();
pp.postprocess(rec, rm, config);
assertTrue(rec.getAbsoluteFile().exists());
assertTrue(rec.getPostProcessedFile().exists());
assertTrue(rm.getAll().isEmpty());
}
@Test
public void testGetName() {
assertEquals("remove recording, but keep the files", new RemoveKeepFile().getName());
}
}

View File

@ -0,0 +1,52 @@
package ctbrec.recorder.postprocessing;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.io.IOException;
import java.nio.file.Files;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.junit.Test;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.Recording;
public class RenameDirectoryTest extends AbstractPpTest {
@Test
public void testOriginalFileReplacement() throws IOException {
Config config = mockConfig();
Recording rec = new Recording();
rec.setModel(mockModel());
rec.setAbsoluteFile(originalDir);
rec.setStartDate(now);
rec.setSingleFile(false);
Rename pp = new Rename();
pp.postprocess(rec, recordingManager, config);
Matcher m = Pattern.compile("Mockita_Boobilicious_\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}").matcher(rec.getAbsoluteFile().getName());
assertTrue(m.matches());
assertEquals(rec.getAbsoluteFile(), rec.getPostProcessedFile());
assertNotEquals(rec.getAbsoluteFile(), original);
}
@Test
public void absoluteFileShouldKeepBeingOriginalIfFilesDiffer() throws IOException {
Config config = mockConfig();
Model model = mockModel();
Recording rec = mock(Recording.class);
when(rec.getModel()).thenReturn(model);
when(rec.getAbsoluteFile()).thenReturn(originalDir);
when(rec.getPostProcessedFile()).thenReturn(postProcessedDir);
when(rec.getStartDate()).thenReturn(now);
doThrow(new RuntimeException("Unexpected call of setAbsoluteFile")).when(rec).setAbsoluteFile(any());
Files.createDirectories(postProcessedDir.toPath());
Rename pp = new Rename();
pp.postprocess(rec, recordingManager, config);
}
}

View File

@ -0,0 +1,73 @@
package ctbrec.recorder.postprocessing;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.junit.Test;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.Recording;
public class RenameSingleFileTest extends AbstractPpTest {
@Test
public void testOriginalFileReplacement() throws IOException {
Config config = mockConfig();
Recording rec = new Recording();
rec.setModel(mockModel());
rec.setAbsoluteFile(original);
rec.setStartDate(now);
rec.setSingleFile(true);
Rename pp = new Rename();
pp.postprocess(rec, recordingManager, config);
Matcher m = Pattern.compile("Mockita_Boobilicious_\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}\\.ts").matcher(rec.getAbsoluteFile().getName());
assertTrue(m.matches());
assertEquals(rec.getAbsoluteFile(), rec.getPostProcessedFile());
assertNotEquals(rec.getAbsoluteFile(), original);
}
@Test
public void testEarlyExit() throws IOException {
Config config = mockConfig();
Model model = mockModel();
Recording rec = mock(Recording.class);
when(rec.getModel()).thenReturn(model);
when(rec.getAbsoluteFile()).thenReturn(original);
when(rec.getPostProcessedFile()).thenReturn(original);
when(rec.getStartDate()).thenReturn(now);
doThrow(new RuntimeException("Unexpected call of setAbsoluteFile")).when(rec).setAbsoluteFile(any());
Rename pp = new Rename();
pp.getConfig().put(Rename.FILE_NAME_TEMPLATE, original.getName());
pp.postprocess(rec, recordingManager, config);
}
@Test
public void absoluteFileShouldKeepBeingOriginalIfFilesDiffer() throws IOException {
Config config = mockConfig();
Model model = mockModel();
Recording rec = mock(Recording.class);
when(rec.getModel()).thenReturn(model);
when(rec.getAbsoluteFile()).thenReturn(original);
when(rec.getPostProcessedFile()).thenReturn(postProcessed);
when(rec.getStartDate()).thenReturn(now);
doThrow(new RuntimeException("Unexpected call of setAbsoluteFile")).when(rec).setAbsoluteFile(any());
Rename pp = new Rename();
pp.postprocess(rec, recordingManager, config);
}
@Test
public void testToString() {
Rename pp = new Rename();
assertEquals("rename", pp.toString());
pp.getConfig().put(Rename.FILE_NAME_TEMPLATE, Rename.DEFAULT);
assertEquals("rename [${modelSanitizedName}_${localDateTime}.${fileSuffix}]", pp.toString());
}
}

View File

@ -6,7 +6,7 @@
<groupId>ctbrec</groupId> <groupId>ctbrec</groupId>
<artifactId>master</artifactId> <artifactId>master</artifactId>
<packaging>pom</packaging> <packaging>pom</packaging>
<version>3.9.0</version> <version>3.10.0</version>
<modules> <modules>
<module>../common</module> <module>../common</module>
@ -26,6 +26,14 @@
<artifactId>maven-assembly-plugin</artifactId> <artifactId>maven-assembly-plugin</artifactId>
<version>3.1.0</version> <version>3.1.0</version>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<redirectTestOutputToFile>true</redirectTestOutputToFile>
</configuration>
</plugin>
</plugins> </plugins>
</pluginManagement> </pluginManagement>
<plugins> <plugins>
@ -103,12 +111,23 @@
<artifactId>guava</artifactId> <artifactId>guava</artifactId>
<version>17.0</version> <version>17.0</version>
</dependency> </dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.8.0</version>
</dependency>
<dependency> <dependency>
<groupId>junit</groupId> <groupId>junit</groupId>
<artifactId>junit</artifactId> <artifactId>junit</artifactId>
<version>4.12</version> <version>4.12</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>3.5.11</version>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.eclipse.jetty</groupId> <groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId> <artifactId>jetty-server</artifactId>
@ -121,4 +140,8 @@
</dependency> </dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>
</project> </project>

View File

@ -8,7 +8,7 @@
<parent> <parent>
<groupId>ctbrec</groupId> <groupId>ctbrec</groupId>
<artifactId>master</artifactId> <artifactId>master</artifactId>
<version>3.9.0</version> <version>3.10.0</version>
<relativePath>../master</relativePath> <relativePath>../master</relativePath>
</parent> </parent>

View File

@ -57,10 +57,8 @@ public class ConfigServlet extends AbstractCtbrecServlet {
addParameter("generatePlaylist", "Generate Playlist", DataType.BOOLEAN, settings.generatePlaylist, json); addParameter("generatePlaylist", "Generate Playlist", DataType.BOOLEAN, settings.generatePlaylist, json);
addParameter("minimumResolution", "Minimum Resolution", DataType.INTEGER, settings.minimumResolution, json); addParameter("minimumResolution", "Minimum Resolution", DataType.INTEGER, settings.minimumResolution, json);
addParameter("maximumResolution", "Maximum Resolution", DataType.INTEGER, settings.maximumResolution, json); addParameter("maximumResolution", "Maximum Resolution", DataType.INTEGER, settings.maximumResolution, json);
addParameter("minimumLengthInSeconds", "Minimum Length (secs)", DataType.INTEGER, settings.minimumLengthInSeconds, json);
addParameter("minimumSpaceLeftInBytes", "Leave Space On Device (GiB)", DataType.LONG, settings.minimumSpaceLeftInBytes, json); addParameter("minimumSpaceLeftInBytes", "Leave Space On Device (GiB)", DataType.LONG, settings.minimumSpaceLeftInBytes, json);
addParameter("onlineCheckIntervalInSecs", "Online Check Interval (secs)", DataType.INTEGER, settings.onlineCheckIntervalInSecs, json); addParameter("onlineCheckIntervalInSecs", "Online Check Interval (secs)", DataType.INTEGER, settings.onlineCheckIntervalInSecs, json);
addParameter("postProcessing", "Post-Processing", DataType.STRING, settings.postProcessing, json);
addParameter("postProcessingThreads", "Post-Processing Threads", DataType.INTEGER, settings.postProcessingThreads, json); addParameter("postProcessingThreads", "Post-Processing Threads", DataType.INTEGER, settings.postProcessingThreads, json);
addParameter("recordingsDir", "Recordings Directory", DataType.STRING, settings.recordingsDir, json); addParameter("recordingsDir", "Recordings Directory", DataType.STRING, settings.recordingsDir, json);
addParameter("recordSingleFile", "Record Single File", DataType.BOOLEAN, settings.recordSingleFile, json); addParameter("recordSingleFile", "Record Single File", DataType.BOOLEAN, settings.recordSingleFile, json);

View File

@ -9,6 +9,8 @@ import java.nio.file.Paths;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -21,6 +23,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Recording;
import ctbrec.recorder.Recorder;
public class HlsServlet extends AbstractCtbrecServlet { public class HlsServlet extends AbstractCtbrecServlet {
@ -28,8 +32,11 @@ public class HlsServlet extends AbstractCtbrecServlet {
private final Config config; private final Config config;
public HlsServlet(Config config) { private Recorder recorder;
public HlsServlet(Config config, Recorder recorder) {
this.config = config; this.config = config;
this.recorder = recorder;
} }
@Override @Override
@ -39,37 +46,54 @@ public class HlsServlet extends AbstractCtbrecServlet {
Path recordingsDirPath = Paths.get(config.getSettings().recordingsDir).toAbsolutePath().normalize(); Path recordingsDirPath = Paths.get(config.getSettings().recordingsDir).toAbsolutePath().normalize();
Path requestedFilePath = recordingsDirPath.resolve(request).toAbsolutePath().normalize(); Path requestedFilePath = recordingsDirPath.resolve(request).toAbsolutePath().normalize();
boolean isValidRequestedPath = requestedFilePath.startsWith(recordingsDirPath);
if (isValidRequestedPath) {
File requestedFile = requestedFilePath.toFile(); File requestedFile = requestedFilePath.toFile();
if (requestedFile.getName().equals("playlist.m3u8")) {
try { try {
if (requestedFile.getName().equals("playlist.m3u8")) {
boolean isRequestAuthenticated = checkAuthentication(req, req.getRequestURI()); boolean isRequestAuthenticated = checkAuthentication(req, req.getRequestURI());
if (!isRequestAuthenticated) { if (!isRequestAuthenticated) {
writeResponse(resp, SC_UNAUTHORIZED, "{\"status\": \"error\", \"msg\": \"HMAC does not match\"}"); writeResponse(resp, SC_UNAUTHORIZED, "{\"status\": \"error\", \"msg\": \"HMAC does not match\"}");
return; return;
} }
String id = request.substring(0, request.indexOf('/'));
Optional<Recording> rec = getRecordingById(id);
if (rec.isPresent()) {
servePlaylist(req, resp, rec.get().getAbsoluteFile());
} else {
error404(req, resp);
return;
}
} else {
String id = request.split("/")[0];
Optional<Recording> rec = getRecordingById(id);
if (rec.isPresent()) {
File path = rec.get().getAbsoluteFile();
if (!path.isFile()) {
path = new File(path, requestedFile.getName());
}
if (LOG.isTraceEnabled()) {
Enumeration<String> headerNames = req.getHeaderNames();
while (headerNames.hasMoreElements()) {
String header = headerNames.nextElement();
LOG.trace("{}: {}", header, req.getHeader(header));
}
}
serveSegment(req, resp, path);
} else {
error404(req, resp);
return;
}
}
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) { } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
writeResponse(resp, SC_UNAUTHORIZED, "{\"status\": \"error\", \"msg\": \"Authentication failed\"}"); writeResponse(resp, SC_UNAUTHORIZED, "{\"status\": \"error\", \"msg\": \"Authentication failed\"}");
return; return;
} }
}
servePlaylist(req, resp, requestedFile); private Optional<Recording> getRecordingById(String id) throws InvalidKeyException, NoSuchAlgorithmException, IOException {
} else { return recorder.getRecordings().stream()
if (requestedFile.exists()) { .filter(r -> Objects.equals(id, r.getId()))
Enumeration<String> headerNames = req.getHeaderNames(); .findFirst();
while(headerNames.hasMoreElements()) {
String header = headerNames.nextElement();
LOG.trace("{}: {}", header, req.getHeader(header));
}
serveSegment(req, resp, requestedFile);
} else {
error404(req, resp);
}
}
} else {
writeResponse(resp, SC_FORBIDDEN, "Stop it!");
}
} }
private void writeResponse(HttpServletResponse resp, int code, String body) { private void writeResponse(HttpServletResponse resp, int code, String body) {
@ -82,6 +106,7 @@ public class HlsServlet extends AbstractCtbrecServlet {
} }
private void error404(HttpServletRequest req, HttpServletResponse resp) { private void error404(HttpServletRequest req, HttpServletResponse resp) {
writeResponse(resp, SC_NOT_FOUND, "{\"status\": \"error\", \"msg\": \"Recording not found\"}");
resp.setStatus(HttpServletResponse.SC_NOT_FOUND); resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
} }
@ -94,7 +119,9 @@ public class HlsServlet extends AbstractCtbrecServlet {
} }
private void servePlaylist(HttpServletRequest req, HttpServletResponse resp, File requestedFile) throws IOException { private void servePlaylist(HttpServletRequest req, HttpServletResponse resp, File requestedFile) throws IOException {
serveFile(req, resp, requestedFile, "application/x-mpegURL"); LOG.debug("Serving playlist {}", requestedFile);
File playlist = new File(requestedFile, "playlist.m3u8");
serveFile(req, resp, playlist, "application/x-mpegURL");
} }
private void serveFile(HttpServletRequest req, HttpServletResponse resp, File file, String contentType) throws IOException { private void serveFile(HttpServletRequest req, HttpServletResponse resp, File file, String contentType) throws IOException {
@ -107,6 +134,7 @@ public class HlsServlet extends AbstractCtbrecServlet {
byte[] buffer = new byte[1024 * 100]; byte[] buffer = new byte[1024 * 100];
long bytesLeft = range.to - range.from; long bytesLeft = range.to - range.from;
resp.setContentLengthLong(bytesLeft); resp.setContentLengthLong(bytesLeft);
resp.addHeader("Content-Disposition", "attachment; filename=\"" + file.getName() + "\"");
if (range.set) { if (range.set) {
resp.setHeader("Content-Range", "bytes " + range.from + '-' + range.to + '/' + file.length()); resp.setHeader("Content-Range", "bytes " + range.from + '-' + range.to + '/' + file.length());
} }

View File

@ -213,7 +213,7 @@ public class HttpServer {
holder = new ServletHolder(configServlet); holder = new ServletHolder(configServlet);
defaultContext.addServlet(holder, "/config"); defaultContext.addServlet(holder, "/config");
HlsServlet hlsServlet = new HlsServlet(this.config); HlsServlet hlsServlet = new HlsServlet(this.config, recorder);
holder = new ServletHolder(hlsServlet); holder = new ServletHolder(hlsServlet);
defaultContext.addServlet(holder, "/hls/*"); defaultContext.addServlet(holder, "/hls/*");

View File

@ -2,6 +2,7 @@ package ctbrec.recorder.server;
import static javax.servlet.http.HttpServletResponse.*; import static javax.servlet.http.HttpServletResponse.*;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
@ -25,6 +26,7 @@ import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.io.BandwidthMeter; import ctbrec.io.BandwidthMeter;
import ctbrec.io.FileJsonAdapter;
import ctbrec.io.InstantJsonAdapter; import ctbrec.io.InstantJsonAdapter;
import ctbrec.io.ModelJsonAdapter; import ctbrec.io.ModelJsonAdapter;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
@ -63,6 +65,7 @@ public class RecorderServlet extends AbstractCtbrecServlet {
Moshi moshi = new Moshi.Builder() Moshi moshi = new Moshi.Builder()
.add(Instant.class, new InstantJsonAdapter()) .add(Instant.class, new InstantJsonAdapter())
.add(Model.class, new ModelJsonAdapter(sites)) .add(Model.class, new ModelJsonAdapter(sites))
.add(File.class, new FileJsonAdapter())
.build(); .build();
JsonAdapter<Request> requestAdapter = moshi.adapter(Request.class); JsonAdapter<Request> requestAdapter = moshi.adapter(Request.class);
Request request = requestAdapter.fromJson(json); Request request = requestAdapter.fromJson(json);

View File

@ -1,5 +1,5 @@
function play(recording) { function play(recording) {
let src = recording.singleFile ? '/hls' + recording.path : recording.playlist; let src = recording.singleFile ? '/hls/' + recording.id : recording.playlist;
let hmacOfPath = CryptoJS.HmacSHA256(src, hmac); let hmacOfPath = CryptoJS.HmacSHA256(src, hmac);
src = '..' + src; src = '..' + src;
if(console) console.log("Path", src, "HMAC", hmacOfPath); if(console) console.log("Path", src, "HMAC", hmacOfPath);
@ -48,7 +48,7 @@ function play(recording) {
} }
function download(recording) { function download(recording) {
let src = recording.singleFile ? '/hls' + recording.path : recording.playlist; let src = recording.singleFile ? '/hls/' + recording.id : recording.playlist;
let hmacOfPath = CryptoJS.HmacSHA256(src, hmac); let hmacOfPath = CryptoJS.HmacSHA256(src, hmac);
src = '..' + src; src = '..' + src;
if(console) console.log("Path", src, "HMAC", hmacOfPath); if(console) console.log("Path", src, "HMAC", hmacOfPath);
@ -77,7 +77,7 @@ function calculateSize(sizeInByte) {
function isRecordingInArray(array, recording) { function isRecordingInArray(array, recording) {
for ( let idx in array) { for ( let idx in array) {
let r = array[idx]; let r = array[idx];
if (r.path === recording.path) { if (r.id === recording.id) {
return true; return true;
} }
} }
@ -115,10 +115,10 @@ function syncRecordings(recordings) {
recording.ko_progressString = ko.observable(recording.progress === -1 ? '' : recording.progress); recording.ko_progressString = ko.observable(recording.progress === -1 ? '' : recording.progress);
recording.ko_size = ko.observable(calculateSize(recording.sizeInByte)); recording.ko_size = ko.observable(calculateSize(recording.sizeInByte));
recording.ko_status = ko.observable(recording.status); recording.ko_status = ko.observable(recording.status);
if (recording.path.endsWith('.mp4')) { if (recording.singleFile) {
recording.playlist = '/hls' + recording.path; recording.playlist = '/hls/' + recording.id;
} else { } else {
recording.playlist = '/hls' + recording.path + '/playlist.m3u8'; recording.playlist = '/hls/' + recording.id + '/playlist.m3u8';
} }
observableRecordingsArray.push(recording); observableRecordingsArray.push(recording);
} }
@ -129,7 +129,7 @@ function syncRecordings(recordings) {
let recording = recordings[i]; let recording = recordings[i];
for ( let j in observableRecordingsArray()) { for ( let j in observableRecordingsArray()) {
let r = observableRecordingsArray()[j]; let r = observableRecordingsArray()[j];
if (recording.path === r.path) { if (recording.id === r.id) {
r.progress = recording.progress; r.progress = recording.progress;
r.sizeInByte = recording.sizeInByte; r.sizeInByte = recording.sizeInByte;
r.status = recording.status; r.status = recording.status;