forked from j62/ctbrec
1
0
Fork 0

Add recording priority for models

Models with high priority will be favored over models with low priority.
Recordings for models with low priority might even get stopped to free
up a slot for a model with a higher priority
This commit is contained in:
0xboobface 2020-01-03 19:06:05 +01:00
parent 8ae41142d1
commit 4d6e74562c
7 changed files with 255 additions and 76 deletions

View File

@ -1,3 +1,14 @@
3.1.0
========================
* Added recording priorities for models. If you restrict the number of
concurrent downloads, models with high priority will be favored over models
with low prio. Running recordings of models with low prio might even get
stopped, so that models with higher prio can get recorded.
You can adjust the prio on the "Recroding" tab by double-clicking on the
value or by using your scroll wheel while holding down CTRL
* Added menu entry to open the recording dir of a model
3.0.4
========================
* MFC now uses DASH again :) You can switch betwenn DASH and HLS in the settings

View File

@ -17,6 +17,7 @@ import ctbrec.recorder.download.StreamSource;
import ctbrec.sites.Site;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
/**
* Just a wrapper for Model, which augments it with JavaFX value binding properties, so that UI widgets get updated proeprly
@ -25,10 +26,12 @@ public class JavaFxModel implements Model {
private transient BooleanProperty onlineProperty = new SimpleBooleanProperty();
private transient BooleanProperty recordingProperty = new SimpleBooleanProperty();
private transient BooleanProperty pausedProperty = new SimpleBooleanProperty();
private transient SimpleIntegerProperty priorityProperty = new SimpleIntegerProperty();
private Model delegate;
public JavaFxModel(Model delegate) {
this.delegate = delegate;
setPriority(delegate.getPriority());
}
@Override
@ -103,6 +106,10 @@ public class JavaFxModel implements Model {
return pausedProperty;
}
public SimpleIntegerProperty getPriorityProperty() {
return priorityProperty;
}
public Model getDelegate() {
return delegate;
}
@ -216,6 +223,17 @@ public class JavaFxModel implements Model {
delegate.setDisplayName(name);
}
@Override
public void setPriority(int priority) {
delegate.setPriority(priority);
priorityProperty.set(priority);
}
@Override
public int getPriority() {
return delegate.getPriority();
}
@Override
public int compareTo(Model o) {
return delegate.compareTo(o);

View File

@ -8,6 +8,7 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@ -38,10 +39,12 @@ import ctbrec.ui.controls.SearchBox;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringPropertyBase;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@ -53,7 +56,9 @@ import javafx.scene.control.MenuItem;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.Tab;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellEditEvent;
import javafx.scene.control.TableColumn.SortType;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
@ -61,6 +66,7 @@ import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.ContextMenuEvent;
@ -72,7 +78,10 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.util.Callback;
import javafx.util.Duration;
import javafx.util.StringConverter;
import javafx.util.converter.NumberStringConverter;
public class RecordedModelsTab extends Tab implements TabSelectionListener {
private static final Logger LOG = LoggerFactory.getLogger(RecordedModelsTab.class);
@ -81,6 +90,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
private ScheduledService<List<JavaFxModel>> updateService;
private Recorder recorder;
private List<Site> sites;
private volatile boolean cellEditing = false;
FlowPane grid = new FlowPane();
ScrollPane scrollPane = new ScrollPane();
@ -136,9 +146,11 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
TableColumn<JavaFxModel, String> name = new TableColumn<>("Model");
name.setPrefWidth(200);
name.setCellValueFactory(new PropertyValueFactory<JavaFxModel, String>("displayName"));
name.setCellFactory(new ClickableCellFactory<>());
name.setEditable(false);
TableColumn<JavaFxModel, String> url = new TableColumn<>("URL");
url.setCellValueFactory(new PropertyValueFactory<JavaFxModel, String>("url"));
url.setCellFactory(new ClickableCellFactory<>());
url.setPrefWidth(400);
url.setEditable(false);
TableColumn<JavaFxModel, Boolean> online = new TableColumn<>("Online");
@ -156,6 +168,14 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
paused.setCellFactory(CheckBoxTableCell.forTableColumn(paused));
paused.setPrefWidth(100);
paused.setEditable(true);
TableColumn<JavaFxModel, Number> priority = new TableColumn<>("Priority");
priority.setCellValueFactory(param -> param.getValue().getPriorityProperty());
priority.setCellFactory(new PriorityCellFactory());
priority.setPrefWidth(90);
priority.setEditable(true);
priority.setOnEditStart(e -> cellEditing = true);
priority.setOnEditCommit(this::updatePriority);
priority.setOnEditCancel(e -> cellEditing = false);
TableColumn<JavaFxModel, String> notes = new TableColumn<>("Notes");
notes.setCellValueFactory(cdf -> {
JavaFxModel m = cdf.getValue();
@ -179,7 +199,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
});
notes.setPrefWidth(400);
notes.setEditable(false);
table.getColumns().addAll(preview, name, url, online, recording, paused, notes);
table.getColumns().addAll(preview, name, url, online, recording, paused, priority, notes);
table.setItems(observableModels);
table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
popup = createContextMenu();
@ -188,14 +208,6 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
}
event.consume();
});
table.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
JavaFxModel selectedModel = table.getSelectionModel().getSelectedItem();
if(selectedModel != null) {
new PlayAction(table, selectedModel).execute();
}
}
});
table.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
if (popup != null) {
popup.hide();
@ -212,6 +224,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
pauseRecording(runningModels);
}
});
scrollPane.setContent(table);
HBox addModelBox = new HBox(5);
@ -264,6 +277,26 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
restoreState();
}
private void updatePriority(CellEditEvent<JavaFxModel, Number> evt) {
try {
int prio = Optional.ofNullable(evt.getNewValue()).map(Number::intValue).orElse(-1);
if (prio < 0 || prio > 100) {
String msg = "Priority has to be between 0 and 100";
Dialogs.showError(table.getScene(), "Invalid value", msg, null);
} else {
evt.getRowValue().setPriority(prio);
try {
Config.getInstance().save();
} catch (IOException e) {
LOG.warn("Couldn't save updated priority value {} for {} - {}", evt.getNewValue(), evt.getRowValue().getName(), e.getMessage());
}
}
table.refresh();
} finally {
cellEditing = false;
}
}
private void addModel(ActionEvent e) {
String input = model.getText();
if (StringUtil.isBlank(input)) {
@ -333,55 +366,70 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
void initializeUpdateService() {
updateService = createUpdateService();
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(2)));
updateService.setOnSucceeded((event) -> {
List<JavaFxModel> models = updateService.getValue();
if (models == null) {
return;
}
lock.lock();
try {
for (JavaFxModel updatedModel : models) {
int index = observableModels.indexOf(updatedModel);
if (index == -1) {
observableModels.add(updatedModel);
updatedModel.getPausedProperty().addListener((obs, oldV, newV) -> {
if (newV.booleanValue()) {
if(!recorder.isSuspended(updatedModel)) {
pauseRecording(Collections.singletonList(updatedModel));
}
} else {
if(recorder.isSuspended(updatedModel)) {
resumeRecording(Collections.singletonList(updatedModel));
}
}
});
} else {
// make sure to update the JavaFX online property, so that the table cell is updated
JavaFxModel oldModel = observableModels.get(index);
oldModel.setSuspended(updatedModel.isSuspended());
oldModel.getOnlineProperty().set(updatedModel.getOnlineProperty().get());
oldModel.getRecordingProperty().set(updatedModel.getRecordingProperty().get());
}
}
for (Iterator<JavaFxModel> iterator = observableModels.iterator(); iterator.hasNext();) {
Model model = iterator.next();
if (!models.contains(model)) {
iterator.remove();
}
}
} finally {
lock.unlock();
}
filteredModels.clear();
filter(filter.getText());
table.sort();
});
updateService.setOnSucceeded(this::onUpdateSuccess);
updateService.setOnFailed(event -> LOG.info("Couldn't get list of models from recorder", event.getSource().getException()));
}
private void onUpdateSuccess(WorkerStateEvent event) {
if (cellEditing) {
return;
}
List<JavaFxModel> updatedModels = updateService.getValue();
if (updatedModels == null) {
return;
}
lock.lock();
try {
addOrUpdateModels(updatedModels);
// remove old ones, which are not in the list of updated models
for (Iterator<JavaFxModel> iterator = observableModels.iterator(); iterator.hasNext();) {
Model oldModel = iterator.next();
if (!updatedModels.contains(oldModel)) {
iterator.remove();
}
}
} finally {
lock.unlock();
}
filteredModels.clear();
filter(filter.getText());
table.sort();
}
private void addOrUpdateModels(List<JavaFxModel> updatedModels) {
for (JavaFxModel updatedModel : updatedModels) {
int index = observableModels.indexOf(updatedModel);
if (index == -1) {
observableModels.add(updatedModel);
updatedModel.getPausedProperty().addListener(createPauseListener(updatedModel));
} else {
// make sure to update the JavaFX online property, so that the table cell is updated
JavaFxModel oldModel = observableModels.get(index);
oldModel.setSuspended(updatedModel.isSuspended());
oldModel.getOnlineProperty().set(updatedModel.getOnlineProperty().get());
oldModel.getRecordingProperty().set(updatedModel.getRecordingProperty().get());
}
}
}
private ChangeListener<Boolean> createPauseListener(JavaFxModel updatedModel) {
return (obs, oldV, newV) -> {
if (newV.booleanValue()) {
if(!recorder.isSuspended(updatedModel)) {
pauseRecording(Collections.singletonList(updatedModel));
}
} else {
if(recorder.isSuspended(updatedModel)) {
resumeRecording(Collections.singletonList(updatedModel));
}
}
};
}
private void filter(String filter) {
lock.lock();
try {
@ -398,7 +446,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
StringBuilder sb = new StringBuilder();
for (TableColumn<JavaFxModel, ?> tc : table.getColumns()) {
Object cellData = tc.getCellData(i);
if(cellData != null) {
if (cellData != null) {
String content = cellData.toString();
sb.append(content).append(' ');
}
@ -407,14 +455,14 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
boolean tokensMissing = false;
for (String token : tokens) {
if(!searchText.toLowerCase().contains(token.toLowerCase())) {
if (!searchText.toLowerCase().contains(token.toLowerCase())) {
tokensMissing = true;
break;
}
}
if(tokensMissing) {
JavaFxModel model = table.getItems().get(i);
filteredModels.add(model);
if (tokensMissing) {
JavaFxModel filteredModel = table.getItems().get(i);
filteredModels.add(filteredModel);
}
}
observableModels.removeAll(filteredModels);
@ -424,7 +472,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
}
private ScheduledService<List<JavaFxModel>> createUpdateService() {
ScheduledService<List<JavaFxModel>> updateService = new ScheduledService<List<JavaFxModel>>() {
ScheduledService<List<JavaFxModel>> modelUpdateService = new ScheduledService<List<JavaFxModel>>() {
@Override
protected Task<List<JavaFxModel>> createTask() {
return new Task<List<JavaFxModel>>() {
@ -464,8 +512,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
t.setName("RecordedModelsTab UpdateService");
return t;
});
updateService.setExecutor(executor);
return updateService;
modelUpdateService.setExecutor(executor);
return modelUpdateService;
}
@Override
@ -489,10 +537,10 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
return null;
}
MenuItem stop = new MenuItem("Remove Model");
stop.setOnAction((e) -> stopAction(selectedModels));
stop.setOnAction(e -> stopAction(selectedModels));
MenuItem copyUrl = new MenuItem("Copy URL");
copyUrl.setOnAction((e) -> {
copyUrl.setOnAction(e -> {
Model selected = selectedModels.get(0);
final Clipboard clipboard = Clipboard.getSystemClipboard();
final ClipboardContent content = new ClipboardContent();
@ -645,4 +693,57 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
}
}
}
private class ClickableCellFactory<S, T> implements Callback<TableColumn<S, T>, TableCell<S, T>> {
@Override
public TableCell<S, T> call(TableColumn<S, T> param) {
TableCell<S, T> cell = new TableCell<>() {
@Override
protected void updateItem(Object item, boolean empty) {
setText(empty ? "" : item.toString());
}
};
cell.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
JavaFxModel selectedModel = table.getSelectionModel().getSelectedItem();
if(selectedModel != null) {
new PlayAction(table, selectedModel).execute();
}
}
});
return cell;
}
}
private class PriorityCellFactory implements Callback<TableColumn<JavaFxModel, Number>, TableCell<JavaFxModel, Number>> {
@Override
public TableCell<JavaFxModel, Number> call(TableColumn<JavaFxModel, Number> param) {
Callback<TableColumn<JavaFxModel, Number>, TableCell<JavaFxModel, Number>> callback = TextFieldTableCell
.<JavaFxModel, Number> forTableColumn((StringConverter<Number>) new NumberStringConverter());
TableCell<JavaFxModel, Number> tableCell = callback.call(param);
tableCell.setOnScroll(event -> {
if(event.isControlDown()) {
event.consume();
JavaFxModel m = tableCell.getTableRow().getItem();
int prio = m.getPriority();
if(event.getDeltaY() < 0) {
prio--;
} else {
prio++;
}
prio = Math.min(Math.max(0, prio), 100);
m.setPriority(prio);
try {
Config.getInstance().save();
} catch (IOException e) {
LOG.warn("Couldn't save updated priority value {} for {} - {}", prio, m.getName(), e.getMessage());
}
}
});
return tableCell;
}
}
}

View File

@ -23,8 +23,9 @@ public abstract class AbstractModel implements Model {
private String description;
private List<String> tags = new ArrayList<>();
private int streamUrlIndex = -1;
private int priority = 50;
private boolean suspended = false;
protected Site site;
protected transient Site site;
protected State onlineState = State.UNKNOWN;
@Override
@ -175,7 +176,7 @@ public abstract class AbstractModel implements Model {
@Override
public int compareTo(Model o) {
String thisName = Optional.ofNullable(getDisplayName()).orElse("").toLowerCase();
String otherName = Optional.ofNullable(o).map(m -> m.getDisplayName()).orElse("").toLowerCase();
String otherName = Optional.ofNullable(o).map(Model::getDisplayName).orElse("").toLowerCase();
return thisName.compareTo(otherName);
}
@ -194,6 +195,16 @@ public abstract class AbstractModel implements Model {
return site;
}
@Override
public int getPriority() {
return priority;
}
@Override
public void setPriority(int priority) {
this.priority = priority;
}
@Override
public Download createDownload() {
if(Config.isServerMode()) {

View File

@ -112,4 +112,8 @@ public interface Model extends Comparable<Model>, Serializable {
public Download createDownload();
public void setPriority(int priority);
public int getPriority();
}

View File

@ -19,7 +19,7 @@ import ctbrec.sites.chaturbate.ChaturbateModel;
public class ModelJsonAdapter extends JsonAdapter<Model> {
private static final transient Logger LOG = LoggerFactory.getLogger(ModelJsonAdapter.class);
private static final Logger LOG = LoggerFactory.getLogger(ModelJsonAdapter.class);
private List<Site> sites;
@ -38,6 +38,7 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
String url = null;
Object type = null;
int streamUrlIndex = -1;
int priority;
boolean suspended = false;
Model model = null;
@ -46,7 +47,11 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
Token token = reader.peek();
if(token == Token.NAME) {
String key = reader.nextName();
if(key.equals("name")) {
if(key.equals("type")) {
type = reader.readJsonValue();
Class<?> modelClass = Class.forName(Optional.ofNullable(type).orElse(ChaturbateModel.class.getName()).toString());
model = (Model) modelClass.getDeclaredConstructor().newInstance();
} else if(key.equals("name")) {
name = reader.nextString();
model.setName(name);
} else if(key.equals("description")) {
@ -55,10 +60,9 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
} else if(key.equals("url")) {
url = reader.nextString();
model.setUrl(url);
} else if(key.equals("type")) {
type = reader.readJsonValue();
Class<?> modelClass = Class.forName(Optional.ofNullable(type).orElse(ChaturbateModel.class.getName()).toString());
model = (Model) modelClass.getDeclaredConstructor().newInstance();
} else if(key.equals("priority")) {
priority = reader.nextInt();
model.setPriority(priority);
} else if(key.equals("streamUrlIndex")) {
streamUrlIndex = reader.nextInt();
model.setStreamUrlIndex(streamUrlIndex);
@ -69,7 +73,7 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
reader.beginObject();
try {
model.readSiteSpecificData(reader);
} catch(Exception e) {
} catch (Exception e) {
LOG.error("Couldn't read site specific data for model {}", model.getName());
throw e;
}
@ -101,6 +105,7 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
writeValueIfSet(writer, "name", model.getName());
writeValueIfSet(writer, "description", model.getDescription());
writeValueIfSet(writer, "url", model.getUrl());
writer.name("priority").value(model.getPriority());
writer.name("streamUrlIndex").value(model.getStreamUrlIndex());
writer.name("suspended").value(model.isSuspended());
writer.name("siteSpecific");

View File

@ -237,8 +237,23 @@ public class NextGenLocalRecorder implements Recorder {
}
if (!downloadSlotAvailable()) {
LOG.info("The number of downloads is maxed out, not starting recording for {}", model);
return;
long now = System.currentTimeMillis();
if ((now - lastSpaceMessage) > TimeUnit.MINUTES.toMillis(1)) {
LOG.info("The number of downloads is maxed out");
}
// check, if we can stop a recording for a model with lower priority
Optional<Recording> lowerPrioRecordingProcess = recordingProcessWithLowerPrio(model.getPriority());
if (lowerPrioRecordingProcess.isPresent()) {
Download download = lowerPrioRecordingProcess.get().getDownload();
Model lowerPrioModel = download.getModel();
LOG.info("Stopping recording for {}. Prio {} < {}", lowerPrioModel.getName(), lowerPrioModel.getPriority(), model.getPriority());
stopRecordingProcess(lowerPrioModel);
} else {
if ((now - lastSpaceMessage) > TimeUnit.MINUTES.toMillis(1)) {
LOG.info("Other models have higher prio, not starting recording for {}", model.getName());
}
return;
}
}
LOG.info("Starting recording for model {}", model.getName());
@ -276,6 +291,20 @@ public class NextGenLocalRecorder implements Recorder {
}
}
private Optional<Recording> recordingProcessWithLowerPrio(int priority) {
Model lowest = null;
for (Model m : recordingProcesses.keySet()) {
if (lowest == null || m.getPriority() < lowest.getPriority()) {
lowest = m;
}
}
if (lowest != null && lowest.getPriority() < priority) {
return Optional.of(recordingProcesses.get(lowest));
} else {
return Optional.empty();
}
}
private boolean deleteIfEmpty(Recording rec) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
rec.refresh();
long sizeInByte = rec.getSizeInByte();