From efc4719018bcab79975d501a2bd0d45830632ef9 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 6 Nov 2018 16:35:41 +0100 Subject: [PATCH] Add possibility to suspend the recording for model This makes it possible to stop the recording without loosing track of the model. The user can pause/unpause recordings in the recorded models tab. There is also an new column "Paused", which indicates, if the recording is suspended for a model. --- src/main/java/ctbrec/AbstractModel.java | 11 +++ src/main/java/ctbrec/Model.java | 3 + src/main/java/ctbrec/io/ModelJsonAdapter.java | 5 + .../java/ctbrec/recorder/LocalRecorder.java | 47 +++++++++- src/main/java/ctbrec/recorder/Recorder.java | 3 + .../java/ctbrec/recorder/RemoteRecorder.java | 12 ++- .../recorder/server/RecorderServlet.java | 12 +++ src/main/java/ctbrec/ui/JavaFxModel.java | 40 +++++++- .../java/ctbrec/ui/RecordedModelsTab.java | 92 +++++++++++++++++-- 9 files changed, 211 insertions(+), 14 deletions(-) diff --git a/src/main/java/ctbrec/AbstractModel.java b/src/main/java/ctbrec/AbstractModel.java index bf0395d0..ce62e3cd 100644 --- a/src/main/java/ctbrec/AbstractModel.java +++ b/src/main/java/ctbrec/AbstractModel.java @@ -16,6 +16,7 @@ public abstract class AbstractModel implements Model { private String description; private List tags = new ArrayList<>(); private int streamUrlIndex = -1; + private boolean suspended = false; @Override public boolean isOnline() throws IOException, ExecutionException, InterruptedException { @@ -92,6 +93,16 @@ public abstract class AbstractModel implements Model { // noop default implementation, can be overriden by concrete models } + @Override + public boolean isSuspended() { + return suspended; + } + + @Override + public void setSuspended(boolean suspended) { + this.suspended = suspended; + } + @Override public int hashCode() { final int prime = 31; diff --git a/src/main/java/ctbrec/Model.java b/src/main/java/ctbrec/Model.java index 351dda3e..3144f777 100644 --- a/src/main/java/ctbrec/Model.java +++ b/src/main/java/ctbrec/Model.java @@ -38,4 +38,7 @@ public interface Model { public Site getSite(); public void writeSiteSpecificData(JsonWriter writer) throws IOException; public void readSiteSpecificData(JsonReader reader) throws IOException; + public boolean isSuspended(); + public void setSuspended(boolean suspended); + } \ No newline at end of file diff --git a/src/main/java/ctbrec/io/ModelJsonAdapter.java b/src/main/java/ctbrec/io/ModelJsonAdapter.java index 804c77fa..2a900282 100644 --- a/src/main/java/ctbrec/io/ModelJsonAdapter.java +++ b/src/main/java/ctbrec/io/ModelJsonAdapter.java @@ -32,6 +32,7 @@ public class ModelJsonAdapter extends JsonAdapter { String url = null; String type = null; int streamUrlIndex = -1; + boolean suspended = false; Model model = null; while(reader.hasNext()) { @@ -55,6 +56,9 @@ public class ModelJsonAdapter extends JsonAdapter { } else if(key.equals("streamUrlIndex")) { streamUrlIndex = reader.nextInt(); model.setStreamUrlIndex(streamUrlIndex); + } else if(key.equals("suspended")) { + suspended = reader.nextBoolean(); + model.setSuspended(suspended); } else if(key.equals("siteSpecific")) { reader.beginObject(); model.readSiteSpecificData(reader); @@ -87,6 +91,7 @@ public class ModelJsonAdapter extends JsonAdapter { writeValueIfSet(writer, "description", model.getDescription()); writeValueIfSet(writer, "url", model.getUrl()); writer.name("streamUrlIndex").value(model.getStreamUrlIndex()); + writer.name("suspended").value(model.isSuspended()); writer.name("siteSpecific"); writer.beginObject(); model.writeSiteSpecificData(writer); diff --git a/src/main/java/ctbrec/recorder/LocalRecorder.java b/src/main/java/ctbrec/recorder/LocalRecorder.java index e1916d93..1b8cdf5c 100644 --- a/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -112,7 +112,12 @@ public class LocalRecorder implements Recorder { } private void startRecordingProcess(Model model) throws IOException { - LOG.debug("Restart recording for model {}", model.getName()); + if(model.isSuspended()) { + LOG.info("Recording for model {} is suspended.", model); + return; + } + + LOG.debug("Starting recording for model {}", model.getName()); if (recordingProcesses.containsKey(model)) { LOG.error("A recording for model {} is already running", model); return; @@ -315,7 +320,7 @@ public class LocalRecorder implements Recorder { while (running) { for (Model model : getModelsRecording()) { try { - if (!recordingProcesses.containsKey(model)) { + if (!model.isSuspended() && !recordingProcesses.containsKey(model)) { boolean isOnline = model.isOnline(IGNORE_CACHE); LOG.trace("Checking online state for {}: {}", model, (isOnline ? "online" : "offline")); if (isOnline) { @@ -529,4 +534,42 @@ public class LocalRecorder implements Recorder { stopRecordingProcess(model); tryRestartRecording(model); } + + @Override + public void suspendRecording(Model model) { + lock.lock(); + try { + if (models.contains(model)) { + int index = models.indexOf(model); + models.get(index).setSuspended(true); + } else { + return; + } + } finally { + lock.unlock(); + } + + Download download = recordingProcesses.get(model); + if(download != null) { + download.stop(); + recordingProcesses.remove(model); + } + } + + @Override + public void resumeRecording(Model model) throws IOException { + lock.lock(); + try { + if (models.contains(model)) { + int index = models.indexOf(model); + Model m = models.get(index); + m.setSuspended(false); + startRecordingProcess(m); + } else { + return; + } + } finally { + lock.unlock(); + } + } } diff --git a/src/main/java/ctbrec/recorder/Recorder.java b/src/main/java/ctbrec/recorder/Recorder.java index 9effa208..e216f668 100644 --- a/src/main/java/ctbrec/recorder/Recorder.java +++ b/src/main/java/ctbrec/recorder/Recorder.java @@ -28,4 +28,7 @@ public interface Recorder { public void delete(Recording recording) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException; public void shutdown(); + + public void suspendRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException; + public void resumeRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException; } diff --git a/src/main/java/ctbrec/recorder/RemoteRecorder.java b/src/main/java/ctbrec/recorder/RemoteRecorder.java index 2cb7b216..ec0f22a5 100644 --- a/src/main/java/ctbrec/recorder/RemoteRecorder.java +++ b/src/main/java/ctbrec/recorder/RemoteRecorder.java @@ -88,7 +88,7 @@ public class RemoteRecorder implements Recorder { if("start".equals(action)) { models.add(model); - } else { + } else if("stop".equals(action)) { models.remove(model); } } else { @@ -276,4 +276,14 @@ public class RemoteRecorder implements Recorder { public void switchStreamSource(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException { sendRequest("switch", model); } + + @Override + public void suspendRecording(Model model) throws InvalidKeyException, NoSuchAlgorithmException, IllegalStateException, IOException { + sendRequest("suspend", model); + } + + @Override + public void resumeRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException { + sendRequest("resume", model); + } } diff --git a/src/main/java/ctbrec/recorder/server/RecorderServlet.java b/src/main/java/ctbrec/recorder/server/RecorderServlet.java index 4d258906..16a4210f 100644 --- a/src/main/java/ctbrec/recorder/server/RecorderServlet.java +++ b/src/main/java/ctbrec/recorder/server/RecorderServlet.java @@ -112,6 +112,18 @@ public class RecorderServlet extends AbstractCtbrecServlet { response = "{\"status\": \"success\", \"msg\": \"Resolution switched\"}"; resp.getWriter().write(response); break; + case "suspend": + LOG.debug("Suspend recording for model {} - {}", request.model.getName(), request.model.getUrl()); + recorder.suspendRecording(request.model); + response = "{\"status\": \"success\", \"msg\": \"Recording suspended\"}"; + resp.getWriter().write(response); + break; + case "resume": + LOG.debug("Resume recording for model {} - {}", request.model.getName(), request.model.getUrl()); + recorder.resumeRecording(request.model); + response = "{\"status\": \"success\", \"msg\": \"Recording resumed\"}"; + resp.getWriter().write(response); + break; default: resp.setStatus(SC_BAD_REQUEST); response = "{\"status\": \"error\", \"msg\": \"Unknown action\"}"; diff --git a/src/main/java/ctbrec/ui/JavaFxModel.java b/src/main/java/ctbrec/ui/JavaFxModel.java index a1c9d399..2a2ece56 100644 --- a/src/main/java/ctbrec/ui/JavaFxModel.java +++ b/src/main/java/ctbrec/ui/JavaFxModel.java @@ -9,7 +9,6 @@ import com.iheartradio.m3u8.PlaylistException; import com.squareup.moshi.JsonReader; import com.squareup.moshi.JsonWriter; -import ctbrec.AbstractModel; import ctbrec.Model; import ctbrec.recorder.download.StreamSource; import ctbrec.sites.Site; @@ -19,14 +18,16 @@ import javafx.beans.property.SimpleBooleanProperty; /** * Just a wrapper for Model, which augments it with JavaFX value binding properties, so that UI widgets get updated proeprly */ -public class JavaFxModel extends AbstractModel { +public class JavaFxModel implements Model { private transient BooleanProperty onlineProperty = new SimpleBooleanProperty(); + private transient BooleanProperty pausedProperty = new SimpleBooleanProperty(); private Model delegate; public JavaFxModel(Model delegate) { this.delegate = delegate; try { onlineProperty.set(delegate.isOnline()); + pausedProperty.set(delegate.isSuspended()); } catch (IOException | ExecutionException | InterruptedException e) {} } @@ -89,6 +90,10 @@ public class JavaFxModel extends AbstractModel { return onlineProperty; } + public BooleanProperty getPausedProperty() { + return pausedProperty; + } + Model getDelegate() { return delegate; } @@ -157,4 +162,35 @@ public class JavaFxModel extends AbstractModel { public void writeSiteSpecificData(JsonWriter writer) throws IOException { delegate.writeSiteSpecificData(writer); } + + @Override + public String getDescription() { + return delegate.getDescription(); + } + + @Override + public void setDescription(String description) { + delegate.setDescription(description); + } + + @Override + public int getStreamUrlIndex() { + return delegate.getStreamUrlIndex(); + } + + @Override + public void setStreamUrlIndex(int streamUrlIndex) { + delegate.setStreamUrlIndex(streamUrlIndex); + } + + @Override + public boolean isSuspended() { + return delegate.isSuspended(); + } + + @Override + public void setSuspended(boolean suspended) { + delegate.setSuspended(suspended); + pausedProperty.set(suspended); + } } diff --git a/src/main/java/ctbrec/ui/RecordedModelsTab.java b/src/main/java/ctbrec/ui/RecordedModelsTab.java index 5a37b229..8702603c 100644 --- a/src/main/java/ctbrec/ui/RecordedModelsTab.java +++ b/src/main/java/ctbrec/ui/RecordedModelsTab.java @@ -67,7 +67,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { ScrollPane scrollPane = new ScrollPane(); TableView table = new TableView(); ObservableList observableModels = FXCollections.observableArrayList(); - ContextMenu popup = createContextMenu(); + ContextMenu popup; Label modelLabel = new Label("Model"); TextField model = new TextField(); @@ -104,11 +104,17 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { online.setCellValueFactory((cdf) -> cdf.getValue().getOnlineProperty()); online.setCellFactory(CheckBoxTableCell.forTableColumn(online)); online.setPrefWidth(60); - table.getColumns().addAll(name, url, online); + TableColumn paused = new TableColumn<>("Paused"); + paused.setCellValueFactory((cdf) -> cdf.getValue().getPausedProperty()); + paused.setCellFactory(CheckBoxTableCell.forTableColumn(paused)); + paused.setPrefWidth(60); + table.getColumns().addAll(name, url, online, paused); table.setItems(observableModels); table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> { popup = createContextMenu(); - popup.show(table, event.getScreenX(), event.getScreenY()); + if(popup != null) { + popup.show(table, event.getScreenX(), event.getScreenY()); + } event.consume(); }); table.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> { @@ -194,6 +200,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { threadPool.submit(() -> { try { javaFxModel.getOnlineProperty().set(javaFxModel.isOnline()); + javaFxModel.setSuspended(model.isSuspended()); } catch (IOException | ExecutionException | InterruptedException e) {} }); } @@ -253,26 +260,37 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { } private ContextMenu createContextMenu() { - MenuItem stop = new MenuItem("Stop Recording"); + JavaFxModel selectedModel = table.getSelectionModel().getSelectedItem(); + if(selectedModel == null) { + return null; + } + MenuItem stop = new MenuItem("Remove Model"); stop.setOnAction((e) -> stopAction()); MenuItem copyUrl = new MenuItem("Copy URL"); copyUrl.setOnAction((e) -> { - Model selected = table.getSelectionModel().getSelectedItem(); + Model selected = selectedModel; final Clipboard clipboard = Clipboard.getSystemClipboard(); final ClipboardContent content = new ClipboardContent(); content.putString(selected.getUrl()); clipboard.setContent(content); }); + MenuItem pauseRecording = new MenuItem("Pause Recording"); + pauseRecording.setOnAction((e) -> pauseRecording()); + MenuItem resumeRecording = new MenuItem("Resume Recording"); + resumeRecording.setOnAction((e) -> resumeRecording()); MenuItem openInBrowser = new MenuItem("Open in Browser"); - openInBrowser.setOnAction((e) -> DesktopIntergation.open(table.getSelectionModel().getSelectedItem().getUrl())); + openInBrowser.setOnAction((e) -> DesktopIntergation.open(selectedModel.getUrl())); MenuItem openInPlayer = new MenuItem("Open in Player"); - openInPlayer.setOnAction((e) -> Player.play(table.getSelectionModel().getSelectedItem().getUrl())); + openInPlayer.setOnAction((e) -> Player.play(selectedModel.getUrl())); MenuItem switchStreamSource = new MenuItem("Switch resolution"); - switchStreamSource.setOnAction((e) -> switchStreamSource(table.getSelectionModel().getSelectedItem())); + switchStreamSource.setOnAction((e) -> switchStreamSource(selectedModel)); - return new ContextMenu(stop, copyUrl, openInBrowser, switchStreamSource); + ContextMenu menu = new ContextMenu(stop); + menu.getItems().add(selectedModel.isSuspended() ? resumeRecording : pauseRecording); + menu.getItems().addAll(copyUrl, openInBrowser, switchStreamSource); + return menu; } private void switchStreamSource(JavaFxModel fxModel) { @@ -345,4 +363,60 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { }.start(); } }; + + private void pauseRecording() { + JavaFxModel model = table.getSelectionModel().getSelectedItem(); + Model delegate = table.getSelectionModel().getSelectedItem().getDelegate(); + if (delegate != null) { + table.setCursor(Cursor.WAIT); + new Thread() { + @Override + public void run() { + try { + recorder.suspendRecording(delegate); + Platform.runLater(() -> model.setSuspended(true)); + } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) { + LOG.error("Couldn't pause recording", e1); + Platform.runLater(() -> { + Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); + alert.setTitle("Error"); + alert.setHeaderText("Couldn't pause recording"); + alert.setContentText("Error while pausing the recording: " + e1.getLocalizedMessage()); + alert.showAndWait(); + }); + } finally { + table.setCursor(Cursor.DEFAULT); + } + } + }.start(); + } + }; + + private void resumeRecording() { + JavaFxModel model = table.getSelectionModel().getSelectedItem(); + Model delegate = table.getSelectionModel().getSelectedItem().getDelegate(); + if (delegate != null) { + table.setCursor(Cursor.WAIT); + new Thread() { + @Override + public void run() { + try { + recorder.resumeRecording(delegate); + Platform.runLater(() -> model.setSuspended(false)); + } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) { + LOG.error("Couldn't resume recording", e1); + Platform.runLater(() -> { + Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); + alert.setTitle("Error"); + alert.setHeaderText("Couldn't resume recording"); + alert.setContentText("Error while resuming the recording: " + e1.getLocalizedMessage()); + alert.showAndWait(); + }); + } finally { + table.setCursor(Cursor.DEFAULT); + } + } + }.start(); + } + }; }