From fe21f8d6a960ce8c0b36dffd06c47036fb69e914 Mon Sep 17 00:00:00 2001 From: XxInvictus <3955293+XxInvictus@users.noreply.github.com> Date: Thu, 23 Nov 2023 11:45:41 +1030 Subject: [PATCH] Patching in ForcePriority changes, needs test case Patching in the first set which is ForcePriority context menu feature, allows instantly bumping a model to top of priority list no matter what their priority value is. # TODO - Needs a test case, I made an attempt but unfortunately I do not know enough to get this working so I have removed it. Doesn't help I had to do some mangling to get the current path joining to work properly with Windows-based paths and pass the current tests. - The basic test case it needs is 2 Models, Model with lower priority does not record, forcepriority to true on lower priority Model, Model with lower priority should now record, set back to false on lower priority Model, Model with lower priority should no longer record. --- .../src/main/java/ctbrec/ui/JavaFxModel.java | 11 ++++ .../ctbrec/ui/action/ForcePriorityAction.java | 20 ++++++++ .../ui/action/ResumePriorityAction.java | 20 ++++++++ .../ctbrec/ui/menu/ForcePriorityHandler.java | 51 +++++++++++++++++++ .../ctbrec/ui/menu/ModelMenuContributor.java | 19 +++++++ .../ui/tabs/recorded/RecordedModelsTab.java | 28 ++++++++++ .../ctbrec/ui/tasks/ForcePriorityTask.java | 17 +++++++ .../ctbrec/ui/tasks/ResumePriorityTask.java | 17 +++++++ .../src/main/java/ctbrec/AbstractModel.java | 7 +++ common/src/main/java/ctbrec/Model.java | 4 ++ .../java/ctbrec/io/json/dto/ModelDto.java | 1 + .../main/java/ctbrec/recorder/Recorder.java | 8 +++ .../recorder/RecordingPreconditions.java | 6 +-- .../java/ctbrec/recorder/RemoteRecorder.java | 29 +++++++++++ .../recorder/SimplifiedLocalRecorder.java | 48 +++++++++++++++++ .../io/json/mapper/ModelMapperTest.java | 4 ++ .../recorder/server/RecorderServlet.java | 18 +++++++ 17 files changed, 305 insertions(+), 3 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/action/ForcePriorityAction.java create mode 100644 client/src/main/java/ctbrec/ui/action/ResumePriorityAction.java create mode 100644 client/src/main/java/ctbrec/ui/menu/ForcePriorityHandler.java create mode 100644 client/src/main/java/ctbrec/ui/tasks/ForcePriorityTask.java create mode 100644 client/src/main/java/ctbrec/ui/tasks/ResumePriorityTask.java diff --git a/client/src/main/java/ctbrec/ui/JavaFxModel.java b/client/src/main/java/ctbrec/ui/JavaFxModel.java index c27760a6..d1db9d20 100644 --- a/client/src/main/java/ctbrec/ui/JavaFxModel.java +++ b/client/src/main/java/ctbrec/ui/JavaFxModel.java @@ -258,6 +258,17 @@ public class JavaFxModel implements Model { return delegate.getPriority(); } + + @Override + public boolean isForcePriority() { + return delegate.isForcePriority(); + } + + @Override + public void setForcePriority(boolean forcePriority) { + delegate.setForcePriority(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..1491a44d 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("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("Resume Priority"); + 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/tabs/recorded/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/tabs/recorded/RecordedModelsTab.java index 50447f62..55271422 100644 --- a/client/src/main/java/ctbrec/ui/tabs/recorded/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/tabs/recorded/RecordedModelsTab.java @@ -285,6 +285,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 +384,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) { @@ -389,6 +413,10 @@ public class RecordedModelsTab extends AbstractRecordedModelsTab implements TabS prio = Math.min(Math.max(0, prio), Model.MAX_PRIO); m.setPriority(prio); updatePriority(m, prio); + if (m.isForcePriority()) { + tableCell.setStyle("-fx-font-weight: bold;"); + tableCell.setStyle("-fx-background-color: red;"); + } } }); tableCell.setStyle("-fx-alignment: CENTER-LEFT;"); 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/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/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..e187238a 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,7 +110,7 @@ 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()) { @@ -120,7 +120,7 @@ public class RecordingPreconditions { 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 7249904d..68c0e9ec 100644 --- a/common/src/main/java/ctbrec/recorder/RemoteRecorder.java +++ b/common/src/main/java/ctbrec/recorder/RemoteRecorder.java @@ -219,6 +219,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); @@ -558,6 +563,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..20f7e503 100644 --- a/common/src/main/java/ctbrec/recorder/SimplifiedLocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/SimplifiedLocalRecorder.java @@ -578,6 +578,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 +906,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..ef3f41c8 100644 --- a/server/src/main/java/ctbrec/recorder/server/RecorderServlet.java +++ b/server/src/main/java/ctbrec/recorder/server/RecorderServlet.java @@ -227,6 +227,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());