Rewrite of recording related model actions

This commit is contained in:
0xb00bface 2021-09-03 13:58:07 +02:00
parent e5468f6849
commit f216b8240b
28 changed files with 635 additions and 193 deletions

View File

@ -0,0 +1,56 @@
package ctbrec.ui.action;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import ctbrec.ui.tasks.TaskExecutionException;
import javafx.application.Platform;
public abstract class AbstractAction<P, R> {
private R result;
public CompletableFuture<Optional<R>> execute(P param) {
try {
result = doExecute(param);
Platform.runLater(() -> onSuccess(Optional.ofNullable(result)));
return CompletableFuture.completedFuture(Optional.of(result));
} catch (Exception e) {
Platform.runLater(() -> onError(e));
return CompletableFuture.failedFuture(e);
} finally {
Platform.runLater(() -> done(Optional.ofNullable(result)));
}
}
protected abstract R doExecute(P param) throws InvalidKeyException, NoSuchAlgorithmException, IOException;
public CompletableFuture<Optional<R>> executeAsync(P param) {
return CompletableFuture.supplyAsync(() -> {
try {
result = doExecute(param);
Platform.runLater(() -> onSuccess(Optional.ofNullable(result)));
return Optional.of(result);
} catch (Exception e) {
Platform.runLater(() -> onError(e));
throw new TaskExecutionException(e);
} finally {
Platform.runLater(() -> done(Optional.ofNullable(result)));
}
});
}
@SuppressWarnings("unchecked")
public <T extends AbstractAction<P, R>> T beforeOnGuiThread(Runnable r) {
return (T) this;
}
protected void onSuccess(Optional<R> result) {}
protected void onError(Exception e) {}
protected void done(Optional<R> result) {}
}

View File

@ -0,0 +1,80 @@
package ctbrec.ui.action;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import ctbrec.GlobalThreadPool;
import ctbrec.Model;
import ctbrec.recorder.Recorder;
import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.tasks.AbstractModelTask;
import javafx.application.Platform;
import javafx.scene.Cursor;
import javafx.scene.Node;
public abstract class AbstractModelAction {
protected Node source;
protected List<? extends Model> models;
protected Recorder recorder;
private AbstractModelTask task;
protected AbstractModelAction(Node source, List<? extends Model> models, Recorder recorder, AbstractModelTask task) {
this.source = source;
this.models = models;
this.recorder = recorder;
this.task = task;
}
protected CompletableFuture<List<Result>> execute(String errorHeader, String errorMsg) {
source.setCursor(Cursor.WAIT);
return CompletableFuture.supplyAsync(() -> {
final List<Result> result = new ArrayList<>(models.size());
final List<CompletableFuture<Model>> futures = new ArrayList<>(models.size());
for (Model model : models) {
futures.add(task
.executeSync(model)
.whenComplete((mdl, ex) ->
result.add(new Result(model, ex))));
}
Platform.runLater(() -> source.setCursor(Cursor.DEFAULT));
List<Result> failed = result.stream().filter(Result::failed).collect(Collectors.toList());
if (!failed.isEmpty()) {
Throwable t = failed.get(0).getThrowable();
String failedModelList = failed.stream().map(Result::getModel).map(Model::getDisplayName).collect(Collectors.joining(", "));
String msg = MessageFormat.format(errorMsg, failedModelList);
Dialogs.showError(source.getScene(), errorHeader, msg, t);
}
return result;
}, GlobalThreadPool.get());
}
public static class Result {
private Model model;
private Throwable throwable;
public Result(Model model, Throwable t) {
this.model = model;
this.throwable = t;
}
public boolean successful() {
return throwable == null;
}
public boolean failed() {
return throwable != null;
}
public Model getModel() {
return model;
}
public Throwable getThrowable() {
return throwable;
}
}
}

View File

@ -1,30 +1,20 @@
package ctbrec.ui.action;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.CompletableFuture;
import ctbrec.Model;
import ctbrec.ui.controls.Dialogs;
import javafx.application.Platform;
import ctbrec.recorder.Recorder;
import ctbrec.ui.tasks.FollowTask;
import javafx.scene.Node;
public class FollowAction extends ModelMassEditAction {
public class FollowAction extends AbstractModelAction {
private static final Logger LOG = LoggerFactory.getLogger(FollowAction.class);
public FollowAction(Node source, List<? extends Model> models, Recorder recorder) {
super(source, models, recorder, new FollowTask(recorder));
}
public FollowAction(Node source, List<? extends Model> models) {
super(source, models);
action = m -> {
try {
m.getSite().login();
m.follow();
} catch(Exception e) {
LOG.error("Couldn't follow model {}", m, e);
Platform.runLater(() ->
Dialogs.showError(source.getScene(), "Couldn't follow model", "Following " + m.getName() + " failed: " + e.getMessage(), e));
}
};
public CompletableFuture<List<Result>> execute() {
return super.execute("Couldn't follow model", "Following of {0} failed:");
}
}

View File

@ -7,6 +7,7 @@ import ctbrec.Config;
import ctbrec.Model;
import ctbrec.recorder.Recorder;
import ctbrec.ui.JavaFxModel;
import ctbrec.ui.action.AbstractModelAction.Result;
import ctbrec.ui.controls.Dialogs;
import javafx.scene.Node;
@ -44,7 +45,8 @@ public class IgnoreModelsAction {
if (withRemoveDialog) {
boolean removeAsWell = Dialogs.showConfirmDialog("Ignore Model", null, "Remove as well?", source.getScene());
if (removeAsWell) {
new StopRecordingAction(source, selectedModels, recorder).execute(callback);
new StopRecordingAction(source, selectedModels, recorder).execute()
.whenComplete((r, ex) -> r.stream().map(Result::getModel).forEach(callback::accept));
}
} else {
for (Model model : selectedModels) {

View File

@ -15,9 +15,9 @@ public class MarkForLaterRecordingAction extends ModelMassEditAction {
action = m -> {
try {
recorder.markForLaterRecording(m, recordLater);
} catch(Exception e) {
Platform.runLater(() ->
Dialogs.showError(source.getScene(), "Couldn't resume recording of model", "Resuming recording of " + m.getName() + " failed", e));
} catch (Exception e) {
Platform.runLater(() -> Dialogs.showError(source.getScene(), "Couldn't model mark model for later recording",
"Marking for later recording of " + m.getName() + " failed", e));
}
};
}

View File

@ -43,33 +43,35 @@ public class ModelMassEditAction {
execute(m -> {});
}
public void executeSync(Consumer<Model> callback) {
Platform.runLater(() -> source.setCursor(Cursor.WAIT));
Consumer<Model> cb = Objects.requireNonNull(callback, "Callback is null, call execute() instead");
List<Future<?>> futures = new LinkedList<>();
for (Model model : getModels()) {
futures.add(GlobalThreadPool.submit(() -> {
action.accept(model);
cb.accept(model);
}));
}
Exception ex = null;
for (Future<?> future : futures) {
try {
future.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
ex = e;
}
}
if (ex != null) {
LOG.error("Error while executing model mass edit", ex);
Dialogs.showError(source.getScene(), "Error", "Error while execution action", ex);
}
Platform.runLater(() -> source.setCursor(Cursor.DEFAULT));
}
public void execute(Consumer<Model> callback) {
GlobalThreadPool.submit(() -> {
Platform.runLater(() -> source.setCursor(Cursor.WAIT));
Consumer<Model> cb = Objects.requireNonNull(callback, "Callback is null, call execute() instead");
List<Future<?>> futures = new LinkedList<>();
for (Model model : getModels()) {
futures.add(GlobalThreadPool.submit(() -> {
action.accept(model);
cb.accept(model);
}));
}
Exception ex = null;
for (Future<?> future : futures) {
try {
future.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
ex = e;
}
}
if (ex != null) {
LOG.error("Error while executing model mass edit", ex);
Dialogs.showError(source.getScene(), "Error", "Error while execution action", ex);
}
Platform.runLater(() -> source.setCursor(Cursor.DEFAULT));
});
GlobalThreadPool.submit(() -> executeSync(callback));
}
@SuppressWarnings("unchecked")

View File

@ -1,24 +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.controls.Dialogs;
import javafx.application.Platform;
import ctbrec.ui.tasks.PauseRecordingTask;
import javafx.scene.Node;
public class PauseAction extends ModelMassEditAction {
public class PauseAction extends AbstractModelAction {
public PauseAction(Node source, List<? extends Model> models, Recorder recorder) {
super(source, models);
action = m -> {
try {
recorder.suspendRecording(m);
} catch(Exception e) {
Platform.runLater(() ->
Dialogs.showError(source.getScene(), "Couldn't suspend recording of model", "Suspending recording of " + m.getName() + " failed", e));
}
};
public PauseAction(Node source, List<Model> models, Recorder recorder) {
super(source, models, recorder, new PauseRecordingTask(recorder));
}
public CompletableFuture<List<Result>> execute() {
return super.execute("Couldn't pause recording", "Pausing recording of {0} failed:");
}
}

View File

@ -1,24 +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.controls.Dialogs;
import javafx.application.Platform;
import ctbrec.ui.tasks.ResumeRecordingTask;
import javafx.scene.Node;
public class ResumeAction extends ModelMassEditAction {
public class ResumeAction extends AbstractModelAction {
public ResumeAction(Node source, List<? extends Model> models, Recorder recorder) {
super(source, models);
action = m -> {
try {
recorder.resumeRecording(m);
} catch(Exception e) {
Platform.runLater(() ->
Dialogs.showError(source.getScene(), "Couldn't resume recording of model", "Resuming recording of " + m.getName() + " failed", e));
}
};
public ResumeAction(Node source, List<Model> models, Recorder recorder) {
super(source, models, recorder, new ResumeRecordingTask(recorder));
}
public CompletableFuture<List<Result>> execute() {
return super.execute("Couldn't resume recording", "Resuming recording of {0} failed:");
}
}

View File

@ -8,7 +8,6 @@ import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.slf4j.Logger;
@ -20,6 +19,8 @@ import ctbrec.SubsequentAction;
import ctbrec.recorder.Recorder;
import ctbrec.ui.controls.DateTimePicker;
import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.tasks.StartRecordingTask;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.Node;
@ -83,7 +84,7 @@ public class SetStopDateAction {
}
return true;
}, GlobalThreadPool.get()).whenComplete((r, e) -> {
source.setCursor(Cursor.DEFAULT);
Platform.runLater(() -> source.setCursor(Cursor.DEFAULT));
if (e != null) {
LOG.error("Error", e);
}
@ -97,13 +98,17 @@ public class SetStopDateAction {
model.setRecordUntil(stopAt);
model.setRecordUntilSubsequentAction(action);
try {
if (!recorder.isTracked(model)) {
new StartRecordingAction(source, List.of(model), recorder).execute(m -> {
if (!recorder.isTracked(model) || model.isMarkedForLaterRecording()) {
new StartRecordingTask(recorder).executeSync(model)
.thenAccept(m -> {
try {
recorder.stopRecordingAt(m);
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e1) {
showError(e1);
}
}).exceptionally(ex -> {
showError(ex);
return null;
});
} else {
recorder.stopRecordingAt(model);
@ -113,8 +118,8 @@ public class SetStopDateAction {
}
}
private void showError(Exception e) {
Dialogs.showError(source.getScene(), "Error", "Couln't set stop date", e);
private void showError(Throwable t) {
Platform.runLater(() -> Dialogs.showError(source.getScene(), "Error", "Couln't set stop date", t));
}

View File

@ -1,24 +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.controls.Dialogs;
import javafx.application.Platform;
import ctbrec.ui.tasks.StartRecordingTask;
import javafx.scene.Node;
public class StartRecordingAction extends ModelMassEditAction {
public class StartRecordingAction extends AbstractModelAction {
public StartRecordingAction(Node source, List<? extends Model> models, Recorder recorder) {
super(source, models);
action = m -> {
try {
recorder.addModel(m);
} catch (Exception e) {
Platform.runLater(() ->
Dialogs.showError(source.getScene(), "Couldn't start recording", "Starting recording of " + m.getName() + " failed", e));
}
};
public StartRecordingAction(Node source, List<Model> models, Recorder recorder) {
super(source, models, recorder, new StartRecordingTask(recorder));
}
public CompletableFuture<List<Result>> execute() {
return super.execute("Couldn't start recording", "Starting recording of {0} failed:");
}
}

View File

@ -1,24 +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.controls.Dialogs;
import javafx.application.Platform;
import ctbrec.ui.tasks.StopRecordingTask;
import javafx.scene.Node;
public class StopRecordingAction extends ModelMassEditAction {
public class StopRecordingAction extends AbstractModelAction {
public StopRecordingAction(Node source, List<? extends Model> models, Recorder recorder) {
super(source, models);
action = m -> {
try {
recorder.stopRecording(m);
} catch(Exception e) {
Platform.runLater(() ->
Dialogs.showError(source.getScene(), "Couldn't stop recording", "Stopping recording of " + m.getName() + " failed", e));
}
};
super(source, models, recorder, new StopRecordingTask(recorder));
}
public CompletableFuture<List<Result>> execute() {
return super.execute("Couldn't stop recording", "Stopping recording 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.UnfollowTask;
import javafx.scene.Node;
public class UnfollowAction extends AbstractModelAction {
public UnfollowAction(Node source, List<? extends Model> models, Recorder recorder) {
super(source, models, recorder, new UnfollowTask(recorder));
}
public CompletableFuture<List<Result>> execute() {
return super.execute("Couldn't unfollow model", "Unfollowing of {0} failed:");
}
}

View File

@ -0,0 +1,49 @@
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.AbstractModelAction.Result;
import ctbrec.ui.action.FollowAction;
import ctbrec.ui.action.TriConsumer;
import ctbrec.ui.action.UnfollowAction;
import javafx.scene.Node;
public class FollowUnfollowHandler {
private static final Logger LOG = LoggerFactory.getLogger(FollowUnfollowHandler.class);
private Node source;
private Recorder recorder;
private TriConsumer<Model, Boolean, Boolean> callback;
public FollowUnfollowHandler(Node source, Recorder recorder, TriConsumer<Model, Boolean, Boolean> callback) {
this.source = source;
this.recorder = recorder;
this.callback = callback;
}
protected void follow(List<Model> selectedModels) {
new FollowAction(source, selectedModels, recorder).execute().thenAccept(r -> {
r.stream().filter(rs -> rs.getThrowable() == null).map(Result::getModel).forEach(m -> callback.accept(m, true, true));
r.stream().filter(rs -> rs.getThrowable() != null).map(Result::getModel).forEach(m -> callback.accept(m, true, false));
}).exceptionally(ex -> {
LOG.error("Couldn't follow model", ex);
return null;
});
}
protected void unfollow(List<Model> selectedModels) {
new UnfollowAction(source, selectedModels, recorder).execute().thenAccept(r -> {
r.stream().filter(rs -> rs.getThrowable() == null).map(Result::getModel).forEach(m -> callback.accept(m, false, true));
r.stream().filter(rs -> rs.getThrowable() != null).map(Result::getModel).forEach(m -> callback.accept(m, false, false));
}).exceptionally(ex -> {
LOG.error("Couldn't unfollow model", ex);
return null;
});
}
}

View File

@ -5,7 +5,6 @@ import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
@ -13,23 +12,20 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.GlobalThreadPool;
import ctbrec.Model;
import ctbrec.ModelGroup;
import ctbrec.recorder.Recorder;
import ctbrec.ui.AutosizeAlert;
import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.SiteUiFactory;
import ctbrec.ui.StreamSourceSelectionDialog;
import ctbrec.ui.action.AbstractModelAction.Result;
import ctbrec.ui.action.AddToGroupAction;
import ctbrec.ui.action.EditNotesAction;
import ctbrec.ui.action.IgnoreModelsAction;
import ctbrec.ui.action.MarkForLaterRecordingAction;
import ctbrec.ui.action.OpenRecordingsDir;
import ctbrec.ui.action.PauseAction;
import ctbrec.ui.action.PlayAction;
import ctbrec.ui.action.RemoveTimeLimitAction;
import ctbrec.ui.action.ResumeAction;
import ctbrec.ui.action.SetPortraitAction;
import ctbrec.ui.action.SetStopDateAction;
import ctbrec.ui.action.StartRecordingAction;
@ -38,8 +34,6 @@ import ctbrec.ui.action.TipAction;
import ctbrec.ui.action.TriConsumer;
import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.tabs.FollowedTab;
import javafx.application.Platform;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.ContextMenu;
@ -214,9 +208,9 @@ public class ModelMenuContributor {
var site = selectedModels.get(0).getSite();
if (site.supportsFollow()) {
var follow = new MenuItem("Follow");
follow.setOnAction(e -> follow(selectedModels, true));
follow.setOnAction(e -> new FollowUnfollowHandler(source, recorder, followCallback).follow(selectedModels));
var unfollow = new MenuItem("Unfollow");
unfollow.setOnAction(e -> follow(selectedModels, false));
unfollow.setOnAction(e -> new FollowUnfollowHandler(source, recorder, followCallback).unfollow(selectedModels));
var followOrUnFollow = isFollowedTab() ? unfollow : follow;
followOrUnFollow.setDisable(!site.credentialsAvailable());
@ -233,52 +227,6 @@ public class ModelMenuContributor {
return false;
}
protected void follow(List<Model> selectedModels, boolean follow) {
for (Model model : selectedModels) {
follow(model, follow);
}
}
CompletableFuture<Boolean> follow(Model model, boolean follow) {
source.setCursor(Cursor.WAIT);
return CompletableFuture.supplyAsync(() -> {
var success = true;
try {
if (follow) {
SiteUiFactory.getUi(model.getSite()).login();
boolean followed = model.follow();
if (followed) {
success = true;
} else {
Dialogs.showError(source.getScene(), "Couldn't follow model", "", null);
success = false;
}
} else {
SiteUiFactory.getUi(model.getSite()).login();
boolean unfollowed = model.unfollow();
if (unfollowed) {
success = true;
} else {
Dialogs.showError(source.getScene(), "Couldn't unfollow model", "", null);
success = false;
}
}
return success;
} catch (Exception e1) {
LOG.error("Couldn't follow/unfollow model {}", model.getName(), e1);
String msg = "I/O error while following/unfollowing model " + model.getName() + ": ";
Dialogs.showError(source.getScene(), "Couldn't follow/unfollow model", msg, e1);
return false;
} finally {
final boolean result = success;
Platform.runLater(() -> {
source.setCursor(Cursor.DEFAULT);
followCallback.accept(model, follow, result);
});
}
}, GlobalThreadPool.get());
}
private void addSwitchStreamSource(ContextMenu menu, List<Model> selectedModels) {
var model = selectedModels.get(0);
if (!recorder.isTracked(model)) {
@ -352,9 +300,9 @@ public class ModelMenuContributor {
var first = selectedModels.get(0);
if (recorder.isTracked(first)) {
var pause = new MenuItem("Pause Recording");
pause.setOnAction(e -> new PauseAction(source, selectedModels, recorder).execute(m -> executeCallback()));
pause.setOnAction(e -> new PauseResumeHandler(source, recorder, callback).pause(selectedModels));
var resume = new MenuItem("Resume Recording");
resume.setOnAction(e -> new ResumeAction(source, selectedModels, recorder).execute(m -> executeCallback()));
resume.setOnAction(e -> new PauseResumeHandler(source, recorder, callback).resume(selectedModels));
var pauseResume = recorder.isSuspended(first) ? resume : pause;
menu.getItems().add(pauseResume);
}
@ -399,10 +347,6 @@ public class ModelMenuContributor {
var start = new MenuItem(text);
menu.getItems().add(start);
start.setOnAction(e -> {
selectedModels.forEach(m -> {
m.setMarkedForLaterRecording(false);
m.setSuspended(false);
});
selectedModels.forEach(m -> new SetStopDateAction(source, m, recorder).execute() //
.thenAccept(b -> executeCallback()));
});
@ -473,11 +417,23 @@ public class ModelMenuContributor {
}
private void startRecording(List<Model> models) {
new StartRecordingAction(source, models, recorder).execute(startStopCallback);
new StartRecordingAction(source, models, recorder).execute()
.whenComplete((r, ex) -> {
if (ex != null) {
LOG.error("Error while starting recordings", ex);
}
r.stream().map(Result::getModel).forEach(startStopCallback);
});
}
private void stopRecording(List<Model> models) {
new StopRecordingAction(source, models, recorder).execute(startStopCallback);
new StopRecordingAction(source, models, recorder).execute()
.whenComplete((r, ex) -> {
if (ex != null) {
LOG.error("Error while stopping recordings", ex);
}
r.stream().map(Result::getModel).forEach(startStopCallback);
});
}
private void executeCallback() {

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.PauseAction;
import ctbrec.ui.action.ResumeAction;
import javafx.scene.Node;
public class PauseResumeHandler {
private static final Logger LOG = LoggerFactory.getLogger(PauseResumeHandler.class);
private Node source;
private Recorder recorder;
private Runnable callback;
public PauseResumeHandler(Node source, Recorder recorder, Runnable callback) {
this.source = source;
this.recorder = recorder;
this.callback = callback;
}
protected void pause(List<Model> selectedModels) {
new PauseAction(source, selectedModels, recorder).execute()
.exceptionally(ex -> {
LOG.error("Error while pausing recordings", ex);
return null;
}).whenComplete((r, ex) -> executeCallback());
}
protected void resume(List<Model> selectedModels) {
new ResumeAction(source, selectedModels, recorder).execute()
.exceptionally(ex -> {
LOG.error("Error while resuming recordings", 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

@ -476,10 +476,15 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
})
.withFollowCallback( (mdl, fllw, success) -> {
if (Boolean.TRUE.equals(fllw) && Boolean.TRUE.equals(success)) {
getThumbCell(mdl).ifPresent(this::showAddToFollowedAnimation);
Platform.runLater(() -> getThumbCell(mdl).ifPresent(this::showAddToFollowedAnimation));
}
if (Boolean.FALSE.equals(fllw)) {
selectedThumbCells.clear();
Platform.runLater(() -> {
if (this instanceof FollowedTab) {
getThumbCell(mdl).ifPresent(thumbCell -> grid.getChildren().remove(thumbCell));
}
selectedThumbCells.clear();
});
}
})
.withIgnoreCallback(m -> getThumbCell(m).ifPresent(thumbCell -> {

View File

@ -18,6 +18,7 @@ import ctbrec.Model;
import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;
import ctbrec.ui.JavaFxModel;
import ctbrec.ui.action.AbstractModelAction.Result;
import ctbrec.ui.action.CheckModelAccountAction;
import ctbrec.ui.action.StopRecordingAction;
import ctbrec.ui.controls.Dialogs;
@ -61,8 +62,8 @@ public class RecordLaterTab extends AbstractRecordedModelsTab implements TabSele
root.setCenter(scrollPane);
setContent(root);
checkModelAccountExistance.setOnAction(evt -> new CheckModelAccountAction(checkModelAccountExistance, recorder)
.execute(Model::isMarkedForLaterRecording));
checkModelAccountExistance
.setOnAction(evt -> new CheckModelAccountAction(checkModelAccountExistance, recorder).execute(Model::isMarkedForLaterRecording));
restoreState();
}
@ -166,10 +167,12 @@ public class RecordLaterTab extends AbstractRecordedModelsTab implements TabSele
}
if (confirmed) {
List<Model> models = selectedModels.stream().map(JavaFxModel::getDelegate).collect(Collectors.toList());
new StopRecordingAction(getTabPane(), models, recorder).execute(m -> Platform.runLater(() -> {
table.getSelectionModel().clearSelection(table.getItems().indexOf(m));
table.getItems().remove(m);
}));
new StopRecordingAction(getTabPane(), models, recorder).execute().whenComplete((r, ex) -> {
r.stream().map(Result::getModel).forEach(m -> Platform.runLater(() -> {
table.getSelectionModel().clearSelection(table.getItems().indexOf(m));
table.getItems().remove(m);
}));
});
portraitCache.invalidateAll(models);
}
}

View File

@ -26,6 +26,7 @@ import ctbrec.Recording;
import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;
import ctbrec.ui.JavaFxModel;
import ctbrec.ui.action.AbstractModelAction.Result;
import ctbrec.ui.action.CheckModelAccountAction;
import ctbrec.ui.action.PauseAction;
import ctbrec.ui.action.ResumeAction;
@ -358,10 +359,12 @@ public class RecordedModelsTab extends AbstractRecordedModelsTab implements TabS
}
if (confirmed) {
List<Model> models = selectedModels.stream().map(JavaFxModel::getDelegate).collect(Collectors.toList());
new StopRecordingAction(getTabPane(), models, recorder).execute(m -> Platform.runLater(() -> {
table.getSelectionModel().clearSelection(table.getItems().indexOf(m));
table.getItems().remove(m);
}));
new StopRecordingAction(getTabPane(), models, recorder).execute().whenComplete((r, ex) -> {
r.stream().map(Result::getModel).forEach(m -> Platform.runLater(() -> {
table.getSelectionModel().clearSelection(table.getItems().indexOf(m));
table.getItems().remove(m);
}));
});
portraitCache.invalidateAll(models);
}
}

View File

@ -0,0 +1,38 @@
package ctbrec.ui.tasks;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import ctbrec.GlobalThreadPool;
import ctbrec.Model;
import ctbrec.recorder.Recorder;
public abstract class AbstractModelTask {
protected Recorder recorder;
private Consumer<Model> concreteTask;
protected AbstractModelTask(Recorder recorder, Consumer<Model> concreteTask) {
this.recorder = recorder;
this.concreteTask = concreteTask;
}
public CompletableFuture<Model> executeSync(Model model) {
try {
concreteTask.accept(model);
return CompletableFuture.completedFuture(model);
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}
public CompletableFuture<Model> execute(Model model) {
return CompletableFuture.supplyAsync(() -> {
try {
concreteTask.accept(model);
return model;
} catch (Exception e) {
throw new TaskExecutionException(e);
}
}, GlobalThreadPool.get());
}
}

View File

@ -0,0 +1,22 @@
package ctbrec.ui.tasks;
import ctbrec.recorder.Recorder;
public class FollowTask extends AbstractModelTask {
public FollowTask(Recorder recorder) {
super(recorder, model -> {
try {
if (model.getSite().login()) {
if (!model.follow()) {
throw new TaskExecutionException(new RuntimeException("Following " + model.getSite().getName() + " failed"));
}
} else {
throw new TaskExecutionException(new RuntimeException("Login to " + model.getSite().getName() + " failed"));
}
} catch (Exception e) {
throw new TaskExecutionException(e);
}
});
}
}

View File

@ -0,0 +1,17 @@
package ctbrec.ui.tasks;
import ctbrec.recorder.Recorder;
public class PauseRecordingTask extends AbstractModelTask {
public PauseRecordingTask(Recorder recorder) {
super(recorder, model -> {
try {
model.setSuspended(true);
recorder.suspendRecording(model);
} catch (Exception e) {
throw new TaskExecutionException(e);
}
});
}
}

View File

@ -0,0 +1,17 @@
package ctbrec.ui.tasks;
import ctbrec.recorder.Recorder;
public class ResumeRecordingTask extends AbstractModelTask {
public ResumeRecordingTask(Recorder recorder) {
super(recorder, model -> {
try {
model.setSuspended(false);
recorder.resumeRecording(model);
} catch (Exception e) {
throw new TaskExecutionException(e);
}
});
}
}

View File

@ -0,0 +1,17 @@
package ctbrec.ui.tasks;
import ctbrec.recorder.Recorder;
public class StartRecordingTask extends AbstractModelTask {
public StartRecordingTask(Recorder recorder) {
super(recorder, model -> {
try {
model.setMarkedForLaterRecording(false);
recorder.addModel(model);
} catch (Exception e) {
throw new TaskExecutionException(e);
}
});
}
}

View File

@ -0,0 +1,16 @@
package ctbrec.ui.tasks;
import ctbrec.recorder.Recorder;
public class StopRecordingTask extends AbstractModelTask {
public StopRecordingTask(Recorder recorder) {
super(recorder, model -> {
try {
recorder.stopRecording(model);
} catch (Exception e) {
throw new TaskExecutionException(e);
}
});
}
}

View File

@ -0,0 +1,9 @@
package ctbrec.ui.tasks;
public class TaskExecutionException extends RuntimeException {
public TaskExecutionException(Exception e) {
super(e);
}
}

View File

@ -0,0 +1,22 @@
package ctbrec.ui.tasks;
import ctbrec.recorder.Recorder;
public class UnfollowTask extends AbstractModelTask {
public UnfollowTask(Recorder recorder) {
super(recorder, model -> {
try {
if (model.getSite().login()) {
if (!model.unfollow()) {
throw new TaskExecutionException(new RuntimeException("Unfollowing " + model.getSite().getName() + " failed"));
}
} else {
throw new TaskExecutionException(new RuntimeException("Login to " + model.getSite().getName() + " failed"));
}
} catch (Exception e) {
throw new TaskExecutionException(e);
}
});
}
}

View File

@ -0,0 +1,85 @@
package ctbrec.ui.tasks;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import ctbrec.Model;
import ctbrec.recorder.Recorder;
class StartRecordingTaskTest {
private Recorder recorder;
private Model model;
@BeforeEach
void setup() {
recorder = mock(Recorder.class);
model = mock(Model.class);
}
@Test
void testExecuteSyncHappyPath() {
CompletableFuture<Model> future = new StartRecordingTask(recorder).executeSync(model);
try {
assertEquals(future.get(), model);
} catch (InterruptedException | ExecutionException e) {
fail("happy path should not throw an " + e.getClass().getSimpleName());
}
}
@Test
void testExecuteSyncWithException() throws InvalidKeyException, NoSuchAlgorithmException, IOException {
String exMsg = "recorder not available";
doThrow(new IOException(exMsg)).when(recorder).addModel(model);
CompletableFuture<Model> future = new StartRecordingTask(recorder).executeSync(model);
ExecutionException ex = assertThrows(ExecutionException.class, future::get);
assertTrue(getRootCause(ex) instanceof IOException);
assertEquals(exMsg, getRootCause(ex).getMessage());
}
@Test
void testExecuteHappyPath() {
CompletableFuture<Model> future = new StartRecordingTask(recorder).execute(model);
try {
assertEquals(future.get(), model);
} catch (InterruptedException | ExecutionException e) {
fail("happy path should not throw an " + e.getClass().getSimpleName());
}
}
@Test
void testExecuteWithException() throws InvalidKeyException, NoSuchAlgorithmException, IOException {
String exMsg = "recorder not available";
doThrow(new IOException(exMsg)).when(recorder).addModel(model);
CompletableFuture<Model> future = new StartRecordingTask(recorder).execute(model);
ExecutionException ex = assertThrows(ExecutionException.class, future::get);
assertTrue(ex.getCause() instanceof TaskExecutionException);
assertTrue(getRootCause(ex) instanceof IOException);
assertEquals(exMsg, getRootCause(ex).getMessage());
}
@Test
void markedForLaterShouldGetStarted() throws InvalidKeyException, NoSuchAlgorithmException, IOException {
when(model.isMarkedForLaterRecording()).thenReturn(true);
new StartRecordingTask(recorder).executeSync(model);
verify(model).setMarkedForLaterRecording(false);
verify(recorder).addModel(model);
}
private Throwable getRootCause(Throwable t) {
if (t.getCause() != null) {
return getRootCause(t.getCause());
} else {
return t;
}
}
}

View File

@ -70,7 +70,7 @@ public class NextGenLocalRecorder implements Recorder {
private volatile boolean recording = true;
private ReentrantLock recorderLock = new ReentrantLock();
private ReentrantLock modelGroupLock = new ReentrantLock();
private RecorderHttpClient client = new RecorderHttpClient();
private RecorderHttpClient client;
private Map<Model, Recording> recordingProcesses = Collections.synchronizedMap(new HashMap<>());
private RecordingManager recordingManager;
private RecordingPreconditions preconditions;
@ -90,6 +90,7 @@ public class NextGenLocalRecorder implements Recorder {
public NextGenLocalRecorder(Config config, List<Site> sites) throws IOException {
this.config = config;
client = new RecorderHttpClient(config);
downloadPool = Executors.newScheduledThreadPool(5, createThreadFactory("Download", MAX_PRIORITY));
threadPoolScaler = new ThreadPoolScaler((ThreadPoolExecutor) downloadPool, 5);
recordingManager = new RecordingManager(config, sites);
@ -265,15 +266,7 @@ public class NextGenLocalRecorder implements Recorder {
@Override
public void addModel(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
Optional<Model> existing = findModel(model);
if (existing.isPresent()) {
existing.get().setSuspended(model.isSuspended());
existing.get().setMarkedForLaterRecording(model.isMarkedForLaterRecording());
if (model.isMarkedForLaterRecording() && getRecordingProcesses().containsKey(model)) {
stopRecording(model);
} else {
startRecordingProcess(existing.get());
}
} else {
if (!existing.isPresent()) {
LOG.info("Model {} added", model);
recorderLock.lock();
try {
@ -574,7 +567,7 @@ public class NextGenLocalRecorder implements Recorder {
Optional<Model> existingModel = findModel(model);
if (existingModel.isPresent()) {
Model m = existingModel.get();
LOG.debug("Mark. Model found: {}", m);
LOG.debug("Mark for later: {}. Model found: {}", mark, m);
m.setMarkedForLaterRecording(mark);
if (mark && getCurrentlyRecording().contains(m)) {
LOG.debug("Stopping recording of {}", m);
@ -585,8 +578,8 @@ public class NextGenLocalRecorder implements Recorder {
stopRecording(model);
}
} else {
LOG.debug("Model {} not found to mark for later recording", model);
if (mark) {
LOG.debug("Model {} not found to mark for later recording", model);
model.setMarkedForLaterRecording(true);
addModel(model);
}