diff --git a/client/src/main/java/ctbrec/ui/InstantProperty.java b/client/src/main/java/ctbrec/ui/InstantProperty.java new file mode 100644 index 00000000..d9efdd9c --- /dev/null +++ b/client/src/main/java/ctbrec/ui/InstantProperty.java @@ -0,0 +1,18 @@ +package ctbrec.ui; + +import java.time.Instant; + +import javafx.beans.property.ObjectPropertyBase; + +public class InstantProperty extends ObjectPropertyBase { + + @Override + public Object getBean() { + return get(); + } + + @Override + public String getName() { + return "Instant"; + } +} diff --git a/client/src/main/java/ctbrec/ui/JavaFxModel.java b/client/src/main/java/ctbrec/ui/JavaFxModel.java index 6fbfb52c..cedfedbc 100644 --- a/client/src/main/java/ctbrec/ui/JavaFxModel.java +++ b/client/src/main/java/ctbrec/ui/JavaFxModel.java @@ -1,6 +1,7 @@ package ctbrec.ui; import java.io.IOException; +import java.time.Instant; import java.util.List; import java.util.concurrent.ExecutionException; @@ -18,6 +19,7 @@ import ctbrec.sites.Site; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleObjectProperty; /** * Just a wrapper for Model, which augments it with JavaFX value binding properties, so that UI widgets get updated proeprly @@ -27,11 +29,16 @@ public class JavaFxModel implements Model { private transient BooleanProperty recordingProperty = new SimpleBooleanProperty(); private transient BooleanProperty pausedProperty = new SimpleBooleanProperty(); private transient SimpleIntegerProperty priorityProperty = new SimpleIntegerProperty(); + private transient SimpleObjectProperty lastSeenProperty = new SimpleObjectProperty<>(); + private transient SimpleObjectProperty lastRecordedProperty = new SimpleObjectProperty<>(); + private Model delegate; public JavaFxModel(Model delegate) { this.delegate = delegate; setPriority(delegate.getPriority()); + setLastSeen(delegate.getLastSeen()); + setLastRecorded(delegate.getLastRecorded()); } @Override @@ -234,6 +241,36 @@ public class JavaFxModel implements Model { return delegate.getPriority(); } + public SimpleObjectProperty lastSeenProperty() { + return lastSeenProperty; + } + + @Override + public void setLastSeen(Instant timestamp) { + delegate.setLastSeen(timestamp); + lastSeenProperty.set(timestamp); + } + + @Override + public Instant getLastSeen() { + return delegate.getLastSeen(); + } + + public SimpleObjectProperty lastRecordedProperty() { + return lastRecordedProperty; + } + + @Override + public void setLastRecorded(Instant timestamp) { + delegate.setLastRecorded(timestamp); + lastRecordedProperty.set(timestamp); + } + + @Override + public Instant getLastRecorded() { + return delegate.getLastRecorded(); + } + @Override public int compareTo(Model o) { return delegate.compareTo(o); diff --git a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java index 87ccfef8..f28dd1ae 100644 --- a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java @@ -3,6 +3,7 @@ package ctbrec.ui; import java.io.IOException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; @@ -34,6 +35,7 @@ import ctbrec.ui.action.PlayAction; import ctbrec.ui.action.ResumeAction; import ctbrec.ui.action.StopRecordingAction; import ctbrec.ui.controls.AutoFillTextField; +import ctbrec.ui.controls.DateTimeCellFactory; import ctbrec.ui.controls.Dialogs; import ctbrec.ui.controls.SearchBox; import javafx.application.Platform; @@ -176,6 +178,16 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { priority.setOnEditStart(e -> cellEditing = true); priority.setOnEditCommit(this::onUpdatePriority); priority.setOnEditCancel(e -> cellEditing = false); + TableColumn lastSeen = new TableColumn<>("last seen"); + lastSeen.setCellValueFactory(cdf -> cdf.getValue().lastSeenProperty()); + lastSeen.setCellFactory(new DateTimeCellFactory()); + lastSeen.setPrefWidth(150); + lastSeen.setEditable(false); + TableColumn lastRecorded = new TableColumn<>("last recorded"); + lastRecorded.setCellValueFactory(cdf -> cdf.getValue().lastRecordedProperty()); + lastRecorded.setCellFactory(new DateTimeCellFactory()); + lastRecorded.setPrefWidth(150); + lastRecorded.setEditable(false); TableColumn notes = new TableColumn<>("Notes"); notes.setCellValueFactory(cdf -> { JavaFxModel m = cdf.getValue(); @@ -199,7 +211,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { }); notes.setPrefWidth(400); notes.setEditable(false); - table.getColumns().addAll(preview, name, url, online, recording, paused, priority, notes); + table.getColumns().addAll(preview, name, url, online, recording, paused, priority, lastSeen, lastRecorded, notes); table.setItems(observableModels); table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> { popup = createContextMenu(); @@ -417,6 +429,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { oldModel.setSuspended(updatedModel.isSuspended()); oldModel.getOnlineProperty().set(updatedModel.getOnlineProperty().get()); oldModel.getRecordingProperty().set(updatedModel.getRecordingProperty().get()); + oldModel.lastRecordedProperty().set(updatedModel.lastRecordedProperty().get()); + oldModel.lastSeenProperty().set(updatedModel.lastSeenProperty().get()); } } } diff --git a/client/src/main/java/ctbrec/ui/RecordingsTab.java b/client/src/main/java/ctbrec/ui/RecordingsTab.java index d2821dd3..179feb21 100644 --- a/client/src/main/java/ctbrec/ui/RecordingsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordingsTab.java @@ -12,10 +12,6 @@ import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.text.DecimalFormat; import java.time.Instant; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.FormatStyle; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -37,6 +33,7 @@ import ctbrec.recorder.ProgressListener; import ctbrec.recorder.Recorder; import ctbrec.recorder.download.hls.MergedHlsDownload; import ctbrec.sites.Site; +import ctbrec.ui.controls.DateTimeCellFactory; import ctbrec.ui.controls.Toast; import javafx.application.Platform; import javafx.beans.property.SimpleObjectProperty; @@ -127,7 +124,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener { Instant instant = cdf.getValue().getStartDate(); return new SimpleObjectProperty(instant); }); - date.setCellFactory(param -> createDateCell()); + date.setCellFactory(new DateTimeCellFactory()); date.setPrefWidth(200); TableColumn status = new TableColumn<>("Status"); status.setCellValueFactory(cdf -> cdf.getValue().getStatusProperty()); @@ -193,23 +190,6 @@ public class RecordingsTab extends Tab implements TabSelectionListener { return cell; } - private TableCell createDateCell() { - TableCell cell = new TableCell() { - @Override - protected void updateItem(Instant instant, boolean empty) { - if(empty || instant == null) { - setText(null); - } else { - ZonedDateTime time = instant.atZone(ZoneId.systemDefault()); - DateTimeFormatter dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM); - setText(dtf.format(time)); - } - } - }; - return cell; - } - - private void onContextMenuRequested(ContextMenuEvent event) { List recordings = table.getSelectionModel().getSelectedItems(); if (recordings != null && !recordings.isEmpty()) { diff --git a/client/src/main/java/ctbrec/ui/controls/DateTimeCellFactory.java b/client/src/main/java/ctbrec/ui/controls/DateTimeCellFactory.java new file mode 100644 index 00000000..97169c48 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/DateTimeCellFactory.java @@ -0,0 +1,30 @@ +package ctbrec.ui.controls; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; + +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.util.Callback; + +public class DateTimeCellFactory implements Callback, TableCell> { + @Override + public TableCell call(TableColumn param) { + return new TableCell() { + @Override + protected void updateItem(Instant item, boolean empty) { + if (empty || item == null) { + setText(""); + } else { + LocalDateTime dateTime = LocalDateTime.ofInstant(item, ZoneId.systemDefault()); + DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT); + String formattedDateTime = formatter.format(dateTime); + setText(item.equals(Instant.EPOCH) ? "" : formattedDateTime); + } + } + }; + } +} diff --git a/common/src/main/java/ctbrec/AbstractModel.java b/common/src/main/java/ctbrec/AbstractModel.java index 67658ff5..efee77ad 100644 --- a/common/src/main/java/ctbrec/AbstractModel.java +++ b/common/src/main/java/ctbrec/AbstractModel.java @@ -1,6 +1,7 @@ package ctbrec; import java.io.IOException; +import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -27,6 +28,8 @@ public abstract class AbstractModel implements Model { private boolean suspended = false; protected transient Site site; protected State onlineState = State.UNKNOWN; + private Instant lastSeen; + private Instant lastRecorded; @Override public boolean isOnline() throws IOException, ExecutionException, InterruptedException { @@ -205,6 +208,26 @@ public abstract class AbstractModel implements Model { this.priority = priority; } + @Override + public Instant getLastSeen() { + return Optional.ofNullable(lastSeen).orElse(Instant.EPOCH); + } + + @Override + public void setLastSeen(Instant lastSeen) { + this.lastSeen = lastSeen; + } + + @Override + public Instant getLastRecorded() { + return Optional.ofNullable(lastRecorded).orElse(Instant.EPOCH); + } + + @Override + public void setLastRecorded(Instant lastRecorded) { + this.lastRecorded = lastRecorded; + } + @Override public Download createDownload() { if(Config.isServerMode()) { diff --git a/common/src/main/java/ctbrec/Model.java b/common/src/main/java/ctbrec/Model.java index c9f6b587..7d0133e6 100644 --- a/common/src/main/java/ctbrec/Model.java +++ b/common/src/main/java/ctbrec/Model.java @@ -2,6 +2,7 @@ package ctbrec; import java.io.IOException; import java.io.Serializable; +import java.time.Instant; import java.util.List; import java.util.concurrent.ExecutionException; @@ -82,6 +83,14 @@ public interface Model extends Comparable, Serializable { public void receiveTip(Double tokens) throws IOException; + public void setLastSeen(Instant timestamp); + + public Instant getLastSeen(); + + public void setLastRecorded(Instant timestamp); + + public Instant getLastRecorded(); + /** * Determines the stream resolution for this model * diff --git a/common/src/main/java/ctbrec/io/ModelJsonAdapter.java b/common/src/main/java/ctbrec/io/ModelJsonAdapter.java index f408c906..6551b94e 100644 --- a/common/src/main/java/ctbrec/io/ModelJsonAdapter.java +++ b/common/src/main/java/ctbrec/io/ModelJsonAdapter.java @@ -2,6 +2,7 @@ package ctbrec.io; import java.io.IOException; import java.lang.reflect.InvocationTargetException; +import java.time.Instant; import java.util.List; import java.util.Optional; @@ -69,6 +70,10 @@ public class ModelJsonAdapter extends JsonAdapter { } else if(key.equals("suspended")) { suspended = reader.nextBoolean(); model.setSuspended(suspended); + } else if(key.equals("lastSeen")) { + model.setLastSeen(Instant.ofEpochMilli(reader.nextLong())); + } else if(key.equals("lastRecorded")) { + model.setLastRecorded(Instant.ofEpochMilli(reader.nextLong())); } else if(key.equals("siteSpecific")) { reader.beginObject(); try { @@ -108,6 +113,8 @@ public class ModelJsonAdapter extends JsonAdapter { writer.name("priority").value(model.getPriority()); writer.name("streamUrlIndex").value(model.getStreamUrlIndex()); writer.name("suspended").value(model.isSuspended()); + writer.name("lastSeen").value(model.getLastSeen().toEpochMilli()); + writer.name("lastRecorded").value(model.getLastRecorded().toEpochMilli()); writer.name("siteSpecific"); writer.beginObject(); model.writeSiteSpecificData(writer); diff --git a/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java b/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java index a9ff35bd..6554b8df 100644 --- a/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java @@ -273,6 +273,7 @@ public class NextGenLocalRecorder implements Recorder { completionService.submit(() -> { try { setRecordingStatus(rec, State.RECORDING); + model.setLastRecorded(rec.getStartDate()); recordingManager.saveRecording(rec); download.start(); } catch (IOException e) { diff --git a/common/src/main/java/ctbrec/recorder/OnlineMonitor.java b/common/src/main/java/ctbrec/recorder/OnlineMonitor.java index e651c05f..6d633d61 100644 --- a/common/src/main/java/ctbrec/recorder/OnlineMonitor.java +++ b/common/src/main/java/ctbrec/recorder/OnlineMonitor.java @@ -108,6 +108,7 @@ public class OnlineMonitor extends Thread { try { if (model.isOnline(IGNORE_CACHE)) { EventBusHolder.BUS.post(new ModelIsOnlineEvent(model)); + model.setLastSeen(Instant.now()); } Model.State state = model.getOnlineState(false); LOG.trace("Model online state: {} {}", model.getName(), state); diff --git a/common/src/main/java/ctbrec/recorder/download/hls/HlsDownload.java b/common/src/main/java/ctbrec/recorder/download/hls/HlsDownload.java index 9630e599..bacbde4d 100644 --- a/common/src/main/java/ctbrec/recorder/download/hls/HlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/hls/HlsDownload.java @@ -177,6 +177,12 @@ public class HlsDownload extends AbstractHlsDownload { } catch (Exception e) { throw new IOException("Couldn't download segment", e); } finally { + try { + Thread.sleep(10_000); + } catch (InterruptedException e1) { + // TODO Auto-generated catch block + e1.printStackTrace(); + } downloadThreadPool.shutdown(); try { LOG.debug("Waiting for last segments for {}", model);