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());