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.
This commit is contained in:
XxInvictus 2023-11-23 11:45:41 +10:30
parent 2f1ef7854a
commit fe21f8d6a9
17 changed files with 305 additions and 3 deletions

View File

@ -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<Instant> lastSeenProperty() {
return lastSeenProperty;
}

View File

@ -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<Model> models, Recorder recorder) {
super(source, models, recorder, new ForcePriorityTask(recorder));
}
public CompletableFuture<List<Result>> execute() {
return super.execute("Couldn't force ignoring priority", "Force priority of {0} failed:");
}
}

View File

@ -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<Model> models, Recorder recorder) {
super(source, models, recorder, new ResumePriorityTask(recorder));
}
public CompletableFuture<List<Result>> execute() {
return super.execute("Couldn't resume respecting priority", "Resuming priority of {0} failed:");
}
}

View File

@ -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<Model> 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<Model> 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);
}
}
}

View File

@ -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<Model> 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<Model> selectedModels) {
var first = selectedModels.get(0);
var recordLater = new MenuItem("Record Later");

View File

@ -285,6 +285,20 @@ public class RecordedModelsTab extends AbstractRecordedModelsTab implements TabS
};
}
private ChangeListener<Boolean> 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<List<JavaFxModel>> createUpdateService() {
ScheduledService<List<JavaFxModel>> 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<JavaFxModel> selectedModels) {
List<Model> models = selectedModels.stream().map(JavaFxModel::getDelegate).toList();
new ForcePriorityAction(getTabPane(), models, recorder).execute();
}
private void resumePriority(List<JavaFxModel> selectedModels) {
List<Model> models = selectedModels.stream().map(JavaFxModel::getDelegate).toList();
new ResumePriorityAction(getTabPane(), models, recorder).execute();
}
private class PriorityCellFactory implements Callback<TableColumn<JavaFxModel, Number>, TableCell<JavaFxModel, Number>> {
@Override
public TableCell<JavaFxModel, Number> call(TableColumn<JavaFxModel, Number> 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;");

View File

@ -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);
}
});
}
}

View File

@ -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);
}
});
}
}

View File

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

View File

@ -123,6 +123,10 @@ public interface Model extends Comparable<Model>, Serializable {
void setSuspended(boolean suspended);
boolean isForcePriority();
void setForcePriority(boolean forcePriority);
void delay();
boolean isDelayed();

View File

@ -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;

View File

@ -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;

View File

@ -98,7 +98,7 @@ public class RecordingPreconditions {
lastPreconditionMessage = now;
}
// check, if we can stop a recording for a model with lower priority
Optional<Recording> lowerPrioRecordingProcess = recordingProcessWithLowerPrio(model.getPriority());
Optional<Recording> 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<Recording> recordingProcessWithLowerPrio(int priority) {
private Optional<Recording> 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();

View File

@ -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<Model> getOnlineModels() {
return onlineModels;

View File

@ -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<Model> 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() {

View File

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

View File

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