From 4d6e74562cc5fb756ce0b0321b059560020f2571 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 3 Jan 2020 19:06:05 +0100 Subject: [PATCH] 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 --- CHANGELOG.md | 11 + .../src/main/java/ctbrec/ui/JavaFxModel.java | 18 ++ .../java/ctbrec/ui/RecordedModelsTab.java | 231 +++++++++++++----- .../src/main/java/ctbrec/AbstractModel.java | 15 +- common/src/main/java/ctbrec/Model.java | 4 + .../main/java/ctbrec/io/ModelJsonAdapter.java | 19 +- .../ctbrec/recorder/NextGenLocalRecorder.java | 33 ++- 7 files changed, 255 insertions(+), 76 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce0af65c..4572a606 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/client/src/main/java/ctbrec/ui/JavaFxModel.java b/client/src/main/java/ctbrec/ui/JavaFxModel.java index c492164a..6fbfb52c 100644 --- a/client/src/main/java/ctbrec/ui/JavaFxModel.java +++ b/client/src/main/java/ctbrec/ui/JavaFxModel.java @@ -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); diff --git a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java index 09565885..8c5f7160 100644 --- a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java @@ -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> updateService; private Recorder recorder; private List 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 name = new TableColumn<>("Model"); name.setPrefWidth(200); name.setCellValueFactory(new PropertyValueFactory("displayName")); + name.setCellFactory(new ClickableCellFactory<>()); name.setEditable(false); TableColumn url = new TableColumn<>("URL"); url.setCellValueFactory(new PropertyValueFactory("url")); + url.setCellFactory(new ClickableCellFactory<>()); url.setPrefWidth(400); url.setEditable(false); TableColumn 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 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 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 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 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 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 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 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 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 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 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> createUpdateService() { - ScheduledService> updateService = new ScheduledService>() { + ScheduledService> modelUpdateService = new ScheduledService>() { @Override protected Task> createTask() { return new Task>() { @@ -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 implements Callback, TableCell> { + @Override + public TableCell call(TableColumn param) { + TableCell 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, TableCell> { + @Override + public TableCell call(TableColumn param) { + Callback, TableCell> callback = TextFieldTableCell + . forTableColumn((StringConverter) new NumberStringConverter()); + TableCell 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; + } + } } diff --git a/common/src/main/java/ctbrec/AbstractModel.java b/common/src/main/java/ctbrec/AbstractModel.java index 0c46958d..67658ff5 100644 --- a/common/src/main/java/ctbrec/AbstractModel.java +++ b/common/src/main/java/ctbrec/AbstractModel.java @@ -23,8 +23,9 @@ public abstract class AbstractModel implements Model { private String description; private List 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()) { diff --git a/common/src/main/java/ctbrec/Model.java b/common/src/main/java/ctbrec/Model.java index 9f0d5850..c9f6b587 100644 --- a/common/src/main/java/ctbrec/Model.java +++ b/common/src/main/java/ctbrec/Model.java @@ -112,4 +112,8 @@ public interface Model extends Comparable, Serializable { public Download createDownload(); + public void setPriority(int priority); + + public int getPriority(); + } \ No newline at end of file diff --git a/common/src/main/java/ctbrec/io/ModelJsonAdapter.java b/common/src/main/java/ctbrec/io/ModelJsonAdapter.java index 3414712d..f408c906 100644 --- a/common/src/main/java/ctbrec/io/ModelJsonAdapter.java +++ b/common/src/main/java/ctbrec/io/ModelJsonAdapter.java @@ -19,7 +19,7 @@ import ctbrec.sites.chaturbate.ChaturbateModel; public class ModelJsonAdapter extends JsonAdapter { - private static final transient Logger LOG = LoggerFactory.getLogger(ModelJsonAdapter.class); + private static final Logger LOG = LoggerFactory.getLogger(ModelJsonAdapter.class); private List sites; @@ -38,6 +38,7 @@ public class ModelJsonAdapter extends JsonAdapter { 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 { 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 { } 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 { 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 { 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"); diff --git a/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java b/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java index 8264e401..02ca0075 100644 --- a/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java @@ -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 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 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();