forked from j62/ctbrec
Add pinning for recordings
Recordings can now be set to pinned. Pinned recordings cannot be deleted.
This commit is contained in:
parent
6f278b6c49
commit
6f57579041
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
package ctbrec.recorder;
|
||||
|
||||
import ctbrec.Recording;
|
||||
|
||||
public class RecordingPinnedException extends RuntimeException {
|
||||
|
||||
public RecordingPinnedException(Recording rec) {
|
||||
super("Recording is pinned: " + rec);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue