forked from j62/ctbrec
1
0
Fork 0

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; package ctbrec.ui.action;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.ui.controls.Dialogs; import ctbrec.recorder.Recorder;
import javafx.application.Platform; import ctbrec.ui.tasks.FollowTask;
import javafx.scene.Node; 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) { public CompletableFuture<List<Result>> execute() {
super(source, models); return super.execute("Couldn't follow model", "Following of {0} failed:");
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));
}
};
} }
} }

View File

@ -7,6 +7,7 @@ import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.ui.JavaFxModel; import ctbrec.ui.JavaFxModel;
import ctbrec.ui.action.AbstractModelAction.Result;
import ctbrec.ui.controls.Dialogs; import ctbrec.ui.controls.Dialogs;
import javafx.scene.Node; import javafx.scene.Node;
@ -44,7 +45,8 @@ public class IgnoreModelsAction {
if (withRemoveDialog) { if (withRemoveDialog) {
boolean removeAsWell = Dialogs.showConfirmDialog("Ignore Model", null, "Remove as well?", source.getScene()); boolean removeAsWell = Dialogs.showConfirmDialog("Ignore Model", null, "Remove as well?", source.getScene());
if (removeAsWell) { 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 { } else {
for (Model model : selectedModels) { for (Model model : selectedModels) {

View File

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

View File

@ -43,33 +43,35 @@ public class ModelMassEditAction {
execute(m -> {}); 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) { public void execute(Consumer<Model> callback) {
GlobalThreadPool.submit(() -> { GlobalThreadPool.submit(() -> executeSync(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));
});
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")

View File

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

View File

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

View File

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

View File

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

View File

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

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.security.NoSuchAlgorithmException;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -13,23 +12,20 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.GlobalThreadPool;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.ModelGroup; import ctbrec.ModelGroup;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.ui.AutosizeAlert; import ctbrec.ui.AutosizeAlert;
import ctbrec.ui.DesktopIntegration; import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.SiteUiFactory;
import ctbrec.ui.StreamSourceSelectionDialog; import ctbrec.ui.StreamSourceSelectionDialog;
import ctbrec.ui.action.AbstractModelAction.Result;
import ctbrec.ui.action.AddToGroupAction; import ctbrec.ui.action.AddToGroupAction;
import ctbrec.ui.action.EditNotesAction; import ctbrec.ui.action.EditNotesAction;
import ctbrec.ui.action.IgnoreModelsAction; import ctbrec.ui.action.IgnoreModelsAction;
import ctbrec.ui.action.MarkForLaterRecordingAction; import ctbrec.ui.action.MarkForLaterRecordingAction;
import ctbrec.ui.action.OpenRecordingsDir; import ctbrec.ui.action.OpenRecordingsDir;
import ctbrec.ui.action.PauseAction;
import ctbrec.ui.action.PlayAction; import ctbrec.ui.action.PlayAction;
import ctbrec.ui.action.RemoveTimeLimitAction; import ctbrec.ui.action.RemoveTimeLimitAction;
import ctbrec.ui.action.ResumeAction;
import ctbrec.ui.action.SetPortraitAction; import ctbrec.ui.action.SetPortraitAction;
import ctbrec.ui.action.SetStopDateAction; import ctbrec.ui.action.SetStopDateAction;
import ctbrec.ui.action.StartRecordingAction; import ctbrec.ui.action.StartRecordingAction;
@ -38,8 +34,6 @@ import ctbrec.ui.action.TipAction;
import ctbrec.ui.action.TriConsumer; import ctbrec.ui.action.TriConsumer;
import ctbrec.ui.controls.Dialogs; import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.tabs.FollowedTab; import ctbrec.ui.tabs.FollowedTab;
import javafx.application.Platform;
import javafx.scene.Cursor;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
import javafx.scene.control.ContextMenu; import javafx.scene.control.ContextMenu;
@ -214,9 +208,9 @@ public class ModelMenuContributor {
var site = selectedModels.get(0).getSite(); var site = selectedModels.get(0).getSite();
if (site.supportsFollow()) { if (site.supportsFollow()) {
var follow = new MenuItem("Follow"); 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"); 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; var followOrUnFollow = isFollowedTab() ? unfollow : follow;
followOrUnFollow.setDisable(!site.credentialsAvailable()); followOrUnFollow.setDisable(!site.credentialsAvailable());
@ -233,52 +227,6 @@ public class ModelMenuContributor {
return false; 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) { private void addSwitchStreamSource(ContextMenu menu, List<Model> selectedModels) {
var model = selectedModels.get(0); var model = selectedModels.get(0);
if (!recorder.isTracked(model)) { if (!recorder.isTracked(model)) {
@ -352,9 +300,9 @@ public class ModelMenuContributor {
var first = selectedModels.get(0); var first = selectedModels.get(0);
if (recorder.isTracked(first)) { if (recorder.isTracked(first)) {
var pause = new MenuItem("Pause Recording"); 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"); 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; var pauseResume = recorder.isSuspended(first) ? resume : pause;
menu.getItems().add(pauseResume); menu.getItems().add(pauseResume);
} }
@ -399,10 +347,6 @@ public class ModelMenuContributor {
var start = new MenuItem(text); var start = new MenuItem(text);
menu.getItems().add(start); menu.getItems().add(start);
start.setOnAction(e -> { start.setOnAction(e -> {
selectedModels.forEach(m -> {
m.setMarkedForLaterRecording(false);
m.setSuspended(false);
});
selectedModels.forEach(m -> new SetStopDateAction(source, m, recorder).execute() // selectedModels.forEach(m -> new SetStopDateAction(source, m, recorder).execute() //
.thenAccept(b -> executeCallback())); .thenAccept(b -> executeCallback()));
}); });
@ -473,11 +417,23 @@ public class ModelMenuContributor {
} }
private void startRecording(List<Model> models) { 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) { 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() { 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) -> { .withFollowCallback( (mdl, fllw, success) -> {
if (Boolean.TRUE.equals(fllw) && Boolean.TRUE.equals(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)) { 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 -> { .withIgnoreCallback(m -> getThumbCell(m).ifPresent(thumbCell -> {

View File

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

View File

@ -26,6 +26,7 @@ import ctbrec.Recording;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.sites.Site; import ctbrec.sites.Site;
import ctbrec.ui.JavaFxModel; import ctbrec.ui.JavaFxModel;
import ctbrec.ui.action.AbstractModelAction.Result;
import ctbrec.ui.action.CheckModelAccountAction; import ctbrec.ui.action.CheckModelAccountAction;
import ctbrec.ui.action.PauseAction; import ctbrec.ui.action.PauseAction;
import ctbrec.ui.action.ResumeAction; import ctbrec.ui.action.ResumeAction;
@ -358,10 +359,12 @@ public class RecordedModelsTab extends AbstractRecordedModelsTab implements TabS
} }
if (confirmed) { if (confirmed) {
List<Model> models = selectedModels.stream().map(JavaFxModel::getDelegate).collect(Collectors.toList()); List<Model> models = selectedModels.stream().map(JavaFxModel::getDelegate).collect(Collectors.toList());
new StopRecordingAction(getTabPane(), models, recorder).execute(m -> Platform.runLater(() -> { new StopRecordingAction(getTabPane(), models, recorder).execute().whenComplete((r, ex) -> {
table.getSelectionModel().clearSelection(table.getItems().indexOf(m)); r.stream().map(Result::getModel).forEach(m -> Platform.runLater(() -> {
table.getItems().remove(m); table.getSelectionModel().clearSelection(table.getItems().indexOf(m));
})); table.getItems().remove(m);
}));
});
portraitCache.invalidateAll(models); 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 volatile boolean recording = true;
private ReentrantLock recorderLock = new ReentrantLock(); private ReentrantLock recorderLock = new ReentrantLock();
private ReentrantLock modelGroupLock = 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 Map<Model, Recording> recordingProcesses = Collections.synchronizedMap(new HashMap<>());
private RecordingManager recordingManager; private RecordingManager recordingManager;
private RecordingPreconditions preconditions; private RecordingPreconditions preconditions;
@ -90,6 +90,7 @@ public class NextGenLocalRecorder implements Recorder {
public NextGenLocalRecorder(Config config, List<Site> sites) throws IOException { public NextGenLocalRecorder(Config config, List<Site> sites) throws IOException {
this.config = config; this.config = config;
client = new RecorderHttpClient(config);
downloadPool = Executors.newScheduledThreadPool(5, createThreadFactory("Download", MAX_PRIORITY)); downloadPool = Executors.newScheduledThreadPool(5, createThreadFactory("Download", MAX_PRIORITY));
threadPoolScaler = new ThreadPoolScaler((ThreadPoolExecutor) downloadPool, 5); threadPoolScaler = new ThreadPoolScaler((ThreadPoolExecutor) downloadPool, 5);
recordingManager = new RecordingManager(config, sites); recordingManager = new RecordingManager(config, sites);
@ -265,15 +266,7 @@ public class NextGenLocalRecorder implements Recorder {
@Override @Override
public void addModel(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException { public void addModel(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
Optional<Model> existing = findModel(model); Optional<Model> existing = findModel(model);
if (existing.isPresent()) { 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 {
LOG.info("Model {} added", model); LOG.info("Model {} added", model);
recorderLock.lock(); recorderLock.lock();
try { try {
@ -574,7 +567,7 @@ public class NextGenLocalRecorder implements Recorder {
Optional<Model> existingModel = findModel(model); Optional<Model> existingModel = findModel(model);
if (existingModel.isPresent()) { if (existingModel.isPresent()) {
Model m = existingModel.get(); Model m = existingModel.get();
LOG.debug("Mark. Model found: {}", m); LOG.debug("Mark for later: {}. Model found: {}", mark, m);
m.setMarkedForLaterRecording(mark); m.setMarkedForLaterRecording(mark);
if (mark && getCurrentlyRecording().contains(m)) { if (mark && getCurrentlyRecording().contains(m)) {
LOG.debug("Stopping recording of {}", m); LOG.debug("Stopping recording of {}", m);
@ -585,8 +578,8 @@ public class NextGenLocalRecorder implements Recorder {
stopRecording(model); stopRecording(model);
} }
} else { } else {
LOG.debug("Model {} not found to mark for later recording", model);
if (mark) { if (mark) {
LOG.debug("Model {} not found to mark for later recording", model);
model.setMarkedForLaterRecording(true); model.setMarkedForLaterRecording(true);
addModel(model); addModel(model);
} }