diff --git a/client/src/main/java/ctbrec/ui/action/AbstractAction.java b/client/src/main/java/ctbrec/ui/action/AbstractAction.java new file mode 100644 index 00000000..08d37fd7 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/action/AbstractAction.java @@ -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 { + + private R result; + + public CompletableFuture> 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> 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 beforeOnGuiThread(Runnable r) { + return (T) this; + } + + protected void onSuccess(Optional result) {} + + protected void onError(Exception e) {} + + protected void done(Optional result) {} +} diff --git a/client/src/main/java/ctbrec/ui/action/AbstractModelAction.java b/client/src/main/java/ctbrec/ui/action/AbstractModelAction.java new file mode 100644 index 00000000..3632d20a --- /dev/null +++ b/client/src/main/java/ctbrec/ui/action/AbstractModelAction.java @@ -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 models; + protected Recorder recorder; + private AbstractModelTask task; + + protected AbstractModelAction(Node source, List models, Recorder recorder, AbstractModelTask task) { + this.source = source; + this.models = models; + this.recorder = recorder; + this.task = task; + } + + protected CompletableFuture> execute(String errorHeader, String errorMsg) { + source.setCursor(Cursor.WAIT); + return CompletableFuture.supplyAsync(() -> { + final List result = new ArrayList<>(models.size()); + final List> 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 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; + } + } +} diff --git a/client/src/main/java/ctbrec/ui/action/FollowAction.java b/client/src/main/java/ctbrec/ui/action/FollowAction.java index 2cb53c84..f60cd099 100644 --- a/client/src/main/java/ctbrec/ui/action/FollowAction.java +++ b/client/src/main/java/ctbrec/ui/action/FollowAction.java @@ -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 models, Recorder recorder) { + super(source, models, recorder, new FollowTask(recorder)); + } - public FollowAction(Node source, List 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> execute() { + return super.execute("Couldn't follow model", "Following of {0} failed:"); } } diff --git a/client/src/main/java/ctbrec/ui/action/IgnoreModelsAction.java b/client/src/main/java/ctbrec/ui/action/IgnoreModelsAction.java index fe2d125d..e4e95531 100644 --- a/client/src/main/java/ctbrec/ui/action/IgnoreModelsAction.java +++ b/client/src/main/java/ctbrec/ui/action/IgnoreModelsAction.java @@ -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) { diff --git a/client/src/main/java/ctbrec/ui/action/MarkForLaterRecordingAction.java b/client/src/main/java/ctbrec/ui/action/MarkForLaterRecordingAction.java index 2aa0b3b0..915cb952 100644 --- a/client/src/main/java/ctbrec/ui/action/MarkForLaterRecordingAction.java +++ b/client/src/main/java/ctbrec/ui/action/MarkForLaterRecordingAction.java @@ -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)); } }; } diff --git a/client/src/main/java/ctbrec/ui/action/ModelMassEditAction.java b/client/src/main/java/ctbrec/ui/action/ModelMassEditAction.java index 65f42c20..b342f18b 100644 --- a/client/src/main/java/ctbrec/ui/action/ModelMassEditAction.java +++ b/client/src/main/java/ctbrec/ui/action/ModelMassEditAction.java @@ -43,33 +43,35 @@ public class ModelMassEditAction { execute(m -> {}); } + public void executeSync(Consumer callback) { + Platform.runLater(() -> source.setCursor(Cursor.WAIT)); + Consumer cb = Objects.requireNonNull(callback, "Callback is null, call execute() instead"); + List> 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 callback) { - GlobalThreadPool.submit(() -> { - Platform.runLater(() -> source.setCursor(Cursor.WAIT)); - Consumer cb = Objects.requireNonNull(callback, "Callback is null, call execute() instead"); - List> 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") diff --git a/client/src/main/java/ctbrec/ui/action/PauseAction.java b/client/src/main/java/ctbrec/ui/action/PauseAction.java index b938160e..9b0425df 100644 --- a/client/src/main/java/ctbrec/ui/action/PauseAction.java +++ b/client/src/main/java/ctbrec/ui/action/PauseAction.java @@ -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 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 models, Recorder recorder) { + super(source, models, recorder, new PauseRecordingTask(recorder)); + } + + public CompletableFuture> execute() { + return super.execute("Couldn't pause recording", "Pausing recording of {0} failed:"); } } diff --git a/client/src/main/java/ctbrec/ui/action/ResumeAction.java b/client/src/main/java/ctbrec/ui/action/ResumeAction.java index 2bbff45f..cab269e9 100644 --- a/client/src/main/java/ctbrec/ui/action/ResumeAction.java +++ b/client/src/main/java/ctbrec/ui/action/ResumeAction.java @@ -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 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 models, Recorder recorder) { + super(source, models, recorder, new ResumeRecordingTask(recorder)); + } + + public CompletableFuture> execute() { + return super.execute("Couldn't resume recording", "Resuming recording of {0} failed:"); } } diff --git a/client/src/main/java/ctbrec/ui/action/SetStopDateAction.java b/client/src/main/java/ctbrec/ui/action/SetStopDateAction.java index b4983763..ee9cbf36 100644 --- a/client/src/main/java/ctbrec/ui/action/SetStopDateAction.java +++ b/client/src/main/java/ctbrec/ui/action/SetStopDateAction.java @@ -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)); } diff --git a/client/src/main/java/ctbrec/ui/action/StartRecordingAction.java b/client/src/main/java/ctbrec/ui/action/StartRecordingAction.java index 693eb3c3..697bd439 100644 --- a/client/src/main/java/ctbrec/ui/action/StartRecordingAction.java +++ b/client/src/main/java/ctbrec/ui/action/StartRecordingAction.java @@ -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 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 models, Recorder recorder) { + super(source, models, recorder, new StartRecordingTask(recorder)); + } + + public CompletableFuture> execute() { + return super.execute("Couldn't start recording", "Starting recording of {0} failed:"); } } diff --git a/client/src/main/java/ctbrec/ui/action/StopRecordingAction.java b/client/src/main/java/ctbrec/ui/action/StopRecordingAction.java index ca185e8d..be18e0ef 100644 --- a/client/src/main/java/ctbrec/ui/action/StopRecordingAction.java +++ b/client/src/main/java/ctbrec/ui/action/StopRecordingAction.java @@ -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 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> execute() { + return super.execute("Couldn't stop recording", "Stopping recording of {0} failed:"); } } diff --git a/client/src/main/java/ctbrec/ui/action/UnfollowAction.java b/client/src/main/java/ctbrec/ui/action/UnfollowAction.java new file mode 100644 index 00000000..2a7e81d8 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/action/UnfollowAction.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.UnfollowTask; +import javafx.scene.Node; + +public class UnfollowAction extends AbstractModelAction { + + public UnfollowAction(Node source, List models, Recorder recorder) { + super(source, models, recorder, new UnfollowTask(recorder)); + } + + public CompletableFuture> execute() { + return super.execute("Couldn't unfollow model", "Unfollowing of {0} failed:"); + } +} diff --git a/client/src/main/java/ctbrec/ui/menu/FollowUnfollowHandler.java b/client/src/main/java/ctbrec/ui/menu/FollowUnfollowHandler.java new file mode 100644 index 00000000..3adfb9a4 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/menu/FollowUnfollowHandler.java @@ -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 callback; + + public FollowUnfollowHandler(Node source, Recorder recorder, TriConsumer callback) { + this.source = source; + this.recorder = recorder; + this.callback = callback; + } + + protected void follow(List 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 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; + }); + } +} diff --git a/client/src/main/java/ctbrec/ui/menu/ModelMenuContributor.java b/client/src/main/java/ctbrec/ui/menu/ModelMenuContributor.java index ab0a74d5..328df984 100644 --- a/client/src/main/java/ctbrec/ui/menu/ModelMenuContributor.java +++ b/client/src/main/java/ctbrec/ui/menu/ModelMenuContributor.java @@ -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 selectedModels, boolean follow) { - for (Model model : selectedModels) { - follow(model, follow); - } - } - - CompletableFuture 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 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 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 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() { diff --git a/client/src/main/java/ctbrec/ui/menu/PauseResumeHandler.java b/client/src/main/java/ctbrec/ui/menu/PauseResumeHandler.java new file mode 100644 index 00000000..d6aae631 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/menu/PauseResumeHandler.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.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 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 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); + } + } +} diff --git a/client/src/main/java/ctbrec/ui/tabs/ThumbOverviewTab.java b/client/src/main/java/ctbrec/ui/tabs/ThumbOverviewTab.java index 6d69d316..c0ee2160 100644 --- a/client/src/main/java/ctbrec/ui/tabs/ThumbOverviewTab.java +++ b/client/src/main/java/ctbrec/ui/tabs/ThumbOverviewTab.java @@ -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 -> { diff --git a/client/src/main/java/ctbrec/ui/tabs/recorded/RecordLaterTab.java b/client/src/main/java/ctbrec/ui/tabs/recorded/RecordLaterTab.java index 7e20a59c..f66e8d1d 100644 --- a/client/src/main/java/ctbrec/ui/tabs/recorded/RecordLaterTab.java +++ b/client/src/main/java/ctbrec/ui/tabs/recorded/RecordLaterTab.java @@ -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 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); } } 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 4e5a3e15..dbb3517c 100644 --- a/client/src/main/java/ctbrec/ui/tabs/recorded/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/tabs/recorded/RecordedModelsTab.java @@ -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 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); } } diff --git a/client/src/main/java/ctbrec/ui/tasks/AbstractModelTask.java b/client/src/main/java/ctbrec/ui/tasks/AbstractModelTask.java new file mode 100644 index 00000000..6b05fc8a --- /dev/null +++ b/client/src/main/java/ctbrec/ui/tasks/AbstractModelTask.java @@ -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 concreteTask; + + protected AbstractModelTask(Recorder recorder, Consumer concreteTask) { + this.recorder = recorder; + this.concreteTask = concreteTask; + } + + public CompletableFuture executeSync(Model model) { + try { + concreteTask.accept(model); + return CompletableFuture.completedFuture(model); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + } + + public CompletableFuture execute(Model model) { + return CompletableFuture.supplyAsync(() -> { + try { + concreteTask.accept(model); + return model; + } catch (Exception e) { + throw new TaskExecutionException(e); + } + }, GlobalThreadPool.get()); + } +} diff --git a/client/src/main/java/ctbrec/ui/tasks/FollowTask.java b/client/src/main/java/ctbrec/ui/tasks/FollowTask.java new file mode 100644 index 00000000..18f1f4ff --- /dev/null +++ b/client/src/main/java/ctbrec/ui/tasks/FollowTask.java @@ -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); + } + }); + } +} diff --git a/client/src/main/java/ctbrec/ui/tasks/PauseRecordingTask.java b/client/src/main/java/ctbrec/ui/tasks/PauseRecordingTask.java new file mode 100644 index 00000000..9af14a61 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/tasks/PauseRecordingTask.java @@ -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); + } + }); + } +} diff --git a/client/src/main/java/ctbrec/ui/tasks/ResumeRecordingTask.java b/client/src/main/java/ctbrec/ui/tasks/ResumeRecordingTask.java new file mode 100644 index 00000000..7e90ef6b --- /dev/null +++ b/client/src/main/java/ctbrec/ui/tasks/ResumeRecordingTask.java @@ -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); + } + }); + } +} diff --git a/client/src/main/java/ctbrec/ui/tasks/StartRecordingTask.java b/client/src/main/java/ctbrec/ui/tasks/StartRecordingTask.java new file mode 100644 index 00000000..e374e827 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/tasks/StartRecordingTask.java @@ -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); + } + }); + } +} diff --git a/client/src/main/java/ctbrec/ui/tasks/StopRecordingTask.java b/client/src/main/java/ctbrec/ui/tasks/StopRecordingTask.java new file mode 100644 index 00000000..8d2be77d --- /dev/null +++ b/client/src/main/java/ctbrec/ui/tasks/StopRecordingTask.java @@ -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); + } + }); + } +} diff --git a/client/src/main/java/ctbrec/ui/tasks/TaskExecutionException.java b/client/src/main/java/ctbrec/ui/tasks/TaskExecutionException.java new file mode 100644 index 00000000..a8ca51c0 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/tasks/TaskExecutionException.java @@ -0,0 +1,9 @@ +package ctbrec.ui.tasks; + +public class TaskExecutionException extends RuntimeException { + + public TaskExecutionException(Exception e) { + super(e); + } + +} diff --git a/client/src/main/java/ctbrec/ui/tasks/UnfollowTask.java b/client/src/main/java/ctbrec/ui/tasks/UnfollowTask.java new file mode 100644 index 00000000..75fb3794 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/tasks/UnfollowTask.java @@ -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); + } + }); + } +} diff --git a/client/src/test/java/ctbrec/ui/tasks/StartRecordingTaskTest.java b/client/src/test/java/ctbrec/ui/tasks/StartRecordingTaskTest.java new file mode 100644 index 00000000..cd881ce9 --- /dev/null +++ b/client/src/test/java/ctbrec/ui/tasks/StartRecordingTaskTest.java @@ -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 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 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 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 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; + } + } +} diff --git a/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java b/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java index 63dae26a..8b260767 100644 --- a/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java @@ -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 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 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 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 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); }