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.
This commit is contained in:
0xboobface 2018-11-06 16:35:41 +01:00
parent 6b16a637f0
commit efc4719018
9 changed files with 211 additions and 14 deletions

View File

@ -16,6 +16,7 @@ public abstract class AbstractModel implements Model {
private String description;
private List<String> 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;

View File

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

View File

@ -32,6 +32,7 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
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<Model> {
} 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<Model> {
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);

View File

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

View File

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

View File

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

View File

@ -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\"}";

View File

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

View File

@ -67,7 +67,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
ScrollPane scrollPane = new ScrollPane();
TableView<JavaFxModel> table = new TableView<JavaFxModel>();
ObservableList<JavaFxModel> 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<JavaFxModel, Boolean> 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();
}
};
}