forked from j62/ctbrec
Merge branch 'pp' into dev
This commit is contained in:
commit
8b6d246732
|
@ -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
|
||||
========================
|
||||
* Added support for Manyvids Live.
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<parent>
|
||||
<groupId>ctbrec</groupId>
|
||||
<artifactId>master</artifactId>
|
||||
<version>3.9.0</version>
|
||||
<version>3.10.0</version>
|
||||
<relativePath>../master</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package ctbrec.ui;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.Instant;
|
||||
|
||||
import ctbrec.Config;
|
||||
|
@ -156,11 +157,7 @@ public class JavaFxRecording extends Recording {
|
|||
setStatus(updated.getStatus());
|
||||
setProgress(updated.getProgress());
|
||||
setSizeInByte(updated.getSizeInByte());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPath() {
|
||||
return delegate.getPath();
|
||||
setSingleFile(updated.isSingleFile());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -192,6 +189,11 @@ public class JavaFxRecording extends Recording {
|
|||
return delegate.isSingleFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSingleFile(boolean singleFile) {
|
||||
delegate.setSingleFile(singleFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPinned() {
|
||||
return delegate.isPinned();
|
||||
|
@ -223,4 +225,26 @@ public class JavaFxRecording extends Recording {
|
|||
public StringProperty getNoteProperty() {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -152,7 +152,7 @@ public class Player {
|
|||
Config cfg = Config.getInstance();
|
||||
try {
|
||||
if (cfg.getSettings().localRecording && rec != null) {
|
||||
File file = new File(cfg.getSettings().recordingsDir, rec.getPath());
|
||||
File file = rec.getAbsoluteFile();
|
||||
String[] cmdline = createCmdline(file.getAbsolutePath());
|
||||
playerProcess = rt.exec(cmdline, OS.getEnvironment(), file.getParentFile());
|
||||
} else {
|
||||
|
@ -206,7 +206,7 @@ public class Player {
|
|||
private String getRemoteRecordingUrl(Recording rec, Config cfg)
|
||||
throws MalformedURLException, InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
|
||||
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) {
|
||||
URL u = new URL(recUrl);
|
||||
String path = u.getPath();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -88,19 +88,18 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
private SimpleIntegerProperty onlineCheckIntervalInSecs;
|
||||
private SimpleBooleanProperty onlineCheckSkipsPausedModels;
|
||||
private SimpleLongProperty leaveSpaceOnDevice;
|
||||
private SimpleIntegerProperty minimumLengthInSecs;
|
||||
private SimpleStringProperty ffmpegParameters;
|
||||
private SimpleStringProperty fileExtension;
|
||||
private SimpleStringProperty server;
|
||||
private SimpleIntegerProperty port;
|
||||
private SimpleStringProperty path;
|
||||
private SimpleStringProperty downloadFilename;
|
||||
private SimpleBooleanProperty requireAuthentication;
|
||||
private SimpleBooleanProperty transportLayerSecurity;
|
||||
private ExclusiveSelectionProperty recordLocal;
|
||||
private SimpleFileProperty postProcessing;
|
||||
private SimpleIntegerProperty postProcessingThreads;
|
||||
private SimpleBooleanProperty removeRecordingAfterPp;
|
||||
private IgnoreList ignoreList;
|
||||
private PostProcessingStepPanel postProcessingStepPanel;
|
||||
|
||||
public SettingsTab(List<Site> sites, Recorder recorder) {
|
||||
this.sites = sites;
|
||||
|
@ -137,23 +136,22 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
concurrentRecordings = new SimpleIntegerProperty(null, "concurrentRecordings", settings.concurrentRecordings);
|
||||
onlineCheckIntervalInSecs = new SimpleIntegerProperty(null, "onlineCheckIntervalInSecs", settings.onlineCheckIntervalInSecs);
|
||||
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);
|
||||
fileExtension = new SimpleStringProperty(null, "ffmpegFileSuffix", settings.ffmpegFileSuffix);
|
||||
server = new SimpleStringProperty(null, "httpServer", settings.httpServer);
|
||||
port = new SimpleIntegerProperty(null, "httpPort", settings.httpPort);
|
||||
path = new SimpleStringProperty(null, "servletContext", settings.servletContext);
|
||||
downloadFilename = new SimpleStringProperty(null, "downloadFilename", settings.downloadFilename);
|
||||
requireAuthentication = new SimpleBooleanProperty(null, "requireAuthentication", settings.requireAuthentication);
|
||||
requireAuthentication.addListener(this::requireAuthenticationChanged);
|
||||
transportLayerSecurity = new SimpleBooleanProperty(null, "transportLayerSecurity", settings.transportLayerSecurity);
|
||||
recordLocal = new ExclusiveSelectionProperty(null, "localRecording", settings.localRecording, "Local", "Remote");
|
||||
postProcessing = new SimpleFileProperty(null, "postProcessing", settings.postProcessing);
|
||||
postProcessingThreads = new SimpleIntegerProperty(null, "postProcessingThreads", settings.postProcessingThreads);
|
||||
removeRecordingAfterPp = new SimpleBooleanProperty(null, "removeRecordingAfterPostProcessing", settings.removeRecordingAfterPostProcessing);
|
||||
onlineCheckSkipsPausedModels = new SimpleBooleanProperty(null, "onlineCheckSkipsPausedModels", settings.onlineCheckSkipsPausedModels);
|
||||
}
|
||||
|
||||
private void createGui() {
|
||||
postProcessingStepPanel = new PostProcessingStepPanel(config);
|
||||
ignoreList = new IgnoreList(sites);
|
||||
List<Category> siteCategories = new ArrayList<>();
|
||||
for (Site site : sites) {
|
||||
|
@ -200,16 +198,15 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
Setting.of("Server", server),
|
||||
Setting.of("Port", port),
|
||||
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("Use Secure Communication (TLS)", transportLayerSecurity)
|
||||
)
|
||||
),
|
||||
Category.of("Post-Processing",
|
||||
Group.of("Post-Processing",
|
||||
Setting.of("Post-Processing", postProcessing),
|
||||
Setting.of("Threads", postProcessingThreads),
|
||||
Setting.of("Delete recordings shorter than (secs)", minimumLengthInSecs, "Delete recordings, which are shorter than x seconds. 0 to disable"),
|
||||
Setting.of("Remove recording after post-processing", removeRecordingAfterPp)
|
||||
Setting.of("Steps", postProcessingStepPanel)
|
||||
)
|
||||
),
|
||||
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("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("downloadFilename").ifPresent(s -> bindEnabledProperty(s, recordLocal));
|
||||
postProcessingStepPanel.disableProperty().bind(recordLocal.not());
|
||||
}
|
||||
|
||||
private void bindEnabledProperty(Setting s, BooleanExpression bindTo) {
|
||||
|
|
|
@ -2,6 +2,7 @@ package ctbrec.ui.settings.api;
|
|||
|
||||
import static java.util.Optional.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
@ -21,6 +22,7 @@ import javafx.scene.control.TreeView;
|
|||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
public class Preferences {
|
||||
|
@ -31,7 +33,10 @@ public class Preferences {
|
|||
|
||||
private TreeView<Category> categoryTree;
|
||||
|
||||
private PreferencesStorage preferencesStorage;
|
||||
|
||||
private Preferences(PreferencesStorage preferencesStorage, Category...categories) {
|
||||
this.preferencesStorage = preferencesStorage;
|
||||
this.categories = categories;
|
||||
for (Category category : categories) {
|
||||
assignPreferencesStorage(category, preferencesStorage);
|
||||
|
@ -56,15 +61,15 @@ public class Preferences {
|
|||
return new Preferences(preferencesStorage, categories);
|
||||
}
|
||||
|
||||
public void save() {
|
||||
throw new RuntimeException("save not implemented");
|
||||
public void save() throws IOException {
|
||||
preferencesStorage.save(this);
|
||||
}
|
||||
|
||||
Category[] getCategories() {
|
||||
return categories;
|
||||
}
|
||||
|
||||
public Node getView() {
|
||||
public Region getView(boolean withNavigation) {
|
||||
SearchBox search = new SearchBox(true);
|
||||
search.textProperty().addListener(this::filterTree);
|
||||
TreeItem<Category> categoryTreeItems = createCategoryTree(categories, new TreeItem<>(), null);
|
||||
|
@ -76,7 +81,9 @@ public class Preferences {
|
|||
VBox.setMargin(categoryTree, new Insets(2));
|
||||
|
||||
BorderPane main = new BorderPane();
|
||||
if (withNavigation) {
|
||||
main.setLeft(leftSide);
|
||||
}
|
||||
main.setCenter(new Label("Center"));
|
||||
BorderPane.setMargin(leftSide, new Insets(2));
|
||||
|
||||
|
@ -92,6 +99,10 @@ public class Preferences {
|
|||
return main;
|
||||
}
|
||||
|
||||
public Region getView() {
|
||||
return getView(true);
|
||||
}
|
||||
|
||||
private void filterTree(ObservableValue<? extends String> obs, String oldV, String newV) {
|
||||
String q = ofNullable(newV).orElse("").toLowerCase().trim();
|
||||
TreeItem<Category> filteredCategoryTree = createCategoryTree(categories, new TreeItem<>(), q);
|
||||
|
@ -151,6 +162,8 @@ public class Preferences {
|
|||
|
||||
private Node createGrid(Setting[] settings) throws Exception {
|
||||
GridPane pane = new GridPane();
|
||||
pane.setHgap(2);
|
||||
pane.vgapProperty().bind(pane.hgapProperty());
|
||||
int row = 0;
|
||||
for (Setting setting : settings) {
|
||||
Node node = setting.getGui();
|
||||
|
@ -198,13 +211,11 @@ public class Preferences {
|
|||
}
|
||||
|
||||
private void visit(Category cat, Consumer<Setting> visitor) {
|
||||
if (cat.hasGroups()) {
|
||||
for (Group group : cat.getGroups()) {
|
||||
for (Setting setting : group.getSettings()) {
|
||||
visitor.accept(setting);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cat.hasSubCategories()) {
|
||||
for (Category subcat : cat.getSubCategories()) {
|
||||
visit(subcat, visitor);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -228,7 +228,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
|
||||
@Override
|
||||
public String get() {
|
||||
String modelNotes = Config.getInstance().getSettings().modelNotes.getOrDefault(m.getUrl(), "");
|
||||
String modelNotes = Config.getInstance().getModelNotes(m);
|
||||
return modelNotes;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -559,9 +559,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
|
||||
private void onOpenDirectory(JavaFxRecording first) {
|
||||
String recordingsDir = Config.getInstance().getSettings().recordingsDir;
|
||||
String path = first.getPath();
|
||||
File tsFile = new File(recordingsDir, path);
|
||||
File tsFile = first.getAbsoluteFile();
|
||||
new Thread(() -> DesktopIntegration.open(tsFile.getParent())).start();
|
||||
}
|
||||
|
||||
|
@ -579,19 +577,19 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
|
||||
private void download(Recording recording) {
|
||||
LOG.debug("Path {}", recording.getPath());
|
||||
LOG.debug("Path {}", recording.getAbsoluteFile());
|
||||
String filename = proposeTargetFilename(recording);
|
||||
FileChooser chooser = new FileChooser();
|
||||
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);
|
||||
while(!dir.exists()) {
|
||||
while (!dir.exists()) {
|
||||
dir = dir.getParentFile();
|
||||
}
|
||||
chooser.setInitialDirectory(dir);
|
||||
}
|
||||
File target = chooser.showSaveDialog(null);
|
||||
if(target != null) {
|
||||
if (target != null) {
|
||||
config.getSettings().lastDownloadDir = target.getParent();
|
||||
startDownloadThread(target, recording);
|
||||
recording.setStatus(DOWNLOADING);
|
||||
|
@ -600,12 +598,12 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
|
||||
private String proposeTargetFilename(Recording recording) {
|
||||
String path = recording.getPath().substring(1);
|
||||
if(recording.isSingleFile()) {
|
||||
return new File(path).getName();
|
||||
return recording.getAbsoluteFile().getName();
|
||||
} else {
|
||||
String downloadFilename = config.getSettings().downloadFilename;
|
||||
String fileSuffix = config.getSettings().ffmpegFileSuffix;
|
||||
String filename = path.replace("/", "-").replace(".mp4", "") + '.' + fileSuffix;
|
||||
String filename = new DownloadPostprocessor().fillInPlaceHolders(downloadFilename, recording, config) + '.' + fileSuffix;
|
||||
return filename;
|
||||
}
|
||||
}
|
||||
|
@ -615,11 +613,11 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
try {
|
||||
String hlsBase = config.getServerUrl() + "/hls";
|
||||
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));
|
||||
download.start(url, target);
|
||||
} 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);
|
||||
download.init(config, recording.getModel(), Instant.now());
|
||||
LOG.info("Downloading {}", url);
|
||||
|
@ -641,7 +639,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
});
|
||||
t.setDaemon(true);
|
||||
t.setName("Download Thread " + recording.getPath());
|
||||
t.setName("Download Thread " + recording.getAbsoluteFile().toString());
|
||||
t.start();
|
||||
}
|
||||
|
||||
|
@ -650,7 +648,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
if (progress == 100) {
|
||||
recording.setStatus(FINISHED);
|
||||
recording.setProgress(-1);
|
||||
LOG.debug("Download finished for recording {}", recording.getPath());
|
||||
LOG.debug("Download finished for recording {} - {}", recording.getId(), recording.getAbsoluteFile());
|
||||
} else {
|
||||
recording.setStatus(DOWNLOADING);
|
||||
recording.setProgress(progress);
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<parent>
|
||||
<groupId>ctbrec</groupId>
|
||||
<artifactId>master</artifactId>
|
||||
<version>3.9.0</version>
|
||||
<version>3.10.0</version>
|
||||
<relativePath>../master</relativePath>
|
||||
</parent>
|
||||
|
||||
|
@ -50,6 +50,10 @@
|
|||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-controls</artifactId>
|
||||
|
@ -71,6 +75,11 @@
|
|||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-inline</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.xml.bind</groupId>
|
||||
<artifactId>jaxb-api</artifactId>
|
||||
|
|
|
@ -23,7 +23,10 @@ import org.slf4j.LoggerFactory;
|
|||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.Moshi;
|
||||
|
||||
import ctbrec.io.FileJsonAdapter;
|
||||
import ctbrec.io.ModelJsonAdapter;
|
||||
import ctbrec.io.PostProcessorJsonAdapter;
|
||||
import ctbrec.recorder.postprocessing.PostProcessor;
|
||||
import ctbrec.sites.Site;
|
||||
|
||||
public class Config {
|
||||
|
@ -55,6 +58,8 @@ public class Config {
|
|||
private void load() throws IOException {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(Model.class, new ModelJsonAdapter(sites))
|
||||
.add(PostProcessor.class, new PostProcessorJsonAdapter())
|
||||
.add(File.class, new FileJsonAdapter())
|
||||
.build();
|
||||
JsonAdapter<Settings> adapter = moshi.adapter(Settings.class).lenient();
|
||||
File configFile = new File(configDir, filename);
|
||||
|
@ -125,6 +130,8 @@ public class Config {
|
|||
public void save() throws IOException {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(Model.class, new ModelJsonAdapter())
|
||||
.add(PostProcessor.class, new PostProcessorJsonAdapter())
|
||||
.add(File.class, new FileJsonAdapter())
|
||||
.build();
|
||||
JsonAdapter<Settings> adapter = moshi.adapter(Settings.class).indent(" ");
|
||||
String json = adapter.toJson(settings);
|
||||
|
@ -186,4 +193,8 @@ public class Config {
|
|||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
public String getModelNotes(Model m) {
|
||||
return Config.getInstance().getSettings().modelNotes.getOrDefault(m.getUrl(), "");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package ctbrec;
|
|||
public class NotImplementedExcetion extends RuntimeException {
|
||||
|
||||
public NotImplementedExcetion() {
|
||||
super();
|
||||
super("Not implemented");
|
||||
}
|
||||
|
||||
public NotImplementedExcetion(String mesg) {
|
||||
|
|
|
@ -17,6 +17,8 @@ import java.time.LocalDateTime;
|
|||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -28,6 +30,7 @@ import ctbrec.recorder.download.Download;
|
|||
public class Recording implements Serializable {
|
||||
private static final transient Logger LOG = LoggerFactory.getLogger(Recording.class);
|
||||
|
||||
private String id;
|
||||
private Model model;
|
||||
private transient Download download;
|
||||
private Instant startDate;
|
||||
|
@ -39,6 +42,9 @@ public class Recording implements Serializable {
|
|||
private boolean singleFile = false;
|
||||
private boolean pinned = false;
|
||||
private String note;
|
||||
private Set<String> associatedFiles = new HashSet<>();
|
||||
private File absoluteFile = null;
|
||||
private File postProcessedFile = null;
|
||||
|
||||
public enum State {
|
||||
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() {
|
||||
return startDate;
|
||||
}
|
||||
|
@ -93,18 +107,38 @@ public class Recording implements Serializable {
|
|||
this.progress = progress;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
// public String getPath() {
|
||||
// return path;
|
||||
// }
|
||||
|
||||
public void setPath(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public File getAbsoluteFile() {
|
||||
if (absoluteFile == null) {
|
||||
String recordingsDir = Config.getInstance().getSettings().recordingsDir;
|
||||
File recordingsFile = new File(recordingsDir, getPath());
|
||||
return recordingsFile;
|
||||
File recordingsFile = new File(recordingsDir, path);
|
||||
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() {
|
||||
|
@ -186,7 +220,7 @@ public class Recording implements Serializable {
|
|||
int result = 1;
|
||||
result = prime * result + ((getStartDate() == null) ? 0 : (int) (getStartDate().toEpochMilli() ^ (getStartDate().toEpochMilli() >>> 32)));
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -207,11 +241,7 @@ public class Recording implements Serializable {
|
|||
} else if (!getModel().equals(other.getModel())) {
|
||||
return false;
|
||||
}
|
||||
if (getPath() == null) {
|
||||
if (other.getPath() != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!getPath().equals(other.getPath())) {
|
||||
if (!getAbsoluteFile().equals(other.getAbsoluteFile())) {
|
||||
return false;
|
||||
}
|
||||
if (getStartDate() == null) {
|
||||
|
@ -232,7 +262,7 @@ public class Recording implements Serializable {
|
|||
}
|
||||
|
||||
private long getSize() {
|
||||
File rec = new File(Config.getInstance().getSettings().recordingsDir, getPath());
|
||||
File rec = getAbsoluteFile();
|
||||
if (rec.isDirectory()) {
|
||||
return getDirectorySize(rec);
|
||||
} else {
|
||||
|
@ -278,4 +308,8 @@ public class Recording implements Serializable {
|
|||
public boolean canBePostProcessed() {
|
||||
return getStatus() == FAILED || getStatus() == WAITING || getStatus() == FINISHED;
|
||||
}
|
||||
|
||||
public Set<String> getAssociatedFiles() {
|
||||
return associatedFiles;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
|
||||
import ctbrec.event.EventHandlerConfiguration;
|
||||
import ctbrec.recorder.postprocessing.PostProcessor;
|
||||
|
||||
public class Settings {
|
||||
|
||||
|
@ -47,6 +48,7 @@ public class Settings {
|
|||
public int concurrentRecordings = 0;
|
||||
public boolean determineResolution = false;
|
||||
public List<String> disabledSites = new ArrayList<>();
|
||||
public String downloadFilename = "${modelSanitizedName}-${localDateTime}";
|
||||
public List<EventHandlerConfiguration> eventHandlers = new ArrayList<>();
|
||||
public String fc2livePassword = "";
|
||||
public String fc2liveUsername = "";
|
||||
|
@ -83,7 +85,6 @@ public class Settings {
|
|||
public String mfcModelsTableSortType = "";
|
||||
public String mfcPassword = "";
|
||||
public String mfcUsername = "";
|
||||
public int minimumLengthInSeconds = 0;
|
||||
public long minimumSpaceLeftInBytes = 0;
|
||||
public Map<String, String> modelNotes = new HashMap<>();
|
||||
public List<Model> models = new ArrayList<>();
|
||||
|
@ -92,8 +93,8 @@ public class Settings {
|
|||
public boolean onlineCheckSkipsPausedModels = false;
|
||||
public int overviewUpdateIntervalInSecs = 10;
|
||||
public String password = ""; // chaturbate password TODO maybe rename this onetime
|
||||
public String postProcessing = "";
|
||||
public int postProcessingThreads = 2;
|
||||
public List<PostProcessor> postProcessors = new ArrayList<>();
|
||||
public String proxyHost;
|
||||
public String proxyPassword;
|
||||
public String proxyPort;
|
||||
|
@ -110,7 +111,6 @@ public class Settings {
|
|||
public String recordingsSortColumn = "";
|
||||
public String recordingsSortType = "";
|
||||
public boolean recordSingleFile = false;
|
||||
public boolean removeRecordingAfterPostProcessing = false;
|
||||
public boolean requireAuthentication = false;
|
||||
public String servletContext = "";
|
||||
public boolean showPlayerStarting = false;
|
||||
|
|
|
@ -60,4 +60,14 @@ public class StringUtil {
|
|||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
public static String sanitize(String input) {
|
||||
return input
|
||||
.replace(' ', '_')
|
||||
.replace('\\', '_')
|
||||
.replace('/', '_')
|
||||
.replace('\'', '_')
|
||||
.replace('"', '_');
|
||||
} // @formatter:on
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package ctbrec.io;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class DevNull extends OutputStream {
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b) throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package ctbrec.io;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
import com.squareup.moshi.JsonWriter;
|
||||
|
||||
public class FileJsonAdapter extends JsonAdapter<File> {
|
||||
|
||||
@Override
|
||||
public File fromJson(JsonReader reader) throws IOException {
|
||||
String path = reader.nextString();
|
||||
if (path != null) {
|
||||
return new File(path);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toJson(JsonWriter writer, File value) throws IOException {
|
||||
if (value != null) {
|
||||
writer.value(value.getCanonicalPath());
|
||||
} else {
|
||||
writer.nullValue();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -9,7 +9,6 @@ import java.nio.file.FileStore;
|
|||
import java.nio.file.Files;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
|
@ -53,6 +52,7 @@ import ctbrec.event.NoSpaceLeftEvent;
|
|||
import ctbrec.event.RecordingStateChangedEvent;
|
||||
import ctbrec.io.HttpClient;
|
||||
import ctbrec.recorder.download.Download;
|
||||
import ctbrec.recorder.postprocessing.PostProcessor;
|
||||
import ctbrec.sites.Site;
|
||||
|
||||
public class NextGenLocalRecorder implements Recorder {
|
||||
|
@ -161,13 +161,14 @@ public class NextGenLocalRecorder implements Recorder {
|
|||
setRecordingStatus(recording, State.POST_PROCESSING);
|
||||
recordingManager.saveRecording(recording);
|
||||
recording.postprocess();
|
||||
List<PostProcessor> postProcessors = config.getSettings().postProcessors;
|
||||
for (PostProcessor postProcessor : postProcessors) {
|
||||
LOG.debug("Running post-processor: {}", postProcessor.getName());
|
||||
postProcessor.postprocess(recording, recordingManager, config);
|
||||
}
|
||||
setRecordingStatus(recording, State.FINISHED);
|
||||
recordingManager.saveRecording(recording);
|
||||
deleteIfTooShort(recording);
|
||||
LOG.info("Post-processing finished for {}", recording.getModel().getName());
|
||||
if (config.getSettings().removeRecordingAfterPostProcessing) {
|
||||
recordingManager.remove(recording);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (e instanceof InterruptedException) { // NOSONAR
|
||||
Thread.currentThread().interrupt();
|
||||
|
@ -276,8 +277,11 @@ public class NextGenLocalRecorder implements Recorder {
|
|||
private Recording createRecording(Download download) throws IOException {
|
||||
Model model = download.getModel();
|
||||
Recording rec = new Recording();
|
||||
rec.setId(UUID.randomUUID().toString());
|
||||
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.setStartDate(download.getStartTime());
|
||||
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
|
||||
public void stopRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||
recorderLock.lock();
|
||||
|
@ -636,12 +624,14 @@ public class NextGenLocalRecorder implements Recorder {
|
|||
|
||||
@Override
|
||||
public void rerunPostProcessing(Recording recording) {
|
||||
recording.setPostProcessedFile(null);
|
||||
List<Recording> recordings = recordingManager.getAll();
|
||||
for (Recording other : recordings) {
|
||||
if(other.equals(recording)) {
|
||||
Download download = other.getModel().createDownload();
|
||||
download.init(Config.getInstance(), other.getModel(), other.getStartDate());
|
||||
other.setDownload(download);
|
||||
other.setPostProcessedFile(null);
|
||||
submitPostProcessingJob(other);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -139,7 +139,7 @@ public class OnlineMonitor extends Thread {
|
|||
}
|
||||
|
||||
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;
|
||||
if(timeCheckTook.getSeconds() < sleepTime) {
|
||||
try {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package ctbrec.recorder;
|
||||
|
||||
import static ctbrec.Recording.State.*;
|
||||
import static ctbrec.io.IoUtils.*;
|
||||
import static java.nio.charset.StandardCharsets.*;
|
||||
import static java.nio.file.StandardOpenOption.*;
|
||||
|
||||
|
@ -13,6 +14,7 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
@ -25,6 +27,7 @@ import ctbrec.Config;
|
|||
import ctbrec.Model;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.Recording.State;
|
||||
import ctbrec.io.FileJsonAdapter;
|
||||
import ctbrec.io.InstantJsonAdapter;
|
||||
import ctbrec.io.ModelJsonAdapter;
|
||||
import ctbrec.sites.Site;
|
||||
|
@ -43,6 +46,7 @@ public class RecordingManager {
|
|||
moshi = new Moshi.Builder()
|
||||
.add(Model.class, new ModelJsonAdapter(sites))
|
||||
.add(Instant.class, new InstantJsonAdapter())
|
||||
.add(File.class, new FileJsonAdapter())
|
||||
.build();
|
||||
adapter = moshi.adapter(Recording.class).indent(" ");
|
||||
|
||||
|
@ -50,6 +54,10 @@ public class RecordingManager {
|
|||
}
|
||||
|
||||
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);
|
||||
recordingsLock.lock();
|
||||
try {
|
||||
|
@ -60,14 +68,14 @@ public class RecordingManager {
|
|||
}
|
||||
|
||||
public void saveRecording(Recording rec) throws IOException {
|
||||
if (rec.getMetaDataFile() != null) {
|
||||
File recordingMetaData = new File(rec.getMetaDataFile());
|
||||
String json = adapter.toJson(rec);
|
||||
File recordingsMetaDir = getDir();
|
||||
String filename = rec.toString() + ".json";
|
||||
File recordingMetaData = new File(recordingsMetaDir, filename);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadRecordings() throws IOException {
|
||||
File recordingsMetaDir = getDir();
|
||||
|
@ -80,6 +88,10 @@ public class RecordingManager {
|
|||
if (recording.getStatus() == RECORDING || recording.getStatus() == GENERATING_PLAYLIST || recording.getStatus() == POST_PROCESSING) {
|
||||
recording.setStatus(WAITING);
|
||||
}
|
||||
if (recording.getId() == null) {
|
||||
recording.setId(UUID.randomUUID().toString());
|
||||
saveRecording(recording);
|
||||
}
|
||||
if (recordingExists(recording)) {
|
||||
recordings.add(recording);
|
||||
} else {
|
||||
|
@ -94,8 +106,7 @@ public class RecordingManager {
|
|||
}
|
||||
|
||||
private boolean recordingExists(Recording recording) {
|
||||
File rec = new File(config.getSettings().recordingsDir, recording.getPath());
|
||||
return rec.exists();
|
||||
return recording.getAbsoluteFile().exists();
|
||||
}
|
||||
|
||||
private File getDir() {
|
||||
|
@ -120,22 +131,39 @@ public class RecordingManager {
|
|||
recording = recordings.get(idx);
|
||||
|
||||
recording.setStatus(State.DELETING);
|
||||
File recordingsDir = new File(config.getSettings().recordingsDir);
|
||||
File path = new File(recordingsDir, recording.getPath());
|
||||
File path = recording.getAbsoluteFile();
|
||||
boolean isFile = path.isFile();
|
||||
LOG.debug("Deleting {}", path);
|
||||
|
||||
// delete the video files
|
||||
if (path.isFile()) {
|
||||
if (isFile) {
|
||||
Files.delete(path.toPath());
|
||||
deleteEmptyParents(path.getParentFile());
|
||||
} else {
|
||||
deleteDirectory(path);
|
||||
deleteEmptyParents(path);
|
||||
}
|
||||
|
||||
// delete files associated with this recording
|
||||
for (String associated : recording.getAssociatedFiles()) {
|
||||
File f = new File(associated);
|
||||
if (f.isFile()) {
|
||||
Files.delete(f.toPath());
|
||||
deleteEmptyParents(f.getParentFile());
|
||||
} else {
|
||||
deleteDirectory(f);
|
||||
deleteEmptyParents(f);
|
||||
}
|
||||
}
|
||||
|
||||
// delete the meta data
|
||||
Files.deleteIfExists(new File(recording.getMetaDataFile()).toPath());
|
||||
|
||||
// delete empty parent files
|
||||
if (isFile) {
|
||||
deleteEmptyParents(path.getParentFile());
|
||||
} else {
|
||||
deleteEmptyParents(path);
|
||||
}
|
||||
|
||||
// remove from data structure
|
||||
recordings.remove(recording);
|
||||
recording.setStatus(State.DELETED);
|
||||
|
@ -154,8 +182,7 @@ public class RecordingManager {
|
|||
try {
|
||||
int idx = recordings.indexOf(recording);
|
||||
recording = recordings.get(idx);
|
||||
File recordingsDir = new File(config.getSettings().recordingsDir);
|
||||
File path = new File(recordingsDir, recording.getPath());
|
||||
File path = recording.getAbsoluteFile();
|
||||
deleteEmptyParents(path.getParentFile());
|
||||
// delete the meta data
|
||||
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 {
|
||||
recordingsLock.lock();
|
||||
try {
|
||||
|
|
|
@ -27,6 +27,7 @@ import ctbrec.event.EventBusHolder;
|
|||
import ctbrec.event.NoSpaceLeftEvent;
|
||||
import ctbrec.event.RecordingStateChangedEvent;
|
||||
import ctbrec.io.BandwidthMeter;
|
||||
import ctbrec.io.FileJsonAdapter;
|
||||
import ctbrec.io.HttpClient;
|
||||
import ctbrec.io.HttpException;
|
||||
import ctbrec.io.InstantJsonAdapter;
|
||||
|
@ -45,7 +46,11 @@ public class RemoteRecorder implements Recorder {
|
|||
private static final Logger LOG = LoggerFactory.getLogger(RemoteRecorder.class);
|
||||
|
||||
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<RecordingListResponse> recordingListResponseAdapter = moshi.adapter(RecordingListResponse.class);
|
||||
private JsonAdapter<ModelRequest> modelRequestAdapter = moshi.adapter(ModelRequest.class);
|
||||
|
@ -325,7 +330,7 @@ public class RemoteRecorder implements Recorder {
|
|||
int idx = newRecordings.indexOf(recording);
|
||||
Recording newRecording = newRecordings.get(idx);
|
||||
if (newRecording.getStatus() != recording.getStatus()) {
|
||||
File file = new File(recording.getPath());
|
||||
File file = recording.getAbsoluteFile();
|
||||
RecordingStateChangedEvent evt = new RecordingStateChangedEvent(file, newRecording.getStatus(), recording.getModel(),
|
||||
recording.getStartDate());
|
||||
EventBusHolder.BUS.post(evt);
|
||||
|
@ -337,7 +342,7 @@ public class RemoteRecorder implements Recorder {
|
|||
justStarted.removeAll(recordings);
|
||||
for (Recording recording : justStarted) {
|
||||
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(),
|
||||
recording.getStartDate());
|
||||
EventBusHolder.BUS.post(evt);
|
||||
|
@ -345,6 +350,19 @@ public class RemoteRecorder implements Recorder {
|
|||
}
|
||||
|
||||
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 {
|
||||
LOG.error(SERVER_RETURNED_ERROR, resp.status, resp.msg);
|
||||
}
|
||||
|
|
|
@ -1,59 +1,11 @@
|
|||
package ctbrec.recorder.download;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
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 {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractDownload.class);
|
||||
|
||||
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
|
||||
public Instant getStartTime() {
|
||||
return startTime;
|
||||
|
|
|
@ -391,13 +391,13 @@ public class DashDownload extends AbstractDownload {
|
|||
try {
|
||||
Thread.currentThread().setName("PP " + model.getName());
|
||||
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 file = new File(dir.getParentFile(), dir.getName().substring(0, dir.getName().length() - 5));
|
||||
new FfmpegMuxer(dir, file);
|
||||
targetFile = file;
|
||||
recording.setPath(path.substring(0, path.length() - 5));
|
||||
runPostProcessingScript(recording);
|
||||
} catch (Exception e) {
|
||||
throw new PostProcessingException(e);
|
||||
}
|
||||
|
|
|
@ -125,12 +125,6 @@ public class FFmpegDownload extends AbstractHlsDownload {
|
|||
|
||||
@Override
|
||||
public void postprocess(Recording recording) {
|
||||
Thread.currentThread().setName("PP " + model.getName());
|
||||
try {
|
||||
runPostProcessingScript(recording);
|
||||
} catch (Exception e) {
|
||||
throw new PostProcessingException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -215,7 +215,6 @@ public class HlsDownload extends AbstractHlsDownload {
|
|||
try {
|
||||
generatePlaylist(recording);
|
||||
recording.setStatusWithEvent(State.POST_PROCESSING);
|
||||
runPostProcessingScript(recording);
|
||||
} catch (Exception e) {
|
||||
throw new PostProcessingException(e);
|
||||
}
|
||||
|
|
|
@ -492,12 +492,6 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
|||
|
||||
@Override
|
||||
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 {
|
||||
|
|
|
@ -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(""));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package ctbrec.recorder.postprocessing;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public abstract class AbstractPostProcessor implements PostProcessor {
|
||||
|
||||
private Map<String, String> config = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public Map<String, String> getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setConfig(Map<String, String> conf) {
|
||||
this.config = conf;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getName();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,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";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
<groupId>ctbrec</groupId>
|
||||
<artifactId>master</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<version>3.9.0</version>
|
||||
<version>3.10.0</version>
|
||||
|
||||
<modules>
|
||||
<module>../common</module>
|
||||
|
@ -26,6 +26,14 @@
|
|||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
</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>
|
||||
</pluginManagement>
|
||||
<plugins>
|
||||
|
@ -103,12 +111,23 @@
|
|||
<artifactId>guava</artifactId>
|
||||
<version>17.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>2.8.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.12</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-inline</artifactId>
|
||||
<version>3.5.11</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-server</artifactId>
|
||||
|
@ -121,4 +140,8 @@
|
|||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
|
||||
|
||||
|
||||
</project>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<parent>
|
||||
<groupId>ctbrec</groupId>
|
||||
<artifactId>master</artifactId>
|
||||
<version>3.9.0</version>
|
||||
<version>3.10.0</version>
|
||||
<relativePath>../master</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -57,10 +57,8 @@ public class ConfigServlet extends AbstractCtbrecServlet {
|
|||
addParameter("generatePlaylist", "Generate Playlist", DataType.BOOLEAN, settings.generatePlaylist, json);
|
||||
addParameter("minimumResolution", "Minimum Resolution", DataType.INTEGER, settings.minimumResolution, 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("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("recordingsDir", "Recordings Directory", DataType.STRING, settings.recordingsDir, json);
|
||||
addParameter("recordSingleFile", "Record Single File", DataType.BOOLEAN, settings.recordSingleFile, json);
|
||||
|
|
|
@ -9,6 +9,8 @@ import java.nio.file.Paths;
|
|||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
@ -21,6 +23,8 @@ import org.slf4j.Logger;
|
|||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.recorder.Recorder;
|
||||
|
||||
public class HlsServlet extends AbstractCtbrecServlet {
|
||||
|
||||
|
@ -28,8 +32,11 @@ public class HlsServlet extends AbstractCtbrecServlet {
|
|||
|
||||
private final Config config;
|
||||
|
||||
public HlsServlet(Config config) {
|
||||
private Recorder recorder;
|
||||
|
||||
public HlsServlet(Config config, Recorder recorder) {
|
||||
this.config = config;
|
||||
this.recorder = recorder;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -39,37 +46,54 @@ public class HlsServlet extends AbstractCtbrecServlet {
|
|||
Path recordingsDirPath = Paths.get(config.getSettings().recordingsDir).toAbsolutePath().normalize();
|
||||
Path requestedFilePath = recordingsDirPath.resolve(request).toAbsolutePath().normalize();
|
||||
|
||||
boolean isValidRequestedPath = requestedFilePath.startsWith(recordingsDirPath);
|
||||
if (isValidRequestedPath) {
|
||||
File requestedFile = requestedFilePath.toFile();
|
||||
if (requestedFile.getName().equals("playlist.m3u8")) {
|
||||
try {
|
||||
if (requestedFile.getName().equals("playlist.m3u8")) {
|
||||
boolean isRequestAuthenticated = checkAuthentication(req, req.getRequestURI());
|
||||
if (!isRequestAuthenticated) {
|
||||
writeResponse(resp, SC_UNAUTHORIZED, "{\"status\": \"error\", \"msg\": \"HMAC does not match\"}");
|
||||
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) {
|
||||
writeResponse(resp, SC_UNAUTHORIZED, "{\"status\": \"error\", \"msg\": \"Authentication failed\"}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
servePlaylist(req, resp, requestedFile);
|
||||
} else {
|
||||
if (requestedFile.exists()) {
|
||||
Enumeration<String> headerNames = req.getHeaderNames();
|
||||
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 Optional<Recording> getRecordingById(String id) throws InvalidKeyException, NoSuchAlgorithmException, IOException {
|
||||
return recorder.getRecordings().stream()
|
||||
.filter(r -> Objects.equals(id, r.getId()))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
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) {
|
||||
writeResponse(resp, SC_NOT_FOUND, "{\"status\": \"error\", \"msg\": \"Recording 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 {
|
||||
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 {
|
||||
|
@ -107,6 +134,7 @@ public class HlsServlet extends AbstractCtbrecServlet {
|
|||
byte[] buffer = new byte[1024 * 100];
|
||||
long bytesLeft = range.to - range.from;
|
||||
resp.setContentLengthLong(bytesLeft);
|
||||
resp.addHeader("Content-Disposition", "attachment; filename=\"" + file.getName() + "\"");
|
||||
if (range.set) {
|
||||
resp.setHeader("Content-Range", "bytes " + range.from + '-' + range.to + '/' + file.length());
|
||||
}
|
||||
|
|
|
@ -213,7 +213,7 @@ public class HttpServer {
|
|||
holder = new ServletHolder(configServlet);
|
||||
defaultContext.addServlet(holder, "/config");
|
||||
|
||||
HlsServlet hlsServlet = new HlsServlet(this.config);
|
||||
HlsServlet hlsServlet = new HlsServlet(this.config, recorder);
|
||||
holder = new ServletHolder(hlsServlet);
|
||||
defaultContext.addServlet(holder, "/hls/*");
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package ctbrec.recorder.server;
|
|||
|
||||
import static javax.servlet.http.HttpServletResponse.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
@ -25,6 +26,7 @@ import ctbrec.Config;
|
|||
import ctbrec.Model;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.io.BandwidthMeter;
|
||||
import ctbrec.io.FileJsonAdapter;
|
||||
import ctbrec.io.InstantJsonAdapter;
|
||||
import ctbrec.io.ModelJsonAdapter;
|
||||
import ctbrec.recorder.Recorder;
|
||||
|
@ -63,6 +65,7 @@ public class RecorderServlet extends AbstractCtbrecServlet {
|
|||
Moshi moshi = new Moshi.Builder()
|
||||
.add(Instant.class, new InstantJsonAdapter())
|
||||
.add(Model.class, new ModelJsonAdapter(sites))
|
||||
.add(File.class, new FileJsonAdapter())
|
||||
.build();
|
||||
JsonAdapter<Request> requestAdapter = moshi.adapter(Request.class);
|
||||
Request request = requestAdapter.fromJson(json);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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);
|
||||
src = '..' + src;
|
||||
if(console) console.log("Path", src, "HMAC", hmacOfPath);
|
||||
|
@ -48,7 +48,7 @@ function play(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);
|
||||
src = '..' + src;
|
||||
if(console) console.log("Path", src, "HMAC", hmacOfPath);
|
||||
|
@ -77,7 +77,7 @@ function calculateSize(sizeInByte) {
|
|||
function isRecordingInArray(array, recording) {
|
||||
for ( let idx in array) {
|
||||
let r = array[idx];
|
||||
if (r.path === recording.path) {
|
||||
if (r.id === recording.id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -115,10 +115,10 @@ function syncRecordings(recordings) {
|
|||
recording.ko_progressString = ko.observable(recording.progress === -1 ? '' : recording.progress);
|
||||
recording.ko_size = ko.observable(calculateSize(recording.sizeInByte));
|
||||
recording.ko_status = ko.observable(recording.status);
|
||||
if (recording.path.endsWith('.mp4')) {
|
||||
recording.playlist = '/hls' + recording.path;
|
||||
if (recording.singleFile) {
|
||||
recording.playlist = '/hls/' + recording.id;
|
||||
} else {
|
||||
recording.playlist = '/hls' + recording.path + '/playlist.m3u8';
|
||||
recording.playlist = '/hls/' + recording.id + '/playlist.m3u8';
|
||||
}
|
||||
observableRecordingsArray.push(recording);
|
||||
}
|
||||
|
@ -129,7 +129,7 @@ function syncRecordings(recordings) {
|
|||
let recording = recordings[i];
|
||||
for ( let j in observableRecordingsArray()) {
|
||||
let r = observableRecordingsArray()[j];
|
||||
if (recording.path === r.path) {
|
||||
if (recording.id === r.id) {
|
||||
r.progress = recording.progress;
|
||||
r.sizeInByte = recording.sizeInByte;
|
||||
r.status = recording.status;
|
||||
|
|
Loading…
Reference in New Issue