Add "record later" tab to "bookmark" models
This commit is contained in:
parent
c462aefd4f
commit
d43e022563
|
@ -1,5 +1,6 @@
|
|||
NEXT
|
||||
========================
|
||||
* Added "record later" tab to "bookmark" models
|
||||
* Added config option to show the total number of models in the title bar
|
||||
|
||||
3.11.0
|
||||
|
|
|
@ -64,7 +64,7 @@ import ctbrec.ui.news.NewsTab;
|
|||
import ctbrec.ui.settings.SettingsTab;
|
||||
import ctbrec.ui.tabs.DonateTabFx;
|
||||
import ctbrec.ui.tabs.HelpTab;
|
||||
import ctbrec.ui.tabs.RecordedModelsTab;
|
||||
import ctbrec.ui.tabs.RecordedTab;
|
||||
import ctbrec.ui.tabs.RecordingsTab;
|
||||
import ctbrec.ui.tabs.SiteTab;
|
||||
import ctbrec.ui.tabs.TabSelectionListener;
|
||||
|
@ -105,7 +105,7 @@ public class CamrecApplication extends Application {
|
|||
public static HttpClient httpClient;
|
||||
public static String title;
|
||||
private Stage primaryStage;
|
||||
private RecordedModelsTab modelsTab;
|
||||
private RecordedTab modelsTab;
|
||||
private RecordingsTab recordingsTab;
|
||||
private ScheduledExecutorService scheduler;
|
||||
private int activeRecordings = 0;
|
||||
|
@ -213,7 +213,7 @@ public class CamrecApplication extends Application {
|
|||
}
|
||||
}
|
||||
|
||||
modelsTab = new RecordedModelsTab("Recording", recorder, sites);
|
||||
modelsTab = new RecordedTab(recorder, sites);
|
||||
tabPane.getTabs().add(modelsTab);
|
||||
recordingsTab = new RecordingsTab("Recordings", recorder, config);
|
||||
tabPane.getTabs().add(recordingsTab);
|
||||
|
|
|
@ -319,4 +319,16 @@ public class JavaFxModel implements Model {
|
|||
public boolean isRecordingTimeLimited() {
|
||||
return delegate.isRecordingTimeLimited();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMarkedForLaterRecording() {
|
||||
return delegate.isMarkedForLaterRecording();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMarkedForLaterRecording(boolean marked) {
|
||||
delegate.setMarkedForLaterRecording(marked);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ import java.io.IOException;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -27,13 +29,15 @@ public class CheckModelAccountAction {
|
|||
|
||||
}
|
||||
|
||||
public void execute() {
|
||||
public void execute(Predicate<Model> filter) {
|
||||
String buttonText = b.getText();
|
||||
b.setDisable(true);
|
||||
CompletableFuture.runAsync(() -> {
|
||||
List<Model> deletedAccounts = new ArrayList<>();
|
||||
try {
|
||||
List<Model> models = recorder.getModels();
|
||||
List<Model> models = recorder.getModels().stream() //
|
||||
.filter(filter) //
|
||||
.collect(Collectors.toList());
|
||||
int total = models.size();
|
||||
for (int i = 0; i < total; i++) {
|
||||
final int counter = i+1;
|
||||
|
|
|
@ -0,0 +1,633 @@
|
|||
package ctbrec.ui.tabs;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.StringUtil;
|
||||
import ctbrec.recorder.Recorder;
|
||||
import ctbrec.sites.Site;
|
||||
import ctbrec.ui.AutosizeAlert;
|
||||
import ctbrec.ui.DesktopIntegration;
|
||||
import ctbrec.ui.JavaFxModel;
|
||||
import ctbrec.ui.PreviewPopupHandler;
|
||||
import ctbrec.ui.action.CheckModelAccountAction;
|
||||
import ctbrec.ui.action.EditNotesAction;
|
||||
import ctbrec.ui.action.FollowAction;
|
||||
import ctbrec.ui.action.IgnoreModelsAction;
|
||||
import ctbrec.ui.action.PlayAction;
|
||||
import ctbrec.ui.action.ResumeAction;
|
||||
import ctbrec.ui.action.StopRecordingAction;
|
||||
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
|
||||
import ctbrec.ui.controls.Dialogs;
|
||||
import ctbrec.ui.controls.SearchBox;
|
||||
import ctbrec.ui.controls.autocomplete.AutoFillTextField;
|
||||
import ctbrec.ui.controls.autocomplete.ObservableListSuggester;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringPropertyBase;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.concurrent.WorkerStateEvent;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.SelectionMode;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.TableCell;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableColumn.SortType;
|
||||
import javafx.scene.control.TableRow;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.control.cell.PropertyValueFactory;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.input.ContextMenuEvent;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.Duration;
|
||||
|
||||
public class RecordLaterTab extends Tab implements TabSelectionListener {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(RecordLaterTab.class);
|
||||
|
||||
private ReentrantLock lock = new ReentrantLock();
|
||||
private ScheduledService<List<JavaFxModel>> updateService;
|
||||
private Recorder recorder;
|
||||
private List<Site> sites;
|
||||
|
||||
FlowPane grid = new FlowPane();
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
TableView<JavaFxModel> table = new TableView<>();
|
||||
ObservableList<JavaFxModel> observableModels = FXCollections.observableArrayList();
|
||||
ObservableList<JavaFxModel> filteredModels = FXCollections.observableArrayList();
|
||||
ContextMenu popup;
|
||||
|
||||
Label modelLabel = new Label("Model");
|
||||
AutoFillTextField model;
|
||||
Button addModelButton = new Button("Record");
|
||||
Button checkModelAccountExistance = new Button("Check URLs");
|
||||
TextField filter;
|
||||
|
||||
public RecordLaterTab(String title, Recorder recorder, List<Site> sites) {
|
||||
super(title);
|
||||
this.recorder = recorder;
|
||||
this.sites = sites;
|
||||
createGui();
|
||||
setClosable(false);
|
||||
initializeUpdateService();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void createGui() {
|
||||
grid.setPadding(new Insets(5));
|
||||
grid.setHgap(5);
|
||||
grid.setVgap(5);
|
||||
|
||||
scrollPane.setContent(grid);
|
||||
scrollPane.setFitToHeight(true);
|
||||
scrollPane.setFitToWidth(true);
|
||||
BorderPane.setMargin(scrollPane, new Insets(5));
|
||||
|
||||
table.setEditable(true);
|
||||
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
PreviewPopupHandler previewPopupHandler = new PreviewPopupHandler(table);
|
||||
table.setRowFactory(tableview -> {
|
||||
TableRow<JavaFxModel> row = new TableRow<>();
|
||||
row.addEventHandler(MouseEvent.ANY, previewPopupHandler);
|
||||
return row;
|
||||
});
|
||||
TableColumn<JavaFxModel, String> preview = new TableColumn<>("🎥");
|
||||
preview.setPrefWidth(35);
|
||||
preview.setCellValueFactory(cdf -> new SimpleStringProperty(" ▶ "));
|
||||
preview.setEditable(false);
|
||||
preview.setId("preview");
|
||||
if (!Config.getInstance().getSettings().livePreviews) {
|
||||
preview.setVisible(false);
|
||||
}
|
||||
TableColumn<JavaFxModel, String> name = new TableColumn<>("Model");
|
||||
name.setPrefWidth(200);
|
||||
name.setCellValueFactory(new PropertyValueFactory<>("displayName"));
|
||||
name.setCellFactory(new ClickableCellFactory<>());
|
||||
name.setEditable(false);
|
||||
name.setId("name");
|
||||
TableColumn<JavaFxModel, String> url = new TableColumn<>("URL");
|
||||
url.setCellValueFactory(new PropertyValueFactory<>("url"));
|
||||
url.setCellFactory(new ClickableCellFactory<>());
|
||||
url.setPrefWidth(400);
|
||||
url.setEditable(false);
|
||||
url.setId("url");
|
||||
TableColumn<JavaFxModel, String> notes = new TableColumn<>("Notes");
|
||||
notes.setCellValueFactory(cdf -> {
|
||||
JavaFxModel m = cdf.getValue();
|
||||
return new StringPropertyBase() {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Model Notes";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String get() {
|
||||
String modelNotes = Config.getInstance().getModelNotes(m);
|
||||
return modelNotes;
|
||||
}
|
||||
};
|
||||
});
|
||||
notes.setPrefWidth(400);
|
||||
notes.setEditable(false);
|
||||
notes.setId("notes");
|
||||
table.getColumns().addAll(preview, name, url, notes);
|
||||
table.setItems(observableModels);
|
||||
table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
|
||||
popup = createContextMenu();
|
||||
if (popup != null) {
|
||||
popup.show(table, event.getScreenX(), event.getScreenY());
|
||||
}
|
||||
event.consume();
|
||||
});
|
||||
table.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
|
||||
if (popup != null) {
|
||||
popup.hide();
|
||||
}
|
||||
});
|
||||
table.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
|
||||
List<JavaFxModel> selectedModels = table.getSelectionModel().getSelectedItems();
|
||||
if (event.getCode() == KeyCode.DELETE) {
|
||||
stopAction(selectedModels);
|
||||
} else {
|
||||
jumpToNextModel(event.getCode());
|
||||
}
|
||||
});
|
||||
|
||||
scrollPane.setContent(table);
|
||||
|
||||
HBox addModelBox = new HBox(5);
|
||||
modelLabel.setPadding(new Insets(5, 0, 0, 0));
|
||||
ObservableList<String> suggestions = FXCollections.observableArrayList();
|
||||
sites.forEach(site -> suggestions.add(site.getClass().getSimpleName()));
|
||||
model = new AutoFillTextField(new ObservableListSuggester(suggestions));
|
||||
model.minWidth(150);
|
||||
model.prefWidth(600);
|
||||
HBox.setHgrow(model, Priority.ALWAYS);
|
||||
model.setPromptText("e.g. MyFreeCams:ModelName or an URL like https://chaturbate.com/modelname/");
|
||||
model.onActionHandler(this::addModel);
|
||||
model.setTooltip(new Tooltip("To add a model enter SiteName:ModelName\n" + "press ENTER to confirm a suggested site name"));
|
||||
BorderPane.setMargin(addModelBox, new Insets(5));
|
||||
addModelButton.setOnAction(this::addModel);
|
||||
addModelButton.setPadding(new Insets(5));
|
||||
addModelBox.getChildren().addAll(modelLabel, model, addModelButton, checkModelAccountExistance);
|
||||
checkModelAccountExistance.setPadding(new Insets(5));
|
||||
checkModelAccountExistance.setTooltip(new Tooltip("Go over all model URLs and check, if the account still exists"));
|
||||
HBox.setMargin(checkModelAccountExistance, new Insets(0, 0, 0, 20));
|
||||
checkModelAccountExistance.setOnAction(evt -> new CheckModelAccountAction(checkModelAccountExistance, recorder)
|
||||
.execute(Model::isMarkedForLaterRecording));
|
||||
|
||||
HBox filterContainer = new HBox();
|
||||
filterContainer.setSpacing(0);
|
||||
filterContainer.setPadding(new Insets(0));
|
||||
filterContainer.setAlignment(Pos.CENTER_RIGHT);
|
||||
filterContainer.minWidth(100);
|
||||
filterContainer.prefWidth(150);
|
||||
HBox.setHgrow(filterContainer, Priority.ALWAYS);
|
||||
filter = new SearchBox(false);
|
||||
filter.minWidth(100);
|
||||
filter.prefWidth(150);
|
||||
filter.setPromptText("Filter");
|
||||
filter.textProperty().addListener((observableValue, oldValue, newValue) -> {
|
||||
String q = filter.getText();
|
||||
lock.lock();
|
||||
try {
|
||||
filter(q);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
});
|
||||
filter.getStyleClass().remove("search-box-icon");
|
||||
filterContainer.getChildren().add(filter);
|
||||
addModelBox.getChildren().add(filterContainer);
|
||||
|
||||
BorderPane root = new BorderPane();
|
||||
root.setPadding(new Insets(5));
|
||||
root.setTop(addModelBox);
|
||||
root.setCenter(scrollPane);
|
||||
setContent(root);
|
||||
|
||||
restoreState();
|
||||
}
|
||||
|
||||
private void jumpToNextModel(KeyCode code) {
|
||||
if (!table.getItems().isEmpty() && (code.isLetterKey() || code.isDigitKey())) {
|
||||
// determine where to start looking for the next model
|
||||
int startAt = 0;
|
||||
if (table.getSelectionModel().getSelectedIndex() >= 0) {
|
||||
startAt = table.getSelectionModel().getSelectedIndex() + 1;
|
||||
if (startAt >= table.getItems().size()) {
|
||||
startAt = 0;
|
||||
}
|
||||
}
|
||||
|
||||
String c = code.getChar().toLowerCase();
|
||||
int i = startAt;
|
||||
do {
|
||||
JavaFxModel current = table.getItems().get(i);
|
||||
if (current.getName().toLowerCase().replaceAll("[^0-9a-z]", "").startsWith(c)) {
|
||||
table.getSelectionModel().clearAndSelect(i);
|
||||
table.scrollTo(i);
|
||||
break;
|
||||
}
|
||||
|
||||
i++;
|
||||
if (i >= table.getItems().size()) {
|
||||
i = 0;
|
||||
}
|
||||
} while (i != startAt);
|
||||
}
|
||||
}
|
||||
|
||||
private void addModel(ActionEvent e) {
|
||||
String input = model.getText();
|
||||
if (StringUtil.isBlank(input)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.startsWith("http")) {
|
||||
addModelByUrl(input);
|
||||
} else {
|
||||
addModelByName(input);
|
||||
}
|
||||
}
|
||||
|
||||
private void addModelByUrl(String url) {
|
||||
for (Site site : sites) {
|
||||
Model newModel = site.createModelFromUrl(url);
|
||||
if (newModel != null) {
|
||||
try {
|
||||
newModel.setMarkedForLaterRecording(true);
|
||||
recorder.startRecording(newModel);
|
||||
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
|
||||
Dialogs.showError(getTabPane().getScene(), "Couldn't add model", "The model " + newModel.getName() + " could not be added: ", e1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Dialogs.showError(getTabPane().getScene(), "Unknown URL format",
|
||||
"The URL you entered has an unknown format or the function does not support this site, yet", null);
|
||||
}
|
||||
|
||||
private void addModelByName(String siteModelCombo) {
|
||||
String[] parts = siteModelCombo.trim().split(":");
|
||||
if (parts.length != 2) {
|
||||
Dialogs.showError(getTabPane().getScene(), "Wrong input format", "Use something like \"MyFreeCams:ModelName\"", null);
|
||||
return;
|
||||
}
|
||||
|
||||
String siteName = parts[0];
|
||||
String modelName = parts[1];
|
||||
for (Site site : sites) {
|
||||
if (Objects.equals(siteName.toLowerCase(), site.getClass().getSimpleName().toLowerCase())) {
|
||||
try {
|
||||
Model m = site.createModel(modelName);
|
||||
m.setMarkedForLaterRecording(true);
|
||||
recorder.startRecording(m);
|
||||
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
|
||||
Dialogs.showError(getTabPane().getScene(), "Couldn't add model", "The model " + modelName + " could not be added:", e1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR, getTabPane().getScene());
|
||||
alert.setTitle("Unknown site");
|
||||
alert.setHeaderText("Couldn't add model");
|
||||
alert.setContentText("The site you entered is unknown");
|
||||
alert.showAndWait();
|
||||
}
|
||||
|
||||
void initializeUpdateService() {
|
||||
updateService = createUpdateService();
|
||||
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(2)));
|
||||
updateService.setOnSucceeded(this::onUpdateSuccess);
|
||||
updateService.setOnFailed(event -> LOG.info("Couldn't get list of models from recorder", event.getSource().getException()));
|
||||
}
|
||||
|
||||
private void onUpdateSuccess(WorkerStateEvent event) {
|
||||
List<JavaFxModel> updatedModels = updateService.getValue();
|
||||
if (updatedModels == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
lock.lock();
|
||||
try {
|
||||
addOrUpdateModels(updatedModels);
|
||||
|
||||
// remove old ones, which are not in the list of updated models
|
||||
for (Iterator<JavaFxModel> iterator = observableModels.iterator(); iterator.hasNext();) {
|
||||
Model oldModel = iterator.next();
|
||||
if (!updatedModels.contains(oldModel)) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
filteredModels.clear();
|
||||
filter(filter.getText());
|
||||
table.sort();
|
||||
}
|
||||
|
||||
private void addOrUpdateModels(List<JavaFxModel> updatedModels) {
|
||||
for (JavaFxModel updatedModel : updatedModels) {
|
||||
int index = observableModels.indexOf(updatedModel);
|
||||
if (index == -1) {
|
||||
observableModels.add(updatedModel);
|
||||
} else {
|
||||
// make sure to update the JavaFX online property, so that the table cell is updated
|
||||
JavaFxModel oldModel = observableModels.get(index);
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void filter(String filter) {
|
||||
lock.lock();
|
||||
try {
|
||||
if (StringUtil.isBlank(filter)) {
|
||||
observableModels.addAll(filteredModels);
|
||||
filteredModels.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
String[] tokens = filter.split(" ");
|
||||
observableModels.addAll(filteredModels);
|
||||
filteredModels.clear();
|
||||
for (int i = 0; i < table.getItems().size(); i++) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (TableColumn<JavaFxModel, ?> tc : table.getColumns()) {
|
||||
Object cellData = tc.getCellData(i);
|
||||
if (cellData != null) {
|
||||
String content = cellData.toString();
|
||||
sb.append(content).append(' ');
|
||||
}
|
||||
}
|
||||
String searchText = sb.toString();
|
||||
|
||||
boolean tokensMissing = false;
|
||||
for (String token : tokens) {
|
||||
if (!searchText.toLowerCase().contains(token.toLowerCase())) {
|
||||
tokensMissing = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (tokensMissing) {
|
||||
JavaFxModel filteredModel = table.getItems().get(i);
|
||||
filteredModels.add(filteredModel);
|
||||
}
|
||||
}
|
||||
observableModels.removeAll(filteredModels);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private ScheduledService<List<JavaFxModel>> createUpdateService() {
|
||||
ScheduledService<List<JavaFxModel>> modelUpdateService = new ScheduledService<List<JavaFxModel>>() {
|
||||
@Override
|
||||
protected Task<List<JavaFxModel>> createTask() {
|
||||
return new Task<List<JavaFxModel>>() {
|
||||
@Override
|
||||
public List<JavaFxModel> call() throws InvalidKeyException, NoSuchAlgorithmException, IOException {
|
||||
LOG.trace("Updating models marked for later recording");
|
||||
return recorder.getModels().stream().filter(Model::isMarkedForLaterRecording).map(JavaFxModel::new).collect(Collectors.toList());
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
|
||||
Thread t = new Thread(r);
|
||||
t.setDaemon(true);
|
||||
t.setName("RecordLaterTab UpdateService");
|
||||
return t;
|
||||
});
|
||||
modelUpdateService.setExecutor(executor);
|
||||
return modelUpdateService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void selected() {
|
||||
if (updateService != null) {
|
||||
updateService.reset();
|
||||
updateService.restart();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deselected() {
|
||||
if (updateService != null) {
|
||||
updateService.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private ContextMenu createContextMenu() {
|
||||
ObservableList<JavaFxModel> selectedModels = table.getSelectionModel().getSelectedItems();
|
||||
if (selectedModels.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
MenuItem start = new MenuItem("Start Recording");
|
||||
start.setOnAction(e -> startAction(selectedModels));
|
||||
MenuItem stop = new MenuItem("Remove Model");
|
||||
stop.setOnAction(e -> stopAction(selectedModels));
|
||||
|
||||
MenuItem copyUrl = new MenuItem("Copy URL");
|
||||
copyUrl.setOnAction(e -> {
|
||||
Model selected = selectedModels.get(0);
|
||||
final Clipboard clipboard = Clipboard.getSystemClipboard();
|
||||
final ClipboardContent content = new ClipboardContent();
|
||||
content.putString(selected.getUrl());
|
||||
clipboard.setContent(content);
|
||||
});
|
||||
|
||||
MenuItem openInBrowser = new MenuItem("Open in Browser");
|
||||
openInBrowser.setOnAction(e -> DesktopIntegration.open(selectedModels.get(0).getUrl()));
|
||||
MenuItem openInPlayer = new MenuItem("Open in Player");
|
||||
openInPlayer.setOnAction(e -> openInPlayer(selectedModels.get(0)));
|
||||
MenuItem follow = new MenuItem("Follow");
|
||||
follow.setOnAction(e -> follow(selectedModels));
|
||||
follow.setDisable(!selectedModels.stream().allMatch(m -> m.getSite().supportsFollow() && m.getSite().credentialsAvailable()));
|
||||
MenuItem ignore = new MenuItem("Ignore");
|
||||
ignore.setOnAction(e -> ignore(selectedModels));
|
||||
MenuItem notes = new MenuItem("Notes");
|
||||
notes.setOnAction(e -> notes(selectedModels));
|
||||
|
||||
ContextMenu menu = new CustomMouseBehaviorContextMenu(start, stop, copyUrl, openInPlayer, openInBrowser, follow, notes, ignore);
|
||||
|
||||
if (selectedModels.size() > 1) {
|
||||
copyUrl.setDisable(true);
|
||||
openInPlayer.setDisable(true);
|
||||
openInBrowser.setDisable(true);
|
||||
notes.setDisable(true);
|
||||
}
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
private void ignore(ObservableList<JavaFxModel> selectedModels) {
|
||||
new IgnoreModelsAction(table, selectedModels, recorder, true).execute();
|
||||
}
|
||||
|
||||
private void follow(ObservableList<JavaFxModel> selectedModels) {
|
||||
new FollowAction(getTabPane(), new ArrayList<>(selectedModels)).execute();
|
||||
}
|
||||
|
||||
private void notes(ObservableList<JavaFxModel> selectedModels) {
|
||||
new EditNotesAction(getTabPane(), selectedModels.get(0), table).execute();
|
||||
}
|
||||
|
||||
private void openInPlayer(JavaFxModel selectedModel) {
|
||||
new PlayAction(getTabPane(), selectedModel).execute();
|
||||
}
|
||||
|
||||
private void startAction(List<JavaFxModel> selectedModels) {
|
||||
List<Model> models = selectedModels.stream().map(JavaFxModel::getDelegate).collect(Collectors.toList());
|
||||
new ResumeAction(table, models, recorder).execute();
|
||||
}
|
||||
|
||||
private void stopAction(List<JavaFxModel> selectedModels) {
|
||||
boolean confirmed = true;
|
||||
if (Config.getInstance().getSettings().confirmationForDangerousActions) {
|
||||
int n = selectedModels.size();
|
||||
String plural = n > 1 ? "s" : "";
|
||||
String header = "This will remove " + n + " model" + plural;
|
||||
confirmed = Dialogs.showConfirmDialog("Remove From List", "Continue?", header, table.getScene());
|
||||
}
|
||||
if (confirmed) {
|
||||
List<Model> models = selectedModels.stream().map(JavaFxModel::getDelegate).collect(Collectors.toList());
|
||||
new StopRecordingAction(getTabPane(), models, recorder).execute(m -> Platform.runLater(() -> {
|
||||
table.getSelectionModel().clearSelection(table.getItems().indexOf(m));
|
||||
table.getItems().remove(m);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
public void saveState() {
|
||||
if (!table.getSortOrder().isEmpty()) {
|
||||
TableColumn<JavaFxModel, ?> col = table.getSortOrder().get(0);
|
||||
Config.getInstance().getSettings().recordLaterSortColumn = col.getText();
|
||||
Config.getInstance().getSettings().recordLaterSortType = col.getSortType().toString();
|
||||
}
|
||||
int columns = table.getColumns().size();
|
||||
double[] columnWidths = new double[columns];
|
||||
String[] columnIds = new String[columns];
|
||||
for (int i = 0; i < columnWidths.length; i++) {
|
||||
columnWidths[i] = table.getColumns().get(i).getWidth();
|
||||
columnIds[i] = table.getColumns().get(i).getId();
|
||||
}
|
||||
Config.getInstance().getSettings().recordLaterColumnWidths = columnWidths;
|
||||
Config.getInstance().getSettings().recordLaterColumnIds = columnIds;
|
||||
}
|
||||
|
||||
private void restoreState() {
|
||||
restoreColumnOrder();
|
||||
restoreColumnWidths();
|
||||
restoreSorting();
|
||||
}
|
||||
|
||||
private void restoreSorting() {
|
||||
String sortCol = Config.getInstance().getSettings().recordLaterSortColumn;
|
||||
if (StringUtil.isNotBlank(sortCol)) {
|
||||
for (TableColumn<JavaFxModel, ?> col : table.getColumns()) {
|
||||
if (Objects.equals(sortCol, col.getText())) {
|
||||
col.setSortType(SortType.valueOf(Config.getInstance().getSettings().recordLaterSortType));
|
||||
table.getSortOrder().clear();
|
||||
table.getSortOrder().add(col);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void restoreColumnOrder() {
|
||||
String[] columnIds = Config.getInstance().getSettings().recordLaterColumnIds;
|
||||
ObservableList<TableColumn<JavaFxModel,?>> columns = table.getColumns();
|
||||
for (int i = 0; i < columnIds.length; i++) {
|
||||
for (int j = 0; j < table.getColumns().size(); j++) {
|
||||
if(Objects.equals(columnIds[i], columns.get(j).getId())) {
|
||||
TableColumn<JavaFxModel, ?> col = columns.get(j);
|
||||
columns.remove(j); // NOSONAR
|
||||
columns.add(i, col);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void restoreColumnWidths() {
|
||||
double[] columnWidths = Config.getInstance().getSettings().recordLaterColumnWidths;
|
||||
if (columnWidths != null && columnWidths.length == table.getColumns().size()) {
|
||||
for (int i = 0; i < columnWidths.length; i++) {
|
||||
table.getColumns().get(i).setPrefWidth(columnWidths[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ClickableCellFactory<S, T> implements Callback<TableColumn<S, T>, TableCell<S, T>> {
|
||||
@Override
|
||||
public TableCell<S, T> call(TableColumn<S, T> param) {
|
||||
TableCell<S, T> cell = new TableCell<>() {
|
||||
@Override
|
||||
protected void updateItem(Object item, boolean empty) {
|
||||
setText(empty ? "" : Objects.toString(item));
|
||||
}
|
||||
};
|
||||
|
||||
cell.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
|
||||
JavaFxModel selectedModel = table.getSelectionModel().getSelectedItem();
|
||||
if (selectedModel != null) {
|
||||
new PlayAction(table, selectedModel).execute();
|
||||
}
|
||||
}
|
||||
});
|
||||
return cell;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ import java.util.concurrent.Executors;
|
|||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
@ -49,6 +50,7 @@ import ctbrec.ui.action.PlayAction;
|
|||
import ctbrec.ui.action.RemoveTimeLimitAction;
|
||||
import ctbrec.ui.action.ResumeAction;
|
||||
import ctbrec.ui.action.SetStopDateAction;
|
||||
import ctbrec.ui.action.StartRecordingAction;
|
||||
import ctbrec.ui.action.StopRecordingAction;
|
||||
import ctbrec.ui.action.ToggleRecordingAction;
|
||||
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
|
||||
|
@ -297,7 +299,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
checkModelAccountExistance.setPadding(new Insets(5));
|
||||
checkModelAccountExistance.setTooltip(new Tooltip("Go over all model URLs and check, if the account still exists"));
|
||||
HBox.setMargin(checkModelAccountExistance, new Insets(0, 0, 0, 20));
|
||||
checkModelAccountExistance.setOnAction(evt -> new CheckModelAccountAction(checkModelAccountExistance, recorder).execute());
|
||||
checkModelAccountExistance.setOnAction(evt -> new CheckModelAccountAction(checkModelAccountExistance, recorder)
|
||||
.execute(Predicate.not(Model::isMarkedForLaterRecording)));
|
||||
|
||||
HBox filterContainer = new HBox();
|
||||
filterContainer.setSpacing(0);
|
||||
|
@ -584,6 +587,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
List<Model> onlineModels = recorder.getOnlineModels();
|
||||
return recorder.getModels()
|
||||
.stream()
|
||||
.filter(Predicate.not(Model::isMarkedForLaterRecording))
|
||||
.map(JavaFxModel::new)
|
||||
.peek(fxm -> { // NOSONAR
|
||||
for (Recording recording : recordings) {
|
||||
|
@ -641,6 +645,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
MenuItem stop = new MenuItem("Remove Model");
|
||||
stop.setOnAction(e -> stopAction(selectedModels));
|
||||
MenuItem recordLater = new MenuItem("Record Later");
|
||||
recordLater.setOnAction(e -> recordLater(selectedModels));
|
||||
|
||||
MenuItem copyUrl = new MenuItem("Copy URL");
|
||||
copyUrl.setOnAction(e -> {
|
||||
|
@ -675,7 +681,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
MenuItem openRecDir = new MenuItem("Open recording directory");
|
||||
openRecDir.setOnAction(e -> new OpenRecordingsDir(table, selectedModels.get(0)).execute());
|
||||
|
||||
ContextMenu menu = new CustomMouseBehaviorContextMenu(stop);
|
||||
ContextMenu menu = new CustomMouseBehaviorContextMenu(stop, recordLater);
|
||||
if (selectedModels.size() == 1) {
|
||||
menu.getItems().add(selectedModels.get(0).isSuspended() ? resumeRecording : pauseRecording);
|
||||
menu.getItems().add(stopRecordingAt);
|
||||
|
@ -772,7 +778,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
alert.showAndWait();
|
||||
}
|
||||
|
||||
private void stopAction(List<JavaFxModel> selectedModels) {
|
||||
private boolean stopAction(List<JavaFxModel> selectedModels) {
|
||||
boolean confirmed = true;
|
||||
if (Config.getInstance().getSettings().confirmationForDangerousActions) {
|
||||
int n = selectedModels.size();
|
||||
|
@ -787,6 +793,21 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
table.getItems().remove(m);
|
||||
}));
|
||||
}
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
private void recordLater(List<JavaFxModel> selectedModels) {
|
||||
boolean confirmed = stopAction(selectedModels);
|
||||
if (confirmed) {
|
||||
List<Model> models = selectedModels.stream()
|
||||
.map(JavaFxModel::getDelegate)
|
||||
.map(m -> {
|
||||
m.setMarkedForLaterRecording(true);
|
||||
return m;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
new StartRecordingAction(table, models, recorder).execute();
|
||||
}
|
||||
}
|
||||
|
||||
private void pauseRecording(List<JavaFxModel> selectedModels) {
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
package ctbrec.ui.tabs;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import ctbrec.recorder.Recorder;
|
||||
import ctbrec.sites.Site;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.geometry.Side;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.TabPane;
|
||||
|
||||
public class RecordedTab extends Tab implements TabSelectionListener {
|
||||
|
||||
private TabPane tabPane;
|
||||
private RecordedModelsTab recordedModelsTab;
|
||||
private RecordLaterTab recordLaterTab;
|
||||
|
||||
public RecordedTab(Recorder recorder, List<Site> sites) {
|
||||
super("Recording");
|
||||
setClosable(false);
|
||||
|
||||
recordedModelsTab = new RecordedModelsTab("Active", recorder, sites);
|
||||
recordLaterTab = new RecordLaterTab("Later", recorder, sites);
|
||||
|
||||
tabPane = new TabPane();
|
||||
tabPane.setSide(Side.LEFT);
|
||||
tabPane.getTabs().addAll(recordedModelsTab, recordLaterTab);
|
||||
setContent(tabPane);
|
||||
|
||||
// register changelistener to activate / deactivate tabs, when the user switches between them
|
||||
tabPane.getSelectionModel().selectedItemProperty().addListener((ChangeListener<Tab>) (ov, from, to) -> {
|
||||
if (from instanceof TabSelectionListener) {
|
||||
((TabSelectionListener) from).deselected();
|
||||
}
|
||||
if (to instanceof TabSelectionListener) {
|
||||
((TabSelectionListener) to).selected();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void selected() {
|
||||
Tab selectedTab = tabPane.getSelectionModel().getSelectedItem();
|
||||
if(selectedTab instanceof TabSelectionListener) {
|
||||
((TabSelectionListener) selectedTab).selected();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deselected() {
|
||||
Tab selectedTab = tabPane.getSelectionModel().getSelectedItem();
|
||||
if(selectedTab instanceof TabSelectionListener) {
|
||||
((TabSelectionListener) selectedTab).deselected();
|
||||
}
|
||||
}
|
||||
|
||||
public void saveState() {
|
||||
recordedModelsTab.saveState();
|
||||
recordLaterTab.saveState();
|
||||
}
|
||||
}
|
|
@ -512,7 +512,7 @@ public class ThumbCell extends StackPane {
|
|||
try {
|
||||
if (start) {
|
||||
recorder.startRecording(model);
|
||||
setRecording(true);
|
||||
setRecording(!model.isMarkedForLaterRecording());
|
||||
} else {
|
||||
recorder.stopRecording(model);
|
||||
setRecording(false);
|
||||
|
@ -561,6 +561,11 @@ public class ThumbCell extends StackPane {
|
|||
});
|
||||
}
|
||||
|
||||
void recordLater() {
|
||||
model.setMarkedForLaterRecording(true);
|
||||
startStopAction(true);
|
||||
}
|
||||
|
||||
public Model getModel() {
|
||||
return model;
|
||||
}
|
||||
|
|
|
@ -488,7 +488,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
MenuItem addPaused = new MenuItem("Add in paused state");
|
||||
addPaused.setOnAction(e -> addPaused(getSelectedThumbCells(cell)));
|
||||
MenuItem recordLater = new MenuItem("Record Later");
|
||||
recordLater.setOnAction(e -> LOG.debug("Record Later not implemented, yet"));
|
||||
recordLater.setOnAction(e -> recordLater(getSelectedThumbCells(cell)));
|
||||
|
||||
MenuItem pause = new MenuItem("Pause Recording");
|
||||
pause.setOnAction(e -> pauseResumeAction(getSelectedThumbCells(cell), true));
|
||||
|
@ -523,7 +523,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
if(modelIsTrackedByRecorder) {
|
||||
contextMenu.getItems().add(pauseResume);
|
||||
} else {
|
||||
contextMenu.getItems().addAll(recordUntil, addPaused/*, recordLater*/);
|
||||
contextMenu.getItems().addAll(recordUntil, addPaused, recordLater);
|
||||
}
|
||||
contextMenu.getItems().add(new SeparatorMenuItem());
|
||||
if(site.supportsFollow()) {
|
||||
|
@ -544,6 +544,12 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
return contextMenu;
|
||||
}
|
||||
|
||||
private void recordLater(List<ThumbCell> list) {
|
||||
for (ThumbCell cell : list) {
|
||||
cell.recordLater();
|
||||
}
|
||||
}
|
||||
|
||||
private void startRecordingWithTimeLimit(List<ThumbCell> list) {
|
||||
for (ThumbCell cell : list) {
|
||||
cell.startStopAction(true);
|
||||
|
|
|
@ -33,6 +33,7 @@ public abstract class AbstractModel implements Model {
|
|||
private int streamUrlIndex = -1;
|
||||
private int priority = 50;
|
||||
private boolean suspended = false;
|
||||
private boolean markedForLaterRecording = false;
|
||||
protected transient Site site;
|
||||
protected State onlineState = State.UNKNOWN;
|
||||
private Instant lastSeen;
|
||||
|
@ -145,6 +146,16 @@ public abstract class AbstractModel implements Model {
|
|||
this.suspended = suspended;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMarkedForLaterRecording() {
|
||||
return markedForLaterRecording;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMarkedForLaterRecording(boolean marked) {
|
||||
this.markedForLaterRecording = marked;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
|
||||
return onlineState;
|
||||
|
|
|
@ -122,6 +122,10 @@ public interface Model extends Comparable<Model>, Serializable {
|
|||
|
||||
public void setSuspended(boolean suspended);
|
||||
|
||||
public boolean isMarkedForLaterRecording();
|
||||
|
||||
public void setMarkedForLaterRecording(boolean marked);
|
||||
|
||||
public Download createDownload();
|
||||
|
||||
public void setPriority(int priority);
|
||||
|
|
|
@ -118,6 +118,10 @@ public class Settings {
|
|||
public String proxyPort;
|
||||
public ProxyType proxyType = ProxyType.DIRECT;
|
||||
public String proxyUser;
|
||||
public double[] recordLaterColumnWidths = new double[0];
|
||||
public String[] recordLaterColumnIds = new String[0];
|
||||
public String recordLaterSortColumn = "";
|
||||
public String recordLaterSortType = "";
|
||||
public double[] recordedModelsColumnWidths = new double[0];
|
||||
public String[] recordedModelsColumnIds = new String[0];
|
||||
public String recordedModelsSortColumn = "";
|
||||
|
@ -128,6 +132,7 @@ public class Settings {
|
|||
public DirectoryStructure recordingsDirStructure = DirectoryStructure.FLAT;
|
||||
public String recordingsSortColumn = "";
|
||||
public String recordingsSortType = "";
|
||||
public List<Model> recordLater = new ArrayList<>();
|
||||
public boolean recordSingleFile = false;
|
||||
public boolean removeRecordingAfterPostProcessing = false;
|
||||
public boolean requireAuthentication = false;
|
||||
|
|
|
@ -35,51 +35,41 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
|
|||
@Override
|
||||
public Model fromJson(JsonReader reader) throws IOException {
|
||||
reader.beginObject();
|
||||
String name = null;
|
||||
String description = null;
|
||||
String url = null;
|
||||
Object type = null;
|
||||
int streamUrlIndex = -1;
|
||||
int priority;
|
||||
boolean suspended = false;
|
||||
|
||||
Model model = null;
|
||||
while(reader.hasNext()) {
|
||||
|
||||
while (reader.hasNext()) {
|
||||
try {
|
||||
Token token = reader.peek();
|
||||
if(token == Token.NAME) {
|
||||
if (token == Token.NAME) {
|
||||
String key = reader.nextName();
|
||||
if(key.equals("type")) {
|
||||
if (key.equals("type")) {
|
||||
type = reader.readJsonValue();
|
||||
Class<?> modelClass = Class.forName(Optional.ofNullable(type).orElse(ChaturbateModel.class.getName()).toString());
|
||||
model = (Model) modelClass.getDeclaredConstructor().newInstance();
|
||||
} else if(key.equals("name")) {
|
||||
name = reader.nextString();
|
||||
model.setName(name);
|
||||
} else if(key.equals("description")) {
|
||||
description = reader.nextString();
|
||||
model.setDescription(description);
|
||||
} else if(key.equals("url")) {
|
||||
url = reader.nextString();
|
||||
model.setUrl(url);
|
||||
} else if(key.equals("priority")) {
|
||||
priority = reader.nextInt();
|
||||
model.setPriority(priority);
|
||||
} 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("lastSeen")) {
|
||||
} else if (key.equals("name")) {
|
||||
model.setName(reader.nextString());
|
||||
} else if (key.equals("description")) {
|
||||
model.setDescription(reader.nextString());
|
||||
} else if (key.equals("url")) {
|
||||
model.setUrl(reader.nextString());
|
||||
} else if (key.equals("priority")) {
|
||||
model.setPriority(reader.nextInt());
|
||||
} else if (key.equals("streamUrlIndex")) {
|
||||
model.setStreamUrlIndex(reader.nextInt());
|
||||
} else if (key.equals("suspended")) {
|
||||
model.setSuspended(reader.nextBoolean());
|
||||
} else if (key.equals("markedForLater")) {
|
||||
model.setMarkedForLaterRecording(reader.nextBoolean());
|
||||
} else if (key.equals("lastSeen")) {
|
||||
model.setLastSeen(Instant.ofEpochMilli(reader.nextLong()));
|
||||
} else if(key.equals("lastRecorded")) {
|
||||
} else if (key.equals("lastRecorded")) {
|
||||
model.setLastRecorded(Instant.ofEpochMilli(reader.nextLong()));
|
||||
} else if(key.equals("recordUntil")) {
|
||||
} else if (key.equals("recordUntil")) {
|
||||
model.setRecordUntil(Instant.ofEpochMilli(reader.nextLong()));
|
||||
} else if(key.equals("recordUntilSubsequentAction")) {
|
||||
} else if (key.equals("recordUntilSubsequentAction")) {
|
||||
model.setRecordUntilSubsequentAction(SubsequentAction.valueOf(reader.nextString()));
|
||||
} else if(key.equals("siteSpecific")) {
|
||||
} else if (key.equals("siteSpecific")) {
|
||||
reader.beginObject();
|
||||
try {
|
||||
model.readSiteSpecificData(reader);
|
||||
|
@ -92,15 +82,16 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
|
|||
} else {
|
||||
reader.skipValue();
|
||||
}
|
||||
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) {
|
||||
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
|
||||
| NoSuchMethodException | SecurityException e) {
|
||||
throw new IOException("Couldn't instantiate model class [" + type + "]", e);
|
||||
}
|
||||
}
|
||||
reader.endObject();
|
||||
|
||||
if(sites != null) {
|
||||
if (sites != null) {
|
||||
for (Site site : sites) {
|
||||
if(site.isSiteForModel(model)) {
|
||||
if (site.isSiteForModel(model)) {
|
||||
model.setSite(site);
|
||||
}
|
||||
}
|
||||
|
@ -118,6 +109,7 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
|
|||
writer.name("priority").value(model.getPriority());
|
||||
writer.name("streamUrlIndex").value(model.getStreamUrlIndex());
|
||||
writer.name("suspended").value(model.isSuspended());
|
||||
writer.name("markedForLater").value(model.isMarkedForLaterRecording());
|
||||
writer.name("lastSeen").value(model.getLastSeen().toEpochMilli());
|
||||
writer.name("lastRecorded").value(model.getLastRecorded().toEpochMilli());
|
||||
writer.name("recordUntil").value(model.getRecordUntil().toEpochMilli());
|
||||
|
@ -130,7 +122,7 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
|
|||
}
|
||||
|
||||
private void writeValueIfSet(JsonWriter writer, String name, String value) throws IOException {
|
||||
if(value != null) {
|
||||
if (value != null) {
|
||||
writer.name(name).value(value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -215,7 +215,7 @@ public class NextGenLocalRecorder implements Recorder {
|
|||
}
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> startRecordingProcess(Model model) throws IOException {
|
||||
private CompletableFuture<Void> startRecordingProcess(Model model) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
recorderLock.lock();
|
||||
try {
|
||||
|
@ -287,7 +287,7 @@ public class NextGenLocalRecorder implements Recorder {
|
|||
Recording rec = new Recording();
|
||||
rec.setId(UUID.randomUUID().toString());
|
||||
rec.setDownload(download);
|
||||
String recordingFile = download.getPath(model).replaceAll("\\\\", "/");
|
||||
String recordingFile = download.getPath(model).replace("\\\\", "/");
|
||||
File absoluteFile = new File(config.getSettings().recordingsDir, recordingFile);
|
||||
rec.setAbsoluteFile(absoluteFile);
|
||||
rec.setModel(model);
|
||||
|
@ -367,10 +367,22 @@ public class NextGenLocalRecorder implements Recorder {
|
|||
}
|
||||
|
||||
private void stopRecordingProcesses() {
|
||||
LOG.debug("Stopping all recording processes");
|
||||
recorderLock.lock();
|
||||
try {
|
||||
for (Recording rec : recordingProcesses.values()) {
|
||||
rec.getDownload().stop();
|
||||
// make a copy to avoid ConcurrentModificationException
|
||||
List<Recording> toStop = new ArrayList<>(recordingProcesses.values());
|
||||
if (!toStop.isEmpty()) {
|
||||
ExecutorService shutdownPool = Executors.newFixedThreadPool(toStop.size());
|
||||
for (Recording rec : toStop) {
|
||||
Optional.ofNullable(rec.getDownload()).ifPresent(d -> shutdownPool.submit(d::stop));
|
||||
}
|
||||
shutdownPool.shutdown();
|
||||
try {
|
||||
shutdownPool.awaitTermination(10, TimeUnit.MINUTES);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
recorderLock.unlock();
|
||||
|
@ -381,7 +393,13 @@ public class NextGenLocalRecorder implements Recorder {
|
|||
public boolean isTracked(Model model) {
|
||||
recorderLock.lock();
|
||||
try {
|
||||
return models.contains(model);
|
||||
int index = models.indexOf(model);
|
||||
if (index >= 0) {
|
||||
Model modelFromList = models.get(index);
|
||||
return !modelFromList.isMarkedForLaterRecording();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} finally {
|
||||
recorderLock.unlock();
|
||||
}
|
||||
|
@ -409,63 +427,44 @@ public class NextGenLocalRecorder implements Recorder {
|
|||
|
||||
@Override
|
||||
public void shutdown(boolean immediately) {
|
||||
// TODO add a config flag for waitign or stopping immediately
|
||||
LOG.info("Shutting down");
|
||||
recording = false;
|
||||
|
||||
if (!immediately) {
|
||||
LOG.debug("Stopping all recording processes");
|
||||
recorderLock.lock();
|
||||
try {
|
||||
// make a copy to avoid ConcurrentModificationException
|
||||
List<Recording> toStop = new ArrayList<>(recordingProcesses.values());
|
||||
if (!toStop.isEmpty()) {
|
||||
ExecutorService shutdownPool = Executors.newFixedThreadPool(toStop.size());
|
||||
List<Future<?>> shutdownFutures = new ArrayList<>(toStop.size());
|
||||
for (Recording rec : toStop) {
|
||||
Optional.ofNullable(rec.getDownload()).ifPresent(d -> {
|
||||
shutdownFutures.add(shutdownPool.submit(() -> d.stop()));
|
||||
});
|
||||
}
|
||||
shutdownPool.shutdown();
|
||||
try {
|
||||
shutdownPool.awaitTermination(10, TimeUnit.MINUTES);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
recorderLock.unlock();
|
||||
}
|
||||
stopRecordingProcesses();
|
||||
awaitDownloadsFinish();
|
||||
shutdownThreadPools();
|
||||
}
|
||||
}
|
||||
|
||||
// wait for downloads to finish
|
||||
LOG.info("Waiting for downloads to finish");
|
||||
for (int i = 0; i < 60; i++) {
|
||||
if (!recordingProcesses.isEmpty()) {
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
LOG.error("Error while waiting for downloads to finish", e);
|
||||
}
|
||||
private void awaitDownloadsFinish() {
|
||||
LOG.info("Waiting for downloads to finish");
|
||||
for (int i = 0; i < 60; i++) {
|
||||
if (!recordingProcesses.isEmpty()) {
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
LOG.error("Error while waiting for downloads to finish", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// shutdown threadpools
|
||||
try {
|
||||
LOG.info("Shutting down download pool");
|
||||
downloadPool.shutdown();
|
||||
client.shutdown();
|
||||
downloadPool.awaitTermination(1, TimeUnit.MINUTES);
|
||||
LOG.info("Shutting down post-processing pool");
|
||||
ppPool.shutdown();
|
||||
int minutesToWait = 10;
|
||||
LOG.info("Waiting {} minutes (max) for post-processing to finish", minutesToWait);
|
||||
ppPool.awaitTermination(minutesToWait, TimeUnit.MINUTES);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
LOG.error("Error while waiting for pools to finish", e);
|
||||
}
|
||||
private void shutdownThreadPools() {
|
||||
try {
|
||||
LOG.info("Shutting down download pool");
|
||||
downloadPool.shutdown();
|
||||
client.shutdown();
|
||||
downloadPool.awaitTermination(1, TimeUnit.MINUTES);
|
||||
LOG.info("Shutting down post-processing pool");
|
||||
ppPool.shutdown();
|
||||
int minutesToWait = 10;
|
||||
LOG.info("Waiting {} minutes (max) for post-processing to finish", minutesToWait);
|
||||
ppPool.awaitTermination(minutesToWait, TimeUnit.MINUTES);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
LOG.error("Error while waiting for pools to finish", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -500,7 +499,9 @@ public class NextGenLocalRecorder implements Recorder {
|
|||
int index = models.indexOf(model);
|
||||
Model m = models.get(index);
|
||||
m.setSuspended(false);
|
||||
m.setMarkedForLaterRecording(false);
|
||||
model.setSuspended(false);
|
||||
model.setMarkedForLaterRecording(false);
|
||||
config.save();
|
||||
startRecordingProcess(m);
|
||||
} else {
|
||||
|
@ -651,7 +652,6 @@ public class NextGenLocalRecorder implements Recorder {
|
|||
config.save();
|
||||
} else {
|
||||
LOG.warn("Couldn't change priority for model {}. Not found in list", model.getName());
|
||||
return;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error("Couldn't save config", e);
|
||||
|
|
|
@ -82,7 +82,9 @@ public class OnlineMonitor extends Thread {
|
|||
// submit online check jobs to the executor for the model's site
|
||||
List<Future<?>> futures = new LinkedList<>();
|
||||
for (Model model : models) {
|
||||
if (config.getSettings().onlineCheckSkipsPausedModels && model.isSuspended()) {
|
||||
boolean skipCheckForSuspended = config.getSettings().onlineCheckSkipsPausedModels && model.isSuspended();
|
||||
boolean skipCheckForMarkedAsLater = model.isMarkedForLaterRecording();
|
||||
if (skipCheckForSuspended || skipCheckForMarkedAsLater) {
|
||||
continue;
|
||||
} else {
|
||||
futures.add(updateModel(model));
|
||||
|
|
|
@ -29,6 +29,7 @@ public class RecordingPreconditions {
|
|||
void check(Model model) throws IOException {
|
||||
ensureRecorderIsActive();
|
||||
ensureModelIsNotSuspended(model);
|
||||
ensureModelIsNotMarkedForLaterRecording(model);
|
||||
ensureRecordUntilIsInFuture(model);
|
||||
ensureNoRecordingRunningForModel(model);
|
||||
ensureModelShouldBeRecorded(model);
|
||||
|
@ -113,6 +114,12 @@ public class RecordingPreconditions {
|
|||
}
|
||||
}
|
||||
|
||||
private void ensureModelIsNotMarkedForLaterRecording(Model model) {
|
||||
if (model.isMarkedForLaterRecording()) {
|
||||
throw new PreconditionNotMetException("Model " + model + " is marked for later recording");
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureRecorderIsActive() {
|
||||
if (!recorder.isRecording()) {
|
||||
throw new PreconditionNotMetException("Recorder is not in recording mode");
|
||||
|
|
Loading…
Reference in New Issue