diff --git a/client/src/main/java/ctbrec/ui/Icon.java b/client/src/main/java/ctbrec/ui/Icon.java index 42051ff8..4686af09 100644 --- a/client/src/main/java/ctbrec/ui/Icon.java +++ b/client/src/main/java/ctbrec/ui/Icon.java @@ -8,7 +8,8 @@ public enum Icon { CLOCK_16(Icon.class.getResource("/16/clock.png").toExternalForm()), GROUP_16(Icon.class.getResource("/16/users.png").toExternalForm()), MEDIA_PLAYBACK_PAUSE_16(Icon.class.getResource("/16/media-playback-pause.png").toExternalForm()), - MEDIA_RECORD_16(Icon.class.getResource("/16/media-record.png").toExternalForm()); + MEDIA_RECORD_16(Icon.class.getResource("/16/media-record.png").toExternalForm()), + MEDIA_FORCE_RECORD_16(Icon.class.getResource("/16/media-force-record.png").toExternalForm()); private String url; diff --git a/client/src/main/java/ctbrec/ui/JavaFxModel.java b/client/src/main/java/ctbrec/ui/JavaFxModel.java index c27760a6..02021f35 100644 --- a/client/src/main/java/ctbrec/ui/JavaFxModel.java +++ b/client/src/main/java/ctbrec/ui/JavaFxModel.java @@ -27,6 +27,7 @@ public class JavaFxModel implements Model { private final transient BooleanProperty onlineProperty = new SimpleBooleanProperty(); private final transient BooleanProperty recordingProperty = new SimpleBooleanProperty(); private final transient BooleanProperty pausedProperty = new SimpleBooleanProperty(); + private final transient BooleanProperty forcePriorityProperty = new SimpleBooleanProperty(); private final transient SimpleIntegerProperty priorityProperty = new SimpleIntegerProperty(); private final transient SimpleObjectProperty lastSeenProperty = new SimpleObjectProperty<>(); private final transient SimpleObjectProperty lastRecordedProperty = new SimpleObjectProperty<>(); @@ -119,6 +120,10 @@ public class JavaFxModel implements Model { public BooleanProperty getPausedProperty() { return pausedProperty; } + + public BooleanProperty getForcePriorityProperty() { + return forcePriorityProperty; + } public SimpleIntegerProperty getPriorityProperty() { return priorityProperty; @@ -258,6 +263,18 @@ public class JavaFxModel implements Model { return delegate.getPriority(); } + + @Override + public boolean isForcePriority() { + return delegate.isForcePriority(); + } + + @Override + public void setForcePriority(boolean forcePriority) { + delegate.setForcePriority(forcePriority); + forcePriorityProperty.set(forcePriority); + } + public SimpleObjectProperty lastSeenProperty() { return lastSeenProperty; } diff --git a/client/src/main/java/ctbrec/ui/action/ForcePriorityAction.java b/client/src/main/java/ctbrec/ui/action/ForcePriorityAction.java new file mode 100644 index 00000000..eccdb346 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/action/ForcePriorityAction.java @@ -0,0 +1,20 @@ +package ctbrec.ui.action; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import ctbrec.Model; +import ctbrec.recorder.Recorder; +import ctbrec.ui.tasks.ForcePriorityTask; +import javafx.scene.Node; + +public class ForcePriorityAction extends AbstractModelAction { + + public ForcePriorityAction(Node source, List models, Recorder recorder) { + super(source, models, recorder, new ForcePriorityTask(recorder)); + } + + public CompletableFuture> execute() { + return super.execute("Couldn't force ignoring priority", "Force priority of {0} failed:"); + } +} \ No newline at end of file diff --git a/client/src/main/java/ctbrec/ui/action/ResumePriorityAction.java b/client/src/main/java/ctbrec/ui/action/ResumePriorityAction.java new file mode 100644 index 00000000..79a98d88 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/action/ResumePriorityAction.java @@ -0,0 +1,20 @@ +package ctbrec.ui.action; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import ctbrec.Model; +import ctbrec.recorder.Recorder; +import ctbrec.ui.tasks.ResumePriorityTask; +import javafx.scene.Node; + +public class ResumePriorityAction extends AbstractModelAction { + + public ResumePriorityAction(Node source, List models, Recorder recorder) { + super(source, models, recorder, new ResumePriorityTask(recorder)); + } + + public CompletableFuture> execute() { + return super.execute("Couldn't resume respecting priority", "Resuming priority of {0} failed:"); + } +} diff --git a/client/src/main/java/ctbrec/ui/menu/ForcePriorityHandler.java b/client/src/main/java/ctbrec/ui/menu/ForcePriorityHandler.java new file mode 100644 index 00000000..809c73dc --- /dev/null +++ b/client/src/main/java/ctbrec/ui/menu/ForcePriorityHandler.java @@ -0,0 +1,51 @@ +package ctbrec.ui.menu; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Model; +import ctbrec.recorder.Recorder; +import ctbrec.ui.action.ForcePriorityAction; +import ctbrec.ui.action.ResumePriorityAction; +import javafx.scene.Node; + +public class ForcePriorityHandler { + + private static final Logger LOG = LoggerFactory.getLogger(ForcePriorityHandler.class); + + private Node source; + private Recorder recorder; + private Runnable callback; + + public ForcePriorityHandler(Node source, Recorder recorder, Runnable callback) { + this.source = source; + this.recorder = recorder; + this.callback = callback; + } + + protected void forcePriority(List selectedModels) { + new ForcePriorityAction(source, selectedModels, recorder).execute() + .exceptionally(ex -> { + LOG.error("Error while forcing ignore priority", ex); + return null; + }).whenComplete((r, ex) -> executeCallback()); + } + + protected void resumePriority(List selectedModels) { + new ResumePriorityAction(source, selectedModels, recorder).execute() + .exceptionally(ex -> { + LOG.error("Error while resuming respecting priority", ex); + return null; + }).whenComplete((r, ex) -> executeCallback()); + } + + private void executeCallback() { + try { + callback.run(); + } catch (Exception e) { + LOG.error("Error while executing menu callback", e); + } + } +} diff --git a/client/src/main/java/ctbrec/ui/menu/ModelMenuContributor.java b/client/src/main/java/ctbrec/ui/menu/ModelMenuContributor.java index 7fbdad24..73bb762c 100644 --- a/client/src/main/java/ctbrec/ui/menu/ModelMenuContributor.java +++ b/client/src/main/java/ctbrec/ui/menu/ModelMenuContributor.java @@ -101,6 +101,7 @@ public class ModelMenuContributor { addStartPaused(menu, selectedModels); addRecordLater(menu, selectedModels); addPauseResume(menu, selectedModels); + addForceRecord(menu, selectedModels); addGroupMenu(menu, selectedModels); menu.getItems().add(new SeparatorMenuItem()); @@ -272,6 +273,24 @@ public class ModelMenuContributor { } } + private void addForceRecord(ContextMenu menu, List selectedModels) { + var forcePriority = new MenuItem("Enable Force Recording"); + forcePriority.setOnAction(e -> { + for (Model model : selectedModels) { + model.setMarkedForLaterRecording(false); + model.setSuspended(false); + } + if (!recorder.isTracked(selectedModels.get(0))) { + startStopAction(selectedModels, true); + } + new ForcePriorityHandler(source, recorder, callback).forcePriority(selectedModels); + }); + var resumePriority = new MenuItem("Disable Force Recording"); + resumePriority.setOnAction(e -> new ForcePriorityHandler(source, recorder, callback).resumePriority(selectedModels)); + var forceResumePriority = recorder.isForcePriority(selectedModels.get(0)) ? resumePriority : forcePriority; + menu.getItems().add(forceResumePriority); + } + private void addRecordLater(ContextMenu menu, List selectedModels) { var first = selectedModels.get(0); var recordLater = new MenuItem("Record Later"); diff --git a/client/src/main/java/ctbrec/ui/settings/IgnoreList.java b/client/src/main/java/ctbrec/ui/settings/IgnoreList.java index 1b34ea03..e8445c26 100644 --- a/client/src/main/java/ctbrec/ui/settings/IgnoreList.java +++ b/client/src/main/java/ctbrec/ui/settings/IgnoreList.java @@ -44,10 +44,6 @@ public class IgnoreList extends GridPane { setVgap(10); setPadding(new Insets(20, 10, 10, 10)); - var headline = new Label("Ignore List"); - headline.getStyleClass().add("settings-group-label"); - add(headline, 0, 0); - ignoreListView = new ListView<>(); ignoreListView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); ignoreListView.addEventHandler(KeyEvent.KEY_PRESSED, event -> { @@ -55,7 +51,7 @@ public class IgnoreList extends GridPane { removeSelectedModels(); } }); - add(ignoreListView, 0, 1); + add(ignoreListView, 0, 0); GridPane.setHgrow(ignoreListView, Priority.ALWAYS); var remove = new Button("Remove"); diff --git a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java index 93634b6e..966bc280 100644 --- a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java @@ -129,6 +129,8 @@ public class SettingsTab extends Tab implements TabSelectionListener { private final VariablePlayGroundDialogFactory variablePlayGroundDialogFactory = new VariablePlayGroundDialogFactory(); private SimpleBooleanProperty checkForUpdates; private PostProcessingStepPanel postProcessingStepPanel; + private SimpleStringProperty filterBlacklist; + private SimpleStringProperty filterWhitelist; public SettingsTab(List sites, Recorder recorder) { this.sites = sites; @@ -207,6 +209,8 @@ public class SettingsTab extends Tab implements TabSelectionListener { dateTimeFormat = new SimpleStringProperty(null, "dateTimeFormat", settings.dateTimeFormat); tabsSortable = new SimpleBooleanProperty(null, "tabsSortable", settings.tabsSortable); checkForUpdates = new SimpleBooleanProperty(null, "checkForUpdates", settings.checkForUpdates); + filterBlacklist = new SimpleStringProperty(null, "filterBlacklist", settings.filterBlacklist); + filterWhitelist = new SimpleStringProperty(null, "filterWhitelist", settings.filterWhitelist); } private void createGui() { @@ -297,8 +301,13 @@ public class SettingsTab extends Tab implements TabSelectionListener { Setting.of("Steps", postProcessingStepPanel), Setting.of("", createHelpButton("Post-Processing Help", "http://localhost:5689/docs/PostProcessing.md")), Setting.of("", createVariablePlayGroundButton()))), - Category.of("Events & Actions", new ActionSettingsPanel(recorder)), Category.of("Ignore List", ignoreList), - Category.of("Sites", siteCategories.toArray(new Category[0])), + Category.of("Events & Actions", new ActionSettingsPanel(recorder)), + Category.of("Filtering", + Group.of("Ignore List", + Setting.of("", ignoreList)), + Group.of("Text Filters", + Setting.of("Blacklist", filterBlacklist, "Default list of blacklist filters for site views, space seperated"), + Setting.of("Whitelist", filterWhitelist, "Default list of whitelist filters for site views, space seperated"))), Category.of("Sites", siteCategories.toArray(new Category[0])), Category.of("Proxy", Group.of("Proxy", Setting.of("Type", proxyType).needsRestart(), diff --git a/client/src/main/java/ctbrec/ui/tabs/ThumbCell.java b/client/src/main/java/ctbrec/ui/tabs/ThumbCell.java index 39b4b5f9..2a28487c 100644 --- a/client/src/main/java/ctbrec/ui/tabs/ThumbCell.java +++ b/client/src/main/java/ctbrec/ui/tabs/ThumbCell.java @@ -73,6 +73,7 @@ public class ThumbCell extends StackPane { private static final Duration ANIMATION_DURATION = new Duration(250); private static final Image imgRecordIndicator = new Image(MEDIA_RECORD_16.url()); + private static final Image imgForceRecordIndicator = new Image(MEDIA_FORCE_RECORD_16.url()); private static final Image imgPauseIndicator = new Image(MEDIA_PLAYBACK_PAUSE_16.url()); private static final Image imgBookmarkIndicator = new Image(BOOKMARK_16.url()); private static final Image imgGroupIndicator = new Image(Icon.GROUP_16.url()); @@ -122,6 +123,7 @@ public class ThumbCell extends StackPane { this.imgAspectRatio = aspectRatio; recording = recorder.isTracked(model); model.setSuspended(recorder.isSuspended(model)); + model.setForcePriority(recorder.isForcePriority(model)); this.setStyle("-fx-background-color: -fx-base"); streamPreview = new StreamPreview(); @@ -497,7 +499,11 @@ public class ThumbCell extends StackPane { recordingIndicatorTooltip.setText("Resume Recording"); } else { modelRecordingState = ModelRecordingState.RECORDING; - recordingIndicator.setImage(imgRecordIndicator); + if (model.isForcePriority()) { + recordingIndicator.setImage(imgForceRecordIndicator); + } else { + recordingIndicator.setImage(imgRecordIndicator); + } recordingIndicatorTooltip.setText("Pause Recording"); } } else { diff --git a/client/src/main/java/ctbrec/ui/tabs/ThumbOverviewTab.java b/client/src/main/java/ctbrec/ui/tabs/ThumbOverviewTab.java index ff33a530..a8953c29 100644 --- a/client/src/main/java/ctbrec/ui/tabs/ThumbOverviewTab.java +++ b/client/src/main/java/ctbrec/ui/tabs/ThumbOverviewTab.java @@ -317,13 +317,29 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { if (updatesSuspended) { return; } - List models = filterIgnoredModels(updateService.getValue()); + List models = filterModels(updateService.getValue()); updateGrid(models); } - private List filterIgnoredModels(List models) { + private List filterModels(List models) { List ignored = Config.getInstance().getSettings().ignoredModels; - return models.stream().filter(m -> !ignored.contains(m.getUrl())).collect(Collectors.toList()); + String filterBlacklist = Config.getInstance().getSettings().filterBlacklist; + String filterWhitelist = Config.getInstance().getSettings().filterWhitelist; + if (filterBlacklist.isBlank() && filterWhitelist.isBlank()) { + return models.stream() + .filter(m -> !ignored.contains(m.getUrl())) + .collect(Collectors.toList()); + } else if (filterBlacklist.isBlank()) { + return models.stream() + .filter(m -> !ignored.contains(m.getUrl())) + .filter(m -> matches(m, filterWhitelist, true)) + .collect(Collectors.toList()); + } + return models.stream() + .filter(m -> !ignored.contains(m.getUrl())) + .filter(m -> !matches(m, filterBlacklist, true)) + .filter(m -> matches(m, filterWhitelist, true)) + .collect(Collectors.toList()); } protected void updateGrid(List models) { @@ -633,7 +649,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { var node = iterator.next(); if (node instanceof ThumbCell cell) { var m = cell.getModel(); - if (!matches(m, filter)) { + if (!matches(m, filter, false)) { iterator.remove(); filteredThumbCells.add(cell); cell.setSelected(false); @@ -645,7 +661,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { for (Iterator iterator = filteredThumbCells.iterator(); iterator.hasNext(); ) { var thumbCell = iterator.next(); var m = thumbCell.getModel(); - if (matches(m, filter)) { + if (matches(m, filter, false)) { iterator.remove(); insert(thumbCell); } @@ -687,12 +703,14 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { } } - private boolean matches(Model m, String filter) { + private boolean matches(Model m, String filter, Boolean anyMatch) { try { String[] tokens = filter.split(" "); var tokensMissing = false; for (String token : tokens) { - if (!modelPropertiesMatchToken(token, m)) { + if (anyMatch && modelPropertiesMatchToken(token, m)) { + return true; + } else if (!modelPropertiesMatchToken(token, m)) { tokensMissing = true; } } diff --git a/client/src/main/java/ctbrec/ui/tabs/recorded/ModelNameTableCell.java b/client/src/main/java/ctbrec/ui/tabs/recorded/ModelNameTableCell.java index 59122b4b..b0c46a4e 100644 --- a/client/src/main/java/ctbrec/ui/tabs/recorded/ModelNameTableCell.java +++ b/client/src/main/java/ctbrec/ui/tabs/recorded/ModelNameTableCell.java @@ -36,6 +36,9 @@ public class ModelNameTableCell extends IconTableCell { tooltip = group.getModelUrls().size() + " models:\n"; tooltip += group.getModelUrls().stream().collect(Collectors.joining("\n")); }); + if (m.isForcePriority()) { + this.setStyle(getStyle() + "-fx-text-fill: darkred;" + "-fx-font-weight: bold;"); + } } super.updateItem(modelName, empty); } diff --git a/client/src/main/java/ctbrec/ui/tabs/recorded/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/tabs/recorded/RecordedModelsTab.java index 50447f62..5613ee2b 100644 --- a/client/src/main/java/ctbrec/ui/tabs/recorded/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/tabs/recorded/RecordedModelsTab.java @@ -257,10 +257,12 @@ public class RecordedModelsTab extends AbstractRecordedModelsTab implements TabS if (index == -1) { observableModels.add(updatedModel); updatedModel.getPausedProperty().addListener(createPauseListener(updatedModel)); + updatedModel.getForcePriorityProperty().addListener(createForcePriorityListener(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.setForcePriority(updatedModel.isForcePriority()); oldModel.getOnlineProperty().set(updatedModel.getOnlineProperty().get()); oldModel.getRecordingProperty().set(updatedModel.getRecordingProperty().get()); oldModel.lastRecordedProperty().set(updatedModel.lastRecordedProperty().get()); @@ -285,6 +287,20 @@ public class RecordedModelsTab extends AbstractRecordedModelsTab implements TabS }; } + private ChangeListener createForcePriorityListener(JavaFxModel updatedModel) { + return (obs, oldV, newV) -> { + if (Boolean.TRUE.equals(newV)) { + if (!recorder.isForcePriority(updatedModel)) { + forcePriority(Collections.singletonList(updatedModel)); + } + } else { + if (recorder.isForcePriority(updatedModel)) { + resumePriority(Collections.singletonList(updatedModel)); + } + } + }; + } + private ScheduledService> createUpdateService() { ScheduledService> modelUpdateService = new ScheduledService<>() { @Override @@ -370,6 +386,16 @@ public class RecordedModelsTab extends AbstractRecordedModelsTab implements TabS new ResumeAction(getTabPane(), models, recorder).execute(); } + private void forcePriority(List selectedModels) { + List models = selectedModels.stream().map(JavaFxModel::getDelegate).toList(); + new ForcePriorityAction(getTabPane(), models, recorder).execute(); + } + + private void resumePriority(List selectedModels) { + List models = selectedModels.stream().map(JavaFxModel::getDelegate).toList(); + new ResumePriorityAction(getTabPane(), models, recorder).execute(); + } + private class PriorityCellFactory implements Callback, TableCell> { @Override public TableCell call(TableColumn param) { diff --git a/client/src/main/java/ctbrec/ui/tasks/ForcePriorityTask.java b/client/src/main/java/ctbrec/ui/tasks/ForcePriorityTask.java new file mode 100644 index 00000000..875ac8c6 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/tasks/ForcePriorityTask.java @@ -0,0 +1,17 @@ +package ctbrec.ui.tasks; + +import ctbrec.recorder.Recorder; + +public class ForcePriorityTask extends AbstractModelTask { + + public ForcePriorityTask(Recorder recorder) { + super(recorder, model -> { + try { + model.setForcePriority(true); + recorder.forcePriorityRecording(model); + } catch (Exception e) { + throw new TaskExecutionException(e); + } + }); + } +} diff --git a/client/src/main/java/ctbrec/ui/tasks/ResumePriorityTask.java b/client/src/main/java/ctbrec/ui/tasks/ResumePriorityTask.java new file mode 100644 index 00000000..7f6e242b --- /dev/null +++ b/client/src/main/java/ctbrec/ui/tasks/ResumePriorityTask.java @@ -0,0 +1,17 @@ +package ctbrec.ui.tasks; + +import ctbrec.recorder.Recorder; + +public class ResumePriorityTask extends AbstractModelTask { + + public ResumePriorityTask(Recorder recorder) { + super(recorder, model -> { + try { + model.setForcePriority(false); + recorder.resumePriorityRecording(model); + } catch (Exception e) { + throw new TaskExecutionException(e); + } + }); + } +} diff --git a/client/src/main/resources/16/media-force-record.png b/client/src/main/resources/16/media-force-record.png new file mode 100644 index 00000000..d73004e2 Binary files /dev/null and b/client/src/main/resources/16/media-force-record.png differ diff --git a/common/src/main/java/ctbrec/AbstractModel.java b/common/src/main/java/ctbrec/AbstractModel.java index b7fe90e0..26f36b2c 100644 --- a/common/src/main/java/ctbrec/AbstractModel.java +++ b/common/src/main/java/ctbrec/AbstractModel.java @@ -30,6 +30,7 @@ public abstract class AbstractModel implements Model { private int streamUrlIndex = -1; private int priority = new Settings().defaultPriority; private boolean suspended = false; + private boolean forcePriority = false; private boolean markedForLaterRecording = false; protected transient Site site; protected State onlineState = State.UNKNOWN; @@ -145,6 +146,12 @@ public abstract class AbstractModel implements Model { this.suspended = suspended; } + @Override + public boolean isForcePriority() { return forcePriority; } + + @Override + public void setForcePriority(boolean forcePriority) { this.forcePriority = forcePriority; } + @Override public void delay() { this.delayUntil = Instant.now().plusSeconds(120); diff --git a/common/src/main/java/ctbrec/Model.java b/common/src/main/java/ctbrec/Model.java index 5cf55025..4563bf08 100644 --- a/common/src/main/java/ctbrec/Model.java +++ b/common/src/main/java/ctbrec/Model.java @@ -123,6 +123,10 @@ public interface Model extends Comparable, Serializable { void setSuspended(boolean suspended); + boolean isForcePriority(); + + void setForcePriority(boolean forcePriority); + void delay(); boolean isDelayed(); diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index f6117682..d2194480 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -216,4 +216,6 @@ public class Settings { public boolean checkForUpdates = true; public int thumbCacheSize = 16; public boolean dreamcamVR = false; + public String filterBlacklist = ""; + public String filterWhitelist = ""; } diff --git a/common/src/main/java/ctbrec/io/json/dto/ModelDto.java b/common/src/main/java/ctbrec/io/json/dto/ModelDto.java index 29f5d3e2..d45ecfe0 100644 --- a/common/src/main/java/ctbrec/io/json/dto/ModelDto.java +++ b/common/src/main/java/ctbrec/io/json/dto/ModelDto.java @@ -24,6 +24,7 @@ public class ModelDto { private int streamUrlIndex = -1; private boolean suspended = false; private boolean bookmarked = false; + private boolean forcePriority = false; @JsonSerialize(converter = InstantToMillisConverter.class) @JsonDeserialize(converter = MillisToInstantConverter.class) private Instant lastSeen; diff --git a/common/src/main/java/ctbrec/recorder/Recorder.java b/common/src/main/java/ctbrec/recorder/Recorder.java index e9bc6228..dfe9b3e4 100644 --- a/common/src/main/java/ctbrec/recorder/Recorder.java +++ b/common/src/main/java/ctbrec/recorder/Recorder.java @@ -67,6 +67,14 @@ public interface Recorder { boolean isSuspended(Model model); + /** + * Returns true, if a model is in the list of models to ignore priorities and immediately record. + */ + public boolean isForcePriority(Model model); + + public void forcePriorityRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException; + public void resumePriorityRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException; + boolean isMarkedForLaterRecording(Model model); void markForLaterRecording(Model model, boolean mark) throws InvalidKeyException, NoSuchAlgorithmException, IOException; diff --git a/common/src/main/java/ctbrec/recorder/RecordingPreconditions.java b/common/src/main/java/ctbrec/recorder/RecordingPreconditions.java index bb372b29..a681ade3 100644 --- a/common/src/main/java/ctbrec/recorder/RecordingPreconditions.java +++ b/common/src/main/java/ctbrec/recorder/RecordingPreconditions.java @@ -98,7 +98,7 @@ public class RecordingPreconditions { lastPreconditionMessage = now; } // check, if we can stop a recording for a model with lower priority - Optional lowerPrioRecordingProcess = recordingProcessWithLowerPrio(model.getPriority()); + Optional lowerPrioRecordingProcess = recordingProcessWithLowerPrio(model.getPriority(), model.isForcePriority()); if (lowerPrioRecordingProcess.isPresent()) { RecordingProcess download = lowerPrioRecordingProcess.get().getRecordingProcess(); Model lowerPrioModel = download.getModel(); @@ -110,17 +110,17 @@ public class RecordingPreconditions { } } - private Optional recordingProcessWithLowerPrio(int priority) { + private Optional recordingProcessWithLowerPrio(int priority, boolean isForced) { Recording lowest = null; int lowestPrio = Integer.MAX_VALUE; for (Recording rec : recorder.getRecordingProcesses()) { Model m = rec.getModel(); - if (m.getPriority() < lowestPrio) { + if (m.getPriority() < lowestPrio && !m.isForcePriority()) { lowest = rec; lowestPrio = m.getPriority(); } } - if (lowestPrio < priority) { + if (isForced || (lowestPrio < priority)) { return Optional.of(lowest); } else { return Optional.empty(); diff --git a/common/src/main/java/ctbrec/recorder/RemoteRecorder.java b/common/src/main/java/ctbrec/recorder/RemoteRecorder.java index cb26d08c..8c7773f9 100644 --- a/common/src/main/java/ctbrec/recorder/RemoteRecorder.java +++ b/common/src/main/java/ctbrec/recorder/RemoteRecorder.java @@ -86,6 +86,7 @@ public class RemoteRecorder implements Recorder { sendRequest("start", model); findModel(model).ifPresent(cachedModel -> { cachedModel.setSuspended(model.isSuspended()); + cachedModel.setForcePriority(model.isForcePriority()); cachedModel.setMarkedForLaterRecording(model.isMarkedForLaterRecording()); cachedModel.setRecordUntil(model.getRecordUntil()); cachedModel.setRecordUntilSubsequentAction(model.getRecordUntilSubsequentAction()); @@ -219,6 +220,11 @@ public class RemoteRecorder implements Recorder { return findModel(model).map(Model::isSuspended).orElse(false); } + @Override + public boolean isForcePriority(Model model) { + return findModel(model).map(Model::isForcePriority).orElse(false); + } + @Override public boolean isMarkedForLaterRecording(Model model) { return findModel(model).map(Model::isMarkedForLaterRecording).orElse(false); @@ -565,6 +571,30 @@ public class RemoteRecorder implements Recorder { } } + @Override + public void forcePriorityRecording(Model model) throws InvalidKeyException, NoSuchAlgorithmException, IOException { + sendRequest("forcePriority", model); + model.setForcePriority(true); + // update cached model + int index = models.indexOf(model); + if (index >= 0) { + Model m = models.get(index); + m.setForcePriority(true); + } + } + + @Override + public void resumePriorityRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException { + sendRequest("resumePriority", model); + model.setForcePriority(false); + // update cached model + int index = models.indexOf(model); + if (index >= 0) { + Model m = models.get(index); + m.setForcePriority(false); + } + } + @Override public List getOnlineModels() { return onlineModels; diff --git a/common/src/main/java/ctbrec/recorder/SimplifiedLocalRecorder.java b/common/src/main/java/ctbrec/recorder/SimplifiedLocalRecorder.java index a57c9973..52e80cfc 100644 --- a/common/src/main/java/ctbrec/recorder/SimplifiedLocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/SimplifiedLocalRecorder.java @@ -345,6 +345,7 @@ public class SimplifiedLocalRecorder implements Recorder { private void copyModelProperties(Model src, Model existing) { existing.setSuspended(src.isSuspended()); + existing.setForcePriority(src.isForcePriority()); existing.setMarkedForLaterRecording(src.isMarkedForLaterRecording()); existing.setPriority(src.getPriority()); existing.setRecordUntil(src.getRecordUntil()); @@ -578,6 +579,49 @@ public class SimplifiedLocalRecorder implements Recorder { } } + @Override + public void forcePriorityRecording(Model model) throws IOException { + recorderLock.lock(); + try { + if (models.contains(model)) { + int index = models.indexOf(model); + Model m = models.get(index); + m.setForcePriority(true); + m.setMarkedForLaterRecording(false); + model.setForcePriority(true); + model.setMarkedForLaterRecording(false); + saveConfig(); + startRecordingProcess(m); + } else { + log.warn("Couldn't force ignore priority for model {}. Not found in list", model.getName()); + } + } finally { + recorderLock.unlock(); + } + } + + @Override + public void resumePriorityRecording(Model model) { + recorderLock.lock(); + try { + if (models.contains(model)) { + int index = models.indexOf(model); + models.get(index).setForcePriority(false); + model.setForcePriority(false); + saveConfig(); + } else { + log.warn("Couldn't resume respecting priority for model {}. Not found in list", model.getName()); + return; + } + + getRecordingProcessForModel(model).ifPresent(this::stopRecordingProcess); + } catch (IOException e) { + errorSavingConfig(e); + } finally { + recorderLock.unlock(); + } + } + @Override public boolean isTracked(Model model) { Optional m = findModel(model); @@ -863,6 +907,11 @@ public class SimplifiedLocalRecorder implements Recorder { log.info("Resuming recorder"); running = true; } + + @Override + public boolean isForcePriority(Model model) { + return findModel(model).map(Model::isForcePriority).orElse(false); + } @Override public int getModelCount() { diff --git a/common/src/test/java/ctbrec/io/json/mapper/ModelMapperTest.java b/common/src/test/java/ctbrec/io/json/mapper/ModelMapperTest.java index 8861cf9c..e1b61ae9 100644 --- a/common/src/test/java/ctbrec/io/json/mapper/ModelMapperTest.java +++ b/common/src/test/java/ctbrec/io/json/mapper/ModelMapperTest.java @@ -25,6 +25,7 @@ class ModelMapperTest { model.setLastSeen(Instant.now().minusSeconds(60)); model.setPriority(51); model.setSuspended(true); + model.setForcePriority(false); model.setMarkedForLaterRecording(true); model.setRecordUntilSubsequentAction(SubsequentAction.REMOVE); model.setDisplayName("whatever"); @@ -45,6 +46,7 @@ class ModelMapperTest { assertEquals(model.getPreview(), mapped.getPreview().toString()); assertEquals(model.isMarkedForLaterRecording(), mapped.isBookmarked()); assertEquals(model.isSuspended(), mapped.isSuspended()); + assertEquals(model.isForcePriority(), mapped.isForcePriority()); } @Test @@ -61,6 +63,7 @@ class ModelMapperTest { dto.setLastSeen(Instant.now().minusSeconds(60)); dto.setPriority(51); dto.setSuspended(true); + dto.setForcePriority(false); dto.setBookmarked(true); dto.setRecordUntilSubsequentAction(SubsequentAction.REMOVE); dto.setDisplayName("whatever"); @@ -81,5 +84,6 @@ class ModelMapperTest { assertEquals(dto.getPreview().toString(), mapped.getPreview()); assertEquals(dto.isBookmarked(), mapped.isMarkedForLaterRecording()); assertEquals(dto.isSuspended(), mapped.isSuspended()); + assertEquals(dto.isForcePriority(), mapped.isForcePriority()); } } diff --git a/server/src/main/java/ctbrec/recorder/server/RecorderServlet.java b/server/src/main/java/ctbrec/recorder/server/RecorderServlet.java index 891bd87e..55bd7fb5 100644 --- a/server/src/main/java/ctbrec/recorder/server/RecorderServlet.java +++ b/server/src/main/java/ctbrec/recorder/server/RecorderServlet.java @@ -87,6 +87,7 @@ public class RecorderServlet extends AbstractCtbrecServlet { log.debug("Starting recording for model {} - {}", model.getName(), model.getUrl()); log.trace("Model marked: {}", model.isMarkedForLaterRecording()); log.trace("Model paused: {}", model.isSuspended()); + log.trace("Model forced: {}", model.isForcePriority()); log.trace("Model until: {}", model.getRecordUntil().equals(Instant.ofEpochMilli(Model.RECORD_INDEFINITELY)) ? "no limit" : model.getRecordUntil()); log.trace("Model after: {}", model.getRecordUntilSubsequentAction()); recorder.addModel(model); @@ -227,6 +228,24 @@ public class RecorderServlet extends AbstractCtbrecServlet { response = "{\"status\": \"success\"}"; responseWriter.write(response); break; + case "forcePriority": + log.debug("Force ignore priority for model {} - {}", model.getName(), model.getUrl()); + recorder.forcePriorityRecording(model); + response = "{\"status\": \"success\", \"msg\": \"Forcing ignore priority\"}"; + responseWriter.write(response); + break; + case "resumePriority": + log.debug("Resume respecting priority for model {} - {}", model.getName(), model.getUrl()); + GlobalThreadPool.submit(() -> { + try { + recorder.resumePriorityRecording(model); + } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) { + log.error("Couldn't resume respecting priority for model {}", model, e); + } + }); + response = "{\"status\": \"success\", \"msg\": \"Resuming respecting priority\"}"; + responseWriter.write(response); + break; case "saveModelGroup": recorder.saveModelGroup(request.getModelGroup()); sendModelGroups(resp, recorder.getModelGroups());