forked from j62/ctbrec
1
0
Fork 0

Add pinning for recordings

Recordings can now be set to pinned. Pinned recordings cannot be
deleted.
This commit is contained in:
0xboobface 2020-03-16 16:10:09 +01:00
parent 6f278b6c49
commit 6f57579041
10 changed files with 216 additions and 25 deletions

View File

@ -95,6 +95,9 @@ public class JavaFxRecording extends Recording {
statusProperty.set("unknown");
break;
}
if (isPinned()) {
statusProperty.set(statusProperty.get() + " 🔒");
}
}
@Override
@ -184,6 +187,17 @@ public class JavaFxRecording extends Recording {
return delegate.isSingleFile();
}
@Override
public boolean isPinned() {
return delegate.isPinned();
}
@Override
public void setPinned(boolean pinned) {
delegate.setPinned(pinned);
setStatus(getStatus());
}
public boolean valueChanged() {
boolean changed = getSizeInByte() != lastValue;
lastValue = getSizeInByte();

View File

@ -13,6 +13,7 @@ import java.security.NoSuchAlgorithmException;
import java.text.DecimalFormat;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
@ -31,6 +32,7 @@ import ctbrec.Recording.State;
import ctbrec.StringUtil;
import ctbrec.recorder.ProgressListener;
import ctbrec.recorder.Recorder;
import ctbrec.recorder.RecordingPinnedException;
import ctbrec.recorder.download.hls.MergedFfmpegHlsDownload;
import ctbrec.sites.Site;
import ctbrec.ui.AutosizeAlert;
@ -387,6 +389,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
deleteRecording.setOnAction(e -> delete(recordings));
if(first.getStatus() == State.FINISHED || first.getStatus() == State.WAITING || first.getStatus() == State.FAILED || recordings.size() > 1) {
contextMenu.getItems().add(deleteRecording);
deleteRecording.setDisable(recordings.stream().allMatch(Recording::isPinned));
}
MenuItem openDir = new MenuItem("Open directory");
@ -401,6 +404,16 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
contextMenu.getItems().add(downloadRecording);
}
if (first.isPinned()) {
MenuItem unpinRecording = new MenuItem("Unpin");
unpinRecording.setOnAction(e -> unpin(recordings));
contextMenu.getItems().add(unpinRecording);
} else {
MenuItem pinRecording = new MenuItem("Pin");
pinRecording.setOnAction(e -> pin(recordings));
contextMenu.getItems().add(pinRecording);
}
MenuItem rerunPostProcessing = new MenuItem("Rerun Post-Processing");
rerunPostProcessing.setOnAction(e -> triggerPostProcessing(first));
if (first.getStatus() == FAILED || first.getStatus() == WAITING || first.getStatus() == FINISHED) {
@ -418,6 +431,58 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
return contextMenu;
}
private void pin(List<JavaFxRecording> recordings) {
table.setCursor(Cursor.WAIT);
Thread backgroundThread = new Thread(() -> {
List<Exception> exceptions = new ArrayList<>();
try {
for (JavaFxRecording javaFxRecording : recordings) {
Recording rec = javaFxRecording.getDelegate();
try {
recorder.pin(rec);
javaFxRecording.setPinned(true);
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
exceptions.add(e);
}
}
} finally {
Platform.runLater(() -> {
table.setCursor(Cursor.DEFAULT);
if (!exceptions.isEmpty()) {
showErrorDialog("Error while pinning recordings", "At least one recording couldn't be pinned", exceptions);
}
});
}
});
backgroundThread.start();
}
private void unpin(List<JavaFxRecording> recordings) {
table.setCursor(Cursor.WAIT);
Thread backgroundThread = new Thread(() -> {
List<Exception> exceptions = new ArrayList<>();
try {
for (JavaFxRecording javaFxRecording : recordings) {
Recording rec = javaFxRecording.getDelegate();
try {
recorder.unpin(rec);
javaFxRecording.setPinned(false);
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
exceptions.add(e);
}
}
} finally {
Platform.runLater(() -> {
table.setCursor(Cursor.DEFAULT);
if (!exceptions.isEmpty()) {
showErrorDialog("Error while unpinning recordings", "At least one recording couldn't be unpinned", exceptions);
}
});
}
});
backgroundThread.start();
}
private void jumpToNextModel(KeyCode code) {
if (!table.getItems().isEmpty() && (code.isLetterKey() || code.isDigitKey())) {
// determine where to start looking for the next model
@ -544,11 +609,19 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
}
private void showErrorDialog(final String title, final String msg, final Exception e) {
showErrorDialog(title, msg, Collections.singletonList(e));
}
private void showErrorDialog(final String title, final String msg, final List<Exception> exceptions) {
Platform.runLater(() -> {
AutosizeAlert autosizeAlert = new AutosizeAlert(AlertType.ERROR, getTabPane().getScene());
autosizeAlert.setTitle(title);
autosizeAlert.setHeaderText(msg);
autosizeAlert.setContentText("An error occured: " + e.getLocalizedMessage());
StringBuilder contentText = new StringBuilder("On or more error(s) occured:");
for (Exception exception : exceptions) {
contentText.append("\n• ").append(exception.getLocalizedMessage());
}
autosizeAlert.setContentText(contentText.toString());
autosizeAlert.showAndWait();
});
}
@ -591,19 +664,23 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
recordingsLock.lock();
try {
List<Recording> deleted = new ArrayList<>();
List<Exception> exceptions = new ArrayList<>();
for (Iterator<JavaFxRecording> iterator = recordings.iterator(); iterator.hasNext();) {
JavaFxRecording r = iterator.next();
if(r.getStatus() != FINISHED && r.getStatus() != FAILED && r.getStatus() != State.WAITING) {
if (r.getStatus() != FINISHED && r.getStatus() != FAILED && r.getStatus() != State.WAITING) {
continue;
}
try {
recorder.delete(r.getDelegate());
deleted.add(r);
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
} catch (RecordingPinnedException | IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
exceptions.add(e1);
LOG.error("Error while deleting recording", e1);
showErrorDialog("Error while deleting recording", "Recording not deleted", e1);
}
}
if (!exceptions.isEmpty()) {
showErrorDialog("Error while deleting recording", "Recording not deleted", exceptions);
}
observableRecordings.removeAll(deleted);
} finally {
recordingsLock.unlock();

View File

@ -58,7 +58,7 @@ public class Config {
.build();
JsonAdapter<Settings> adapter = moshi.adapter(Settings.class).lenient();
File configFile = new File(configDir, filename);
LOG.debug("Loading config from {}", configFile.getAbsolutePath());
LOG.info("Loading config from {}", configFile.getAbsolutePath());
if (configFile.exists()) {
try {
byte[] fileContent = Files.readAllBytes(configFile.toPath());

View File

@ -35,6 +35,7 @@ public class Recording implements Serializable {
private long sizeInByte = -1;
private String metaDataFile;
private boolean singleFile = false;
private boolean pinned = false;
public enum State {
RECORDING("recording"),
@ -152,6 +153,14 @@ public class Recording implements Serializable {
this.metaDataFile = metaDataFile;
}
public boolean isPinned() {
return pinned;
}
public void setPinned(boolean pinned) {
this.pinned = pinned;
}
public Duration getLength() {
if (getDownload() != null) {
return getDownload().getLength();

View File

@ -678,4 +678,14 @@ public class NextGenLocalRecorder implements Recorder {
recorderLock.unlock();
}
}
@Override
public void pin(Recording recording) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
recordingManager.pin(recording);
}
@Override
public void unpin(Recording recording) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
recordingManager.unpin(recording);
}
}

View File

@ -34,6 +34,23 @@ public interface Recorder {
public void delete(Recording recording) throws IOException, InvalidKeyException, NoSuchAlgorithmException;
/**
* Pins a recording. A pinned recording cannot be deleted.
* @param recording
* @throws IOException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
*/
public void pin(Recording recording) throws IOException, InvalidKeyException, NoSuchAlgorithmException;
/**
* Unpins a previously pinned recording. A pinned recording cannot be deleted.
* @param recording
* @throws IOException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
*/
public void unpin(Recording recording) throws IOException, InvalidKeyException, NoSuchAlgorithmException;
public void shutdown();
public void suspendRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException;

View File

@ -103,6 +103,10 @@ public class RecordingManager {
}
public void delete(Recording recording) throws IOException {
if (recording.isPinned()) {
throw new RecordingPinnedException(recording);
}
recordingsLock.lock();
try {
int idx = recordings.indexOf(recording);
@ -182,4 +186,24 @@ public class RecordingManager {
throw new IOException("Couldn't delete all files in " + directory);
}
}
public void pin(Recording recording) throws IOException {
recordingsLock.lock();
try {
recording.setPinned(true);
saveRecording(recording);
} finally {
recordingsLock.unlock();
}
}
public void unpin(Recording recording) throws IOException {
recordingsLock.lock();
try {
recording.setPinned(false);
saveRecording(recording);
} finally {
recordingsLock.unlock();
}
}
}

View File

@ -0,0 +1,10 @@
package ctbrec.recorder;
import ctbrec.Recording;
public class RecordingPinnedException extends RuntimeException {
public RecordingPinnedException(Recording rec) {
super("Recording is pinned: " + rec);
}
}

View File

@ -111,6 +111,30 @@ public class RemoteRecorder implements Recorder {
}
}
private void sendRequest(String action, Recording recording, Runnable... onSuccess) throws InvalidKeyException, NoSuchAlgorithmException, IOException {
RecordingRequest recReq = new RecordingRequest(action, recording);
String msg = recordingRequestAdapter.toJson(recReq);
RequestBody body = RequestBody.create(JSON, msg);
Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
addHmacIfNeeded(msg, builder);
Request request = builder.build();
try (Response response = client.execute(request)) {
String json = response.body().string();
RecordingListResponse resp = recordingListResponseAdapter.fromJson(json);
if (response.isSuccessful()) {
if (!resp.status.equals(SUCCESS)) {
throw new IOException("Request failed: " + resp.msg);
} else {
for (Runnable callback : onSuccess) {
callback.run();
}
}
} else {
throw new IOException("Request failed: " + resp.msg);
}
}
}
private void addHmacIfNeeded(String msg, Builder builder) throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
if (Config.getInstance().getSettings().requireAuthentication) {
byte[] key = Config.getInstance().getSettings().key;
@ -336,26 +360,8 @@ public class RemoteRecorder implements Recorder {
}
@Override
public void delete(Recording recording) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
RecordingRequest recReq = new RecordingRequest("delete", recording);
String msg = recordingRequestAdapter.toJson(recReq);
RequestBody body = RequestBody.create(JSON, msg);
Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
addHmacIfNeeded(msg, builder);
Request request = builder.build();
try (Response response = client.execute(request)) {
String json = response.body().string();
RecordingListResponse resp = recordingListResponseAdapter.fromJson(json);
if (response.isSuccessful()) {
if (!resp.status.equals(SUCCESS)) {
throw new IOException("Couldn't delete recording: " + resp.msg);
} else {
recordings.remove(recording);
}
} else {
throw new IOException("Couldn't delete recording: " + resp.msg);
}
}
public void delete(Recording recording) throws InvalidKeyException, NoSuchAlgorithmException, IOException {
sendRequest("delete", recording, () -> recordings.remove(recording));
}
public static class ModelRequest {
@ -461,6 +467,16 @@ public class RemoteRecorder implements Recorder {
return spaceFree;
}
@Override
public void pin(Recording recording) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
sendRequest("pin", recording);
}
@Override
public void unpin(Recording recording) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
sendRequest("unpin", recording);
}
@Override
public void rerunPostProcessing(Recording recording) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
RecordingRequest recReq = new RecordingRequest("rerunPostProcessing", recording);

View File

@ -136,6 +136,20 @@ public class RecorderServlet extends AbstractCtbrecServlet {
resp.getWriter().write(recAdapter.toJson(request.recording));
resp.getWriter().write("]}");
break;
case "pin":
recorder.pin(request.recording);
recAdapter = moshi.adapter(Recording.class);
resp.getWriter().write("{\"status\": \"success\", \"msg\": \"List of recordings\", \"recordings\": [");
resp.getWriter().write(recAdapter.toJson(request.recording));
resp.getWriter().write("]}");
break;
case "unpin":
recorder.unpin(request.recording);
recAdapter = moshi.adapter(Recording.class);
resp.getWriter().write("{\"status\": \"success\", \"msg\": \"List of recordings\", \"recordings\": [");
resp.getWriter().write(recAdapter.toJson(request.recording));
resp.getWriter().write("]}");
break;
case "rerunPostProcessing":
recorder.rerunPostProcessing(request.recording);
recAdapter = moshi.adapter(Recording.class);