Merge branch 'dev' into notify
This commit is contained in:
commit
5b8cfc02d6
|
@ -1,3 +1,12 @@
|
|||
1.12.2
|
||||
========================
|
||||
* Fix: Player not starting when path contains spaces
|
||||
* Added setting to toggle "Player Starting" message
|
||||
* Added possibility to add models by their URL
|
||||
* Added pause / resume all buttons
|
||||
* Implemented multi-selection for Recording and Recordings tab
|
||||
* Fix: Don't do space check, if minimum is set to 0
|
||||
|
||||
1.12.1
|
||||
========================
|
||||
* Fixed downloads in client / server mode
|
||||
|
|
|
@ -288,9 +288,8 @@ public class CamrecApplication extends Application {
|
|||
LOG.error("Couldn't load settings", e);
|
||||
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
||||
alert.setTitle("Whoopsie");
|
||||
alert.setContentText("Couldn't load settings.");
|
||||
alert.setContentText("Couldn't load settings. Falling back to defaults. A backup of your settings has been created.");
|
||||
alert.showAndWait();
|
||||
System.exit(1);
|
||||
}
|
||||
config = Config.getInstance();
|
||||
}
|
||||
|
|
|
@ -197,4 +197,14 @@ public class JavaFxModel implements Model {
|
|||
delegate.setSuspended(suspended);
|
||||
pausedProperty.set(suspended);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return delegate.getDisplayName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDisplayName(String name) {
|
||||
delegate.setDisplayName(name);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -120,7 +120,11 @@ public class Player {
|
|||
try {
|
||||
if (Config.getInstance().getSettings().localRecording && rec != null) {
|
||||
File file = new File(Config.getInstance().getSettings().recordingsDir, rec.getPath());
|
||||
playerProcess = rt.exec(Config.getInstance().getSettings().mediaPlayer + " " + file, OS.getEnvironment(), file.getParentFile());
|
||||
String[] args = new String[] {
|
||||
Config.getInstance().getSettings().mediaPlayer,
|
||||
file.getName()
|
||||
};
|
||||
playerProcess = rt.exec(args, OS.getEnvironment(), file.getParentFile());
|
||||
} else {
|
||||
if(Config.getInstance().getSettings().requireAuthentication) {
|
||||
URL u = new URL(url);
|
||||
|
@ -136,10 +140,12 @@ public class Player {
|
|||
// create threads, which read stdout and stderr of the player process. these are needed,
|
||||
// because otherwise the internal buffer for these streams fill up and block the process
|
||||
Thread std = new Thread(new StreamRedirectThread(playerProcess.getInputStream(), new DevNull()));
|
||||
//Thread std = new Thread(new StreamRedirectThread(playerProcess.getInputStream(), System.out));
|
||||
std.setName("Player stdout pipe");
|
||||
std.setDaemon(true);
|
||||
std.start();
|
||||
Thread err = new Thread(new StreamRedirectThread(playerProcess.getErrorStream(), new DevNull()));
|
||||
//Thread err = new Thread(new StreamRedirectThread(playerProcess.getErrorStream(), System.err));
|
||||
err.setName("Player stderr pipe");
|
||||
err.setDaemon(true);
|
||||
err.start();
|
||||
|
|
|
@ -0,0 +1,332 @@
|
|||
package ctbrec.ui;
|
||||
|
||||
import java.io.InterruptedIOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.io.HttpException;
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
import javafx.application.Platform;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableRow;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.media.Media;
|
||||
import javafx.scene.media.MediaPlayer;
|
||||
import javafx.scene.media.MediaView;
|
||||
import javafx.stage.Popup;
|
||||
|
||||
public class PreviewPopupHandler implements EventHandler<MouseEvent> {
|
||||
private static final transient Logger LOG = LoggerFactory.getLogger(PreviewPopupHandler.class);
|
||||
|
||||
private static final int offset = 10;
|
||||
private long timeForPopupOpen = TimeUnit.SECONDS.toMillis(1);
|
||||
private long timeForPopupClose = 400;
|
||||
private Popup popup = new Popup();
|
||||
private Node parent;
|
||||
private ImageView preview = new ImageView();
|
||||
private MediaView videoPreview;
|
||||
private MediaPlayer videoPlayer;
|
||||
private Media video;
|
||||
private JavaFxModel model;
|
||||
private volatile long openCountdown = -1;
|
||||
private volatile long closeCountdown = -1;
|
||||
private volatile long lastModelChange = -1;
|
||||
private volatile boolean changeModel = false;
|
||||
private ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
private Future<?> future;
|
||||
private ProgressIndicator progressIndicator;
|
||||
private StackPane pane;
|
||||
|
||||
public PreviewPopupHandler(Node parent) {
|
||||
this.parent = parent;
|
||||
|
||||
videoPreview = new MediaView();
|
||||
videoPreview.setFitWidth(Config.getInstance().getSettings().thumbWidth);
|
||||
videoPreview.setFitHeight(videoPreview.getFitWidth() * 9 / 16);
|
||||
videoPreview.setPreserveRatio(true);
|
||||
StackPane.setMargin(videoPreview, new Insets(5));
|
||||
|
||||
preview.setFitWidth(Config.getInstance().getSettings().thumbWidth);
|
||||
preview.setPreserveRatio(true);
|
||||
preview.setSmooth(true);
|
||||
preview.setStyle("-fx-background-radius: 10px, 10px, 10px, 10px;");
|
||||
preview.visibleProperty().bind(videoPreview.visibleProperty().not());
|
||||
StackPane.setMargin(preview, new Insets(5));
|
||||
|
||||
progressIndicator = new ProgressIndicator();
|
||||
progressIndicator.setVisible(false);
|
||||
progressIndicator.prefWidthProperty().bind(videoPreview.fitWidthProperty());
|
||||
|
||||
Region veil = new Region();
|
||||
veil.setStyle("-fx-background-color: rgba(0, 0, 0, 0.8)");
|
||||
veil.visibleProperty().bind(progressIndicator.visibleProperty());
|
||||
StackPane.setMargin(veil, new Insets(5));
|
||||
|
||||
pane = new StackPane();
|
||||
pane.getChildren().addAll(preview, videoPreview, veil, progressIndicator);
|
||||
pane.setStyle("-fx-background-color: -fx-outer-border, -fx-inner-border, -fx-base;"+
|
||||
"-fx-background-insets: 0 0 -1 0, 0, 1, 2;" +
|
||||
"-fx-background-radius: 10px, 10px, 10px, 10px;" +
|
||||
"-fx-padding: 1;" +
|
||||
"-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.8), 20, 0, 0, 0);");
|
||||
popup.getContent().add(pane);
|
||||
|
||||
createTimerThread();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(MouseEvent event) {
|
||||
if(!isInPreviewColumn(event)) {
|
||||
closeCountdown = timeForPopupClose;
|
||||
return;
|
||||
}
|
||||
|
||||
if(event.getEventType() == MouseEvent.MOUSE_CLICKED && event.getButton() == MouseButton.PRIMARY) {
|
||||
model = getModel(event);
|
||||
popup.setX(event.getScreenX()+ offset);
|
||||
popup.setY(event.getScreenY()+ offset);
|
||||
showPopup();
|
||||
openCountdown = -1;
|
||||
} else if(event.getEventType() == MouseEvent.MOUSE_ENTERED) {
|
||||
popup.setX(event.getScreenX()+ offset);
|
||||
popup.setY(event.getScreenY()+ offset);
|
||||
JavaFxModel model = getModel(event);
|
||||
if(model != null) {
|
||||
closeCountdown = -1;
|
||||
boolean modelChanged = model != this.model;
|
||||
this.model = model;
|
||||
if(popup.isShowing()) {
|
||||
openCountdown = -1;
|
||||
if(modelChanged) {
|
||||
lastModelChange = System.currentTimeMillis();
|
||||
changeModel = true;
|
||||
future.cancel(true);
|
||||
progressIndicator.setVisible(true);
|
||||
}
|
||||
} else {
|
||||
openCountdown = timeForPopupOpen;
|
||||
}
|
||||
}
|
||||
} else if(event.getEventType() == MouseEvent.MOUSE_EXITED) {
|
||||
openCountdown = -1;
|
||||
closeCountdown = timeForPopupClose;
|
||||
model = null;
|
||||
} else if(event.getEventType() == MouseEvent.MOUSE_MOVED) {
|
||||
popup.setX(event.getScreenX() + offset);
|
||||
popup.setY(event.getScreenY() + offset);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInPreviewColumn(MouseEvent event) {
|
||||
@SuppressWarnings("unchecked")
|
||||
TableRow<JavaFxModel> row = (TableRow<JavaFxModel>) event.getSource();
|
||||
TableView<JavaFxModel> table = row.getTableView();
|
||||
double offset = 0;
|
||||
double width = 0;
|
||||
for (TableColumn<JavaFxModel, ?> col : table.getColumns()) {
|
||||
offset += width;
|
||||
width = col.getWidth();
|
||||
if(Objects.equals(col.getId(), "preview")) {
|
||||
Point2D screenToLocal = table.screenToLocal(event.getScreenX(), event.getScreenY());
|
||||
double x = screenToLocal.getX();
|
||||
return x >= offset && x <= offset + width;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private JavaFxModel getModel(MouseEvent event) {
|
||||
@SuppressWarnings("unchecked")
|
||||
TableRow<JavaFxModel> row = (TableRow<JavaFxModel>) event.getSource();
|
||||
TableView<JavaFxModel> table = row.getTableView();
|
||||
int rowIndex = row.getIndex();
|
||||
if(rowIndex < table.getItems().size()) {
|
||||
return table.getItems().get(rowIndex);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void showPopup() {
|
||||
startStream(model);
|
||||
}
|
||||
|
||||
private void startStream(JavaFxModel model) {
|
||||
if(future != null && !future.isDone()) {
|
||||
future.cancel(true);
|
||||
}
|
||||
future = executor.submit(() -> {
|
||||
try {
|
||||
Platform.runLater(() -> {
|
||||
progressIndicator.setVisible(true);
|
||||
popup.show(parent.getScene().getWindow());
|
||||
});
|
||||
List<StreamSource> sources = model.getStreamSources();
|
||||
Collections.sort(sources);
|
||||
StreamSource best = sources.get(0);
|
||||
checkInterrupt();
|
||||
LOG.debug("Preview url for {} is {}", model.getName(), best.getMediaPlaylistUrl());
|
||||
video = new Media(best.getMediaPlaylistUrl());
|
||||
if(videoPlayer != null) {
|
||||
videoPlayer.dispose();
|
||||
}
|
||||
videoPlayer = new MediaPlayer(video);
|
||||
videoPlayer.setMute(true);
|
||||
checkInterrupt();
|
||||
videoPlayer.setOnReady(() -> {
|
||||
if(!future.isCancelled()) {
|
||||
Platform.runLater(() -> {
|
||||
double aspect = (double)video.getWidth() / video.getHeight();
|
||||
double w = Config.getInstance().getSettings().thumbWidth;
|
||||
double h = w / aspect;
|
||||
resize(w, h);
|
||||
progressIndicator.setVisible(false);
|
||||
videoPreview.setVisible(true);
|
||||
videoPreview.setMediaPlayer(videoPlayer);
|
||||
videoPlayer.play();
|
||||
});
|
||||
}
|
||||
});
|
||||
videoPlayer.setOnError(() -> onError(videoPlayer));
|
||||
} catch (IllegalStateException e) {
|
||||
if(e.getMessage().equals("Stream url unknown")) {
|
||||
// fine hls url for mfc not known yet
|
||||
} else {
|
||||
LOG.warn("Couldn't start preview video: {}", e.getMessage());
|
||||
}
|
||||
showTestImage();
|
||||
} catch (HttpException e) {
|
||||
if(e.getResponseCode() != 404) {
|
||||
LOG.warn("Couldn't start preview video: {}", e.getMessage());
|
||||
}
|
||||
showTestImage();
|
||||
} catch (InterruptedException | InterruptedIOException e) {
|
||||
// future has been canceled, that's fine
|
||||
} catch (ExecutionException e) {
|
||||
if(e.getCause() instanceof InterruptedException || e.getCause() instanceof InterruptedIOException) {
|
||||
// future has been canceled, that's fine
|
||||
} else {
|
||||
LOG.warn("Couldn't start preview video: {}", e.getMessage());
|
||||
showTestImage();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Couldn't start preview video: {}", e.getMessage());
|
||||
showTestImage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onError(MediaPlayer videoPlayer) {
|
||||
LOG.error("Error while starting preview stream", videoPlayer.getError());
|
||||
if(videoPlayer.getError().getCause() != null) {
|
||||
LOG.error("Error while starting preview stream root cause:", videoPlayer.getError().getCause());
|
||||
}
|
||||
Platform.runLater(() -> {
|
||||
showTestImage();
|
||||
});
|
||||
}
|
||||
|
||||
private void resize(double w, double h) {
|
||||
preview.setFitWidth(w);
|
||||
preview.setFitHeight(h);
|
||||
videoPreview.setFitWidth(w);
|
||||
videoPreview.setFitHeight(h);
|
||||
pane.setPrefSize(w, h);
|
||||
popup.setWidth(w);
|
||||
popup.setHeight(h);
|
||||
}
|
||||
|
||||
private void checkInterrupt() throws InterruptedException {
|
||||
if(Thread.interrupted()) {
|
||||
throw new InterruptedException();
|
||||
}
|
||||
}
|
||||
|
||||
private void showTestImage() {
|
||||
Platform.runLater(() -> {
|
||||
videoPreview.setVisible(false);
|
||||
Image img = new Image(getClass().getResource("/image_not_found.png").toString(), true);
|
||||
preview.setImage(img);
|
||||
double aspect = img.getWidth() / img.getHeight();
|
||||
double w = Config.getInstance().getSettings().thumbWidth;
|
||||
double h = w / aspect;
|
||||
resize(w, h);
|
||||
progressIndicator.setVisible(false);
|
||||
});
|
||||
}
|
||||
|
||||
private void hidePopup() {
|
||||
Platform.runLater(() -> {
|
||||
popup.setX(-1000);
|
||||
popup.setY(-1000);
|
||||
popup.hide();
|
||||
if(videoPlayer != null) {
|
||||
videoPlayer.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void createTimerThread() {
|
||||
Thread timerThread = new Thread(() -> {
|
||||
while(true) {
|
||||
openCountdown--;
|
||||
if(openCountdown == 0) {
|
||||
openCountdown = -1;
|
||||
if(model != null) {
|
||||
showPopup();
|
||||
}
|
||||
}
|
||||
|
||||
closeCountdown--;
|
||||
if(closeCountdown == 0) {
|
||||
hidePopup();
|
||||
closeCountdown = -1;
|
||||
}
|
||||
|
||||
openCountdown = Math.max(openCountdown, -1);
|
||||
closeCountdown = Math.max(closeCountdown, -1);
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
long diff = (now - lastModelChange);
|
||||
if(changeModel && diff > 400) {
|
||||
changeModel = false;
|
||||
if(model != null) {
|
||||
startStream(model);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(1);
|
||||
} catch (InterruptedException e) {
|
||||
LOG.error("PreviewPopupTimer interrupted");
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
timerThread.setDaemon(true);
|
||||
timerThread.setPriority(Thread.MIN_PRIORITY);
|
||||
timerThread.setName("PreviewPopupTimer");
|
||||
timerThread.start();
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ package ctbrec.ui;
|
|||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
@ -14,6 +15,7 @@ import java.util.concurrent.LinkedBlockingQueue;
|
|||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
@ -29,6 +31,7 @@ import ctbrec.sites.Site;
|
|||
import ctbrec.ui.controls.AutoFillTextField;
|
||||
import ctbrec.ui.controls.Toast;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
|
@ -42,9 +45,11 @@ 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.TableColumn;
|
||||
import javafx.scene.control.TableColumn.SortType;
|
||||
import javafx.scene.control.TableRow;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.control.cell.CheckBoxTableCell;
|
||||
|
@ -79,6 +84,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
Label modelLabel = new Label("Model");
|
||||
AutoFillTextField model;
|
||||
Button addModelButton = new Button("Record");
|
||||
Button pauseAll = new Button("Pause All");
|
||||
Button resumeAll = new Button("Resume All");
|
||||
|
||||
public RecordedModelsTab(String title, Recorder recorder, List<Site> sites) {
|
||||
super(title);
|
||||
|
@ -100,54 +107,68 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
scrollPane.setFitToWidth(true);
|
||||
BorderPane.setMargin(scrollPane, new Insets(5));
|
||||
|
||||
table.setEditable(false);
|
||||
|
||||
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");
|
||||
TableColumn<JavaFxModel, String> name = new TableColumn<>("Model");
|
||||
name.setPrefWidth(200);
|
||||
name.setCellValueFactory(new PropertyValueFactory<JavaFxModel, String>("name"));
|
||||
name.setCellValueFactory(new PropertyValueFactory<JavaFxModel, String>("displayName"));
|
||||
name.setEditable(false);
|
||||
TableColumn<JavaFxModel, String> url = new TableColumn<>("URL");
|
||||
url.setCellValueFactory(new PropertyValueFactory<JavaFxModel, String>("url"));
|
||||
url.setPrefWidth(400);
|
||||
url.setEditable(false);
|
||||
TableColumn<JavaFxModel, Boolean> online = new TableColumn<>("Online");
|
||||
online.setCellValueFactory((cdf) -> cdf.getValue().getOnlineProperty());
|
||||
online.setCellValueFactory(cdf -> cdf.getValue().getOnlineProperty());
|
||||
online.setCellFactory(CheckBoxTableCell.forTableColumn(online));
|
||||
online.setPrefWidth(100);
|
||||
online.setEditable(false);
|
||||
TableColumn<JavaFxModel, Boolean> recording = new TableColumn<>("Recording");
|
||||
recording.setCellValueFactory((cdf) -> cdf.getValue().getRecordingProperty());
|
||||
recording.setCellValueFactory(cdf -> cdf.getValue().getRecordingProperty());
|
||||
recording.setCellFactory(CheckBoxTableCell.forTableColumn(recording));
|
||||
recording.setPrefWidth(100);
|
||||
recording.setEditable(false);
|
||||
TableColumn<JavaFxModel, Boolean> paused = new TableColumn<>("Paused");
|
||||
paused.setCellValueFactory((cdf) -> cdf.getValue().getPausedProperty());
|
||||
paused.setCellValueFactory(cdf -> cdf.getValue().getPausedProperty());
|
||||
paused.setCellFactory(CheckBoxTableCell.forTableColumn(paused));
|
||||
paused.setPrefWidth(100);
|
||||
table.getColumns().addAll(name, url, online, recording, paused);
|
||||
paused.setEditable(true);
|
||||
table.getColumns().addAll(preview, name, url, online, recording, paused);
|
||||
table.setItems(observableModels);
|
||||
table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
|
||||
popup = createContextMenu();
|
||||
if(popup != null) {
|
||||
if (popup != null) {
|
||||
popup.show(table, event.getScreenX(), event.getScreenY());
|
||||
}
|
||||
event.consume();
|
||||
});
|
||||
table.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
|
||||
if(popup != null) {
|
||||
if (popup != null) {
|
||||
popup.hide();
|
||||
}
|
||||
});
|
||||
table.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||
if(event.getCode() == KeyCode.DELETE) {
|
||||
stopAction();
|
||||
table.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
|
||||
List<JavaFxModel> selectedModels = table.getSelectionModel().getSelectedItems();
|
||||
if (event.getCode() == KeyCode.DELETE) {
|
||||
stopAction(selectedModels);
|
||||
} else if (event.getCode() == KeyCode.P) {
|
||||
List<JavaFxModel> pausedModels = selectedModels.stream().filter(m -> m.isSuspended()).collect(Collectors.toList());
|
||||
List<JavaFxModel> runningModels = selectedModels.stream().filter(m -> !m.isSuspended()).collect(Collectors.toList());
|
||||
resumeRecording(pausedModels);
|
||||
pauseRecording(runningModels);
|
||||
}
|
||||
});
|
||||
table.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||
if(event.getCode() == KeyCode.S) {
|
||||
for (TableColumn<JavaFxModel, ?> col : table.getSortOrder()) {
|
||||
System.out.println(col.getText());
|
||||
System.out.println(col.getSortType());
|
||||
System.out.println(col.getComparator());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
scrollPane.setContent(table);
|
||||
|
||||
HBox addModelBox = new HBox(5);
|
||||
|
@ -155,14 +176,17 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
ObservableList<String> suggestions = FXCollections.observableArrayList();
|
||||
sites.forEach(site -> suggestions.add(site.getName()));
|
||||
model = new AutoFillTextField(suggestions);
|
||||
model.setPrefWidth(300);
|
||||
model.setPromptText("e.g. MyFreeCams:ModelName");
|
||||
model.onActionHandler(e -> addModel(e));
|
||||
model.setPrefWidth(600);
|
||||
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((e) -> addModel(e));
|
||||
addModelBox.getChildren().addAll(modelLabel, model, addModelButton);
|
||||
addModelButton.setOnAction(this::addModel);
|
||||
addModelBox.getChildren().addAll(modelLabel, model, addModelButton, pauseAll, resumeAll);
|
||||
HBox.setMargin(pauseAll, new Insets(0, 0, 0, 20));
|
||||
pauseAll.setOnAction(this::pauseAll);
|
||||
resumeAll.setOnAction(this::resumeAll);
|
||||
|
||||
BorderPane root = new BorderPane();
|
||||
root.setPadding(new Insets(5));
|
||||
|
@ -174,6 +198,43 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
|
||||
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 model = site.createModelFromUrl(url);
|
||||
if (model != null) {
|
||||
try {
|
||||
recorder.startRecording(model);
|
||||
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
|
||||
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
||||
alert.setTitle("Error");
|
||||
alert.setHeaderText("Couldn't add model");
|
||||
alert.setContentText("The model " + model.getName() + " could not be added: " + e1.getLocalizedMessage());
|
||||
alert.showAndWait();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
||||
alert.setTitle("Unknown URL format");
|
||||
alert.setHeaderText("Couldn't add model");
|
||||
alert.setContentText("The URL you entered has an unknown format or the function does not support this site, yet");
|
||||
alert.showAndWait();
|
||||
}
|
||||
|
||||
private void addModelByName(String siteModelCombo) {
|
||||
String[] parts = model.getText().trim().split(":");
|
||||
if (parts.length != 2) {
|
||||
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
||||
|
@ -207,15 +268,50 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
alert.setHeaderText("Couldn't add model");
|
||||
alert.setContentText("The site you entered is unknown");
|
||||
alert.showAndWait();
|
||||
};
|
||||
}
|
||||
|
||||
private void pauseAll(ActionEvent evt) {
|
||||
List<Model> models = recorder.getModelsRecording();
|
||||
Consumer<Model> action = (m) -> {
|
||||
try {
|
||||
recorder.suspendRecording(m);
|
||||
} catch(Exception e) {
|
||||
Platform.runLater(() ->
|
||||
showErrorDialog(e, "Couldn't suspend recording of model", "Suspending recording of " + m.getName() + " failed"));
|
||||
}
|
||||
};
|
||||
massEdit(models, action);
|
||||
}
|
||||
|
||||
private void resumeAll(ActionEvent evt) {
|
||||
List<Model> models = recorder.getModelsRecording();
|
||||
Consumer<Model> action = (m) -> {
|
||||
try {
|
||||
recorder.resumeRecording(m);
|
||||
} catch(Exception e) {
|
||||
Platform.runLater(() ->
|
||||
showErrorDialog(e, "Couldn't resume recording of model", "Resuming recording of " + m.getName() + " failed"));
|
||||
}
|
||||
};
|
||||
massEdit(models, action);
|
||||
}
|
||||
|
||||
private void massEdit(List<Model> models, Consumer<Model> action) {
|
||||
getTabPane().setCursor(Cursor.WAIT);
|
||||
threadPool.submit(() -> {
|
||||
for (Model model : models) {
|
||||
action.accept(model);
|
||||
}
|
||||
Platform.runLater(() -> getTabPane().setCursor(Cursor.DEFAULT));
|
||||
});
|
||||
}
|
||||
|
||||
void initializeUpdateService() {
|
||||
updateService = createUpdateService();
|
||||
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(2)));
|
||||
updateService.setOnSucceeded((event) -> {
|
||||
List<JavaFxModel> models = updateService.getValue();
|
||||
if(models == null) {
|
||||
if (models == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -223,6 +319,17 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
int index = observableModels.indexOf(updatedModel);
|
||||
if (index == -1) {
|
||||
observableModels.add(updatedModel);
|
||||
updatedModel.getPausedProperty().addListener((obs, oldV, newV) -> {
|
||||
if (newV) {
|
||||
if(!recorder.isSuspended(updatedModel)) {
|
||||
pauseRecording(Collections.singletonList(updatedModel));
|
||||
}
|
||||
} else {
|
||||
if(recorder.isSuspended(updatedModel)) {
|
||||
resumeRecording(Collections.singletonList(updatedModel));
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// make sure to update the JavaFX online property, so that the table cell is updated
|
||||
JavaFxModel oldModel = observableModels.get(index);
|
||||
|
@ -310,16 +417,16 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
|
||||
private ContextMenu createContextMenu() {
|
||||
JavaFxModel selectedModel = table.getSelectionModel().getSelectedItem();
|
||||
if(selectedModel == null) {
|
||||
ObservableList<JavaFxModel> selectedModels = table.getSelectionModel().getSelectedItems();
|
||||
if (selectedModels.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
MenuItem stop = new MenuItem("Remove Model");
|
||||
stop.setOnAction((e) -> stopAction());
|
||||
stop.setOnAction((e) -> stopAction(selectedModels));
|
||||
|
||||
MenuItem copyUrl = new MenuItem("Copy URL");
|
||||
copyUrl.setOnAction((e) -> {
|
||||
Model selected = selectedModel;
|
||||
Model selected = selectedModels.get(0);
|
||||
final Clipboard clipboard = Clipboard.getSystemClipboard();
|
||||
final ClipboardContent content = new ClipboardContent();
|
||||
content.putString(selected.getUrl());
|
||||
|
@ -327,19 +434,31 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
});
|
||||
|
||||
MenuItem pauseRecording = new MenuItem("Pause Recording");
|
||||
pauseRecording.setOnAction((e) -> pauseRecording());
|
||||
pauseRecording.setOnAction((e) -> pauseRecording(selectedModels));
|
||||
MenuItem resumeRecording = new MenuItem("Resume Recording");
|
||||
resumeRecording.setOnAction((e) -> resumeRecording());
|
||||
resumeRecording.setOnAction((e) -> resumeRecording(selectedModels));
|
||||
MenuItem openInBrowser = new MenuItem("Open in Browser");
|
||||
openInBrowser.setOnAction((e) -> DesktopIntegration.open(selectedModel.getUrl()));
|
||||
openInBrowser.setOnAction((e) -> DesktopIntegration.open(selectedModels.get(0).getUrl()));
|
||||
MenuItem openInPlayer = new MenuItem("Open in Player");
|
||||
openInPlayer.setOnAction((e) -> openInPlayer(selectedModel));
|
||||
openInPlayer.setOnAction((e) -> openInPlayer(selectedModels.get(0)));
|
||||
MenuItem switchStreamSource = new MenuItem("Switch resolution");
|
||||
switchStreamSource.setOnAction((e) -> switchStreamSource(selectedModel));
|
||||
switchStreamSource.setOnAction((e) -> switchStreamSource(selectedModels.get(0)));
|
||||
|
||||
ContextMenu menu = new ContextMenu(stop);
|
||||
menu.getItems().add(selectedModel.isSuspended() ? resumeRecording : pauseRecording);
|
||||
if (selectedModels.size() == 1) {
|
||||
menu.getItems().add(selectedModels.get(0).isSuspended() ? resumeRecording : pauseRecording);
|
||||
} else {
|
||||
menu.getItems().addAll(resumeRecording, pauseRecording);
|
||||
}
|
||||
menu.getItems().addAll(copyUrl, openInPlayer, openInBrowser, switchStreamSource);
|
||||
|
||||
if (selectedModels.size() > 1) {
|
||||
copyUrl.setDisable(true);
|
||||
openInPlayer.setDisable(true);
|
||||
openInBrowser.setDisable(true);
|
||||
switchStreamSource.setDisable(true);
|
||||
}
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
|
@ -348,7 +467,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
new Thread(() -> {
|
||||
boolean started = Player.play(selectedModel);
|
||||
Platform.runLater(() -> {
|
||||
if(started) {
|
||||
if (started && Config.getInstance().getSettings().showPlayerStarting) {
|
||||
Toast.makeText(getTabPane().getScene(), "Starting Player", 2000, 500, 500);
|
||||
}
|
||||
table.setCursor(Cursor.DEFAULT);
|
||||
|
@ -358,7 +477,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
|
||||
private void switchStreamSource(JavaFxModel fxModel) {
|
||||
try {
|
||||
if(!fxModel.isOnline()) {
|
||||
if (!fxModel.isOnline()) {
|
||||
Alert alert = new AutosizeAlert(Alert.AlertType.INFORMATION);
|
||||
alert.setTitle("Switch resolution");
|
||||
alert.setHeaderText("Couldn't switch stream resolution");
|
||||
|
@ -393,98 +512,61 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
|
||||
private void showStreamSwitchErrorDialog(Throwable throwable) {
|
||||
showErrorDialog(throwable, "Couldn't switch stream resolution", "Error while switching stream resolution");
|
||||
}
|
||||
|
||||
private void showErrorDialog(Throwable throwable, String header, String msg) {
|
||||
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
||||
alert.setTitle("Error");
|
||||
alert.setHeaderText("Couldn't switch stream resolution");
|
||||
alert.setContentText("Error while switching stream resolution: " + throwable.getLocalizedMessage());
|
||||
alert.setHeaderText(header);
|
||||
alert.setContentText(msg + ": " + throwable.getLocalizedMessage());
|
||||
alert.showAndWait();
|
||||
}
|
||||
|
||||
private void stopAction() {
|
||||
Model selected = table.getSelectionModel().getSelectedItem().getDelegate();
|
||||
if (selected != null) {
|
||||
table.setCursor(Cursor.WAIT);
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
recorder.stopRecording(selected);
|
||||
observableModels.remove(selected);
|
||||
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
|
||||
LOG.error("Couldn't stop recording", e1);
|
||||
Platform.runLater(() -> {
|
||||
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
||||
alert.setTitle("Error");
|
||||
alert.setHeaderText("Couldn't stop recording");
|
||||
alert.setContentText("Error while stopping the recording: " + e1.getLocalizedMessage());
|
||||
alert.showAndWait();
|
||||
});
|
||||
} finally {
|
||||
table.setCursor(Cursor.DEFAULT);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
private void stopAction(List<JavaFxModel> selectedModels) {
|
||||
Consumer<Model> action = (m) -> {
|
||||
try {
|
||||
recorder.stopRecording(m);
|
||||
observableModels.remove(m);
|
||||
} catch(Exception e) {
|
||||
Platform.runLater(() ->
|
||||
showErrorDialog(e, "Couldn't stop recording", "Stopping recording of " + m.getName() + " failed"));
|
||||
}
|
||||
};
|
||||
List<Model> models = selectedModels.stream().map(jfxm -> jfxm.getDelegate()).collect(Collectors.toList());
|
||||
massEdit(models, action);
|
||||
};
|
||||
|
||||
private void pauseRecording() {
|
||||
JavaFxModel model = table.getSelectionModel().getSelectedItem();
|
||||
Model delegate = table.getSelectionModel().getSelectedItem().getDelegate();
|
||||
if (delegate != null) {
|
||||
table.setCursor(Cursor.WAIT);
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
recorder.suspendRecording(delegate);
|
||||
Platform.runLater(() -> model.setSuspended(true));
|
||||
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
|
||||
LOG.error("Couldn't pause recording", e1);
|
||||
Platform.runLater(() -> {
|
||||
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
||||
alert.setTitle("Error");
|
||||
alert.setHeaderText("Couldn't pause recording");
|
||||
alert.setContentText("Error while pausing the recording: " + e1.getLocalizedMessage());
|
||||
alert.showAndWait();
|
||||
});
|
||||
} finally {
|
||||
table.setCursor(Cursor.DEFAULT);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
private void pauseRecording(List<JavaFxModel> selectedModels) {
|
||||
Consumer<Model> action = (m) -> {
|
||||
try {
|
||||
recorder.suspendRecording(m);
|
||||
m.setSuspended(true);
|
||||
} catch(Exception e) {
|
||||
Platform.runLater(() ->
|
||||
showErrorDialog(e, "Couldn't pause recording of model", "Pausing recording of " + m.getName() + " failed"));
|
||||
}
|
||||
};
|
||||
List<Model> models = selectedModels.stream().map(jfxm -> jfxm.getDelegate()).collect(Collectors.toList());
|
||||
massEdit(models, action);
|
||||
};
|
||||
|
||||
private void resumeRecording() {
|
||||
JavaFxModel model = table.getSelectionModel().getSelectedItem();
|
||||
Model delegate = table.getSelectionModel().getSelectedItem().getDelegate();
|
||||
if (delegate != null) {
|
||||
table.setCursor(Cursor.WAIT);
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
recorder.resumeRecording(delegate);
|
||||
Platform.runLater(() -> model.setSuspended(false));
|
||||
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
|
||||
LOG.error("Couldn't resume recording", e1);
|
||||
Platform.runLater(() -> {
|
||||
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
||||
alert.setTitle("Error");
|
||||
alert.setHeaderText("Couldn't resume recording");
|
||||
alert.setContentText("Error while resuming the recording: " + e1.getLocalizedMessage());
|
||||
alert.showAndWait();
|
||||
});
|
||||
} finally {
|
||||
table.setCursor(Cursor.DEFAULT);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
private void resumeRecording(List<JavaFxModel> selectedModels) {
|
||||
Consumer<Model> action = (m) -> {
|
||||
try {
|
||||
recorder.resumeRecording(m);
|
||||
m.setSuspended(false);
|
||||
} catch(Exception e) {
|
||||
Platform.runLater(() ->
|
||||
showErrorDialog(e, "Couldn't resume recording of model", "Resuming recording of " + m.getName() + " failed"));
|
||||
}
|
||||
};
|
||||
List<Model> models = selectedModels.stream().map(jfxm -> jfxm.getDelegate()).collect(Collectors.toList());
|
||||
massEdit(models, action);
|
||||
}
|
||||
|
||||
public void saveState() {
|
||||
if(!table.getSortOrder().isEmpty()) {
|
||||
if (!table.getSortOrder().isEmpty()) {
|
||||
TableColumn<JavaFxModel, ?> col = table.getSortOrder().get(0);
|
||||
Config.getInstance().getSettings().recordedModelsSortColumn = col.getText();
|
||||
Config.getInstance().getSettings().recordedModelsSortType = col.getSortType().toString();
|
||||
|
@ -498,9 +580,9 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
|
||||
private void restoreState() {
|
||||
String sortCol = Config.getInstance().getSettings().recordedModelsSortColumn;
|
||||
if(StringUtil.isNotBlank(sortCol)) {
|
||||
if (StringUtil.isNotBlank(sortCol)) {
|
||||
for (TableColumn<JavaFxModel, ?> col : table.getColumns()) {
|
||||
if(Objects.equals(sortCol, col.getText())) {
|
||||
if (Objects.equals(sortCol, col.getText())) {
|
||||
col.setSortType(SortType.valueOf(Config.getInstance().getSettings().recordedModelsSortType));
|
||||
table.getSortOrder().clear();
|
||||
table.getSortOrder().add(col);
|
||||
|
@ -510,7 +592,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
|
||||
double[] columnWidths = Config.getInstance().getSettings().recordedModelsColumnWidths;
|
||||
if(columnWidths != null && columnWidths.length == table.getColumns().size()) {
|
||||
if (columnWidths != null && columnWidths.length == table.getColumns().size()) {
|
||||
for (int i = 0; i < columnWidths.length; i++) {
|
||||
table.getColumns().get(i).setPrefWidth(columnWidths[i]);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import java.io.File;
|
|||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.DecimalFormat;
|
||||
|
@ -22,6 +23,8 @@ import java.util.concurrent.ExecutorService;
|
|||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -52,6 +55,7 @@ import javafx.scene.control.Label;
|
|||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.ProgressBar;
|
||||
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;
|
||||
|
@ -91,6 +95,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
ContextMenu popup;
|
||||
ProgressBar spaceLeft;
|
||||
Label spaceLabel;
|
||||
Lock recordingsLock = new ReentrantLock();
|
||||
|
||||
public RecordingsTab(String title, Recorder recorder, Config config, List<Site> sites) {
|
||||
super(title);
|
||||
|
@ -114,6 +119,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
BorderPane.setMargin(scrollPane, new Insets(5));
|
||||
|
||||
table.setEditable(false);
|
||||
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
TableColumn<JavaFxRecording, String> name = new TableColumn<>("Model");
|
||||
name.setPrefWidth(200);
|
||||
name.setCellValueFactory(new PropertyValueFactory<JavaFxRecording, String>("modelName"));
|
||||
|
@ -162,14 +168,12 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
setStyle(null);
|
||||
} else {
|
||||
setText(StringUtil.formatSize(sizeInByte));
|
||||
setStyle("-fx-alignment: CENTER-RIGHT;");
|
||||
if(Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
|
||||
int row = this.getTableRow().getIndex();
|
||||
JavaFxRecording rec = tableViewProperty().get().getItems().get(row);
|
||||
if(!rec.valueChanged() && rec.getStatus() == STATUS.RECORDING) {
|
||||
setStyle("-fx-alignment: CENTER-RIGHT; -fx-background-color: red");
|
||||
} else {
|
||||
setStyle("-fx-alignment: CENTER-RIGHT;");
|
||||
//setStyle(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -182,9 +186,9 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
table.getColumns().addAll(name, date, status, progress, size);
|
||||
table.setItems(observableRecordings);
|
||||
table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
|
||||
Recording recording = table.getSelectionModel().getSelectedItem();
|
||||
if(recording != null) {
|
||||
popup = createContextMenu(recording);
|
||||
List<JavaFxRecording> recordings = table.getSelectionModel().getSelectedItems();
|
||||
if(recordings != null && !recordings.isEmpty()) {
|
||||
popup = createContextMenu(recordings);
|
||||
if(!popup.getItems().isEmpty()) {
|
||||
popup.show(table, event.getScreenX(), event.getScreenY());
|
||||
}
|
||||
|
@ -205,13 +209,15 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
});
|
||||
table.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||
JavaFxRecording recording = table.getSelectionModel().getSelectedItem();
|
||||
if (recording != null) {
|
||||
List<JavaFxRecording> recordings = table.getSelectionModel().getSelectedItems();
|
||||
if (recordings != null && !recordings.isEmpty()) {
|
||||
if (event.getCode() == KeyCode.DELETE) {
|
||||
delete(recording);
|
||||
if(recordings.size() > 1 || recordings.get(0).getStatus() == STATUS.FINISHED) {
|
||||
delete(recordings);
|
||||
}
|
||||
} else if (event.getCode() == KeyCode.ENTER) {
|
||||
if(recording.getStatus() == STATUS.FINISHED) {
|
||||
play(recording);
|
||||
if(recordings.get(0).getStatus() == STATUS.FINISHED) {
|
||||
play(recordings.get(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -275,23 +281,28 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
return;
|
||||
}
|
||||
|
||||
for (Iterator<JavaFxRecording> iterator = observableRecordings.iterator(); iterator.hasNext();) {
|
||||
JavaFxRecording old = iterator.next();
|
||||
if (!recordings.contains(old)) {
|
||||
// remove deleted recordings
|
||||
iterator.remove();
|
||||
recordingsLock.lock();
|
||||
try {
|
||||
for (Iterator<JavaFxRecording> iterator = observableRecordings.iterator(); iterator.hasNext();) {
|
||||
JavaFxRecording old = iterator.next();
|
||||
if (!recordings.contains(old)) {
|
||||
// remove deleted recordings
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
for (JavaFxRecording recording : recordings) {
|
||||
if (!observableRecordings.contains(recording)) {
|
||||
// add new recordings
|
||||
observableRecordings.add(recording);
|
||||
} else {
|
||||
// update existing ones
|
||||
int index = observableRecordings.indexOf(recording);
|
||||
JavaFxRecording old = observableRecordings.get(index);
|
||||
old.update(recording);
|
||||
for (JavaFxRecording recording : recordings) {
|
||||
if (!observableRecordings.contains(recording)) {
|
||||
// add new recordings
|
||||
observableRecordings.add(recording);
|
||||
} else {
|
||||
// update existing ones
|
||||
int index = observableRecordings.indexOf(recording);
|
||||
JavaFxRecording old = observableRecordings.get(index);
|
||||
old.update(recording);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
recordingsLock.unlock();
|
||||
}
|
||||
table.sort();
|
||||
}
|
||||
|
@ -316,6 +327,10 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
try {
|
||||
spaceTotal = recorder.getTotalSpaceBytes();
|
||||
spaceFree = recorder.getFreeSpaceBytes();
|
||||
Platform.runLater(() -> spaceLeft.setTooltip(new Tooltip()));
|
||||
} catch (NoSuchFileException e) {
|
||||
// recordings dir does not exist
|
||||
Platform.runLater(() -> spaceLeft.setTooltip(new Tooltip("Recordings directory does not exist")));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Couldn't update free space", e);
|
||||
}
|
||||
|
@ -351,7 +366,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
}
|
||||
|
||||
private ContextMenu createContextMenu(Recording recording) {
|
||||
private ContextMenu createContextMenu(List<JavaFxRecording> recordings) {
|
||||
ContextMenu contextMenu = new ContextMenu();
|
||||
contextMenu.setHideOnEscape(true);
|
||||
contextMenu.setAutoHide(true);
|
||||
|
@ -359,9 +374,9 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
|
||||
MenuItem openInPlayer = new MenuItem("Open in Player");
|
||||
openInPlayer.setOnAction((e) -> {
|
||||
play(recording);
|
||||
play(recordings.get(0));
|
||||
});
|
||||
if(recording.getStatus() == STATUS.FINISHED || Config.getInstance().getSettings().localRecording) {
|
||||
if(recordings.get(0).getStatus() == STATUS.FINISHED || Config.getInstance().getSettings().localRecording) {
|
||||
contextMenu.getItems().add(openInPlayer);
|
||||
}
|
||||
|
||||
|
@ -381,16 +396,16 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
|
||||
MenuItem deleteRecording = new MenuItem("Delete");
|
||||
deleteRecording.setOnAction((e) -> {
|
||||
delete(recording);
|
||||
delete(recordings);
|
||||
});
|
||||
if(recording.getStatus() == STATUS.FINISHED) {
|
||||
if(recordings.get(0).getStatus() == STATUS.FINISHED || recordings.size() > 1) {
|
||||
contextMenu.getItems().add(deleteRecording);
|
||||
}
|
||||
|
||||
MenuItem openDir = new MenuItem("Open directory");
|
||||
openDir.setOnAction((e) -> {
|
||||
String recordingsDir = Config.getInstance().getSettings().recordingsDir;
|
||||
String path = recording.getPath();
|
||||
String path = recordings.get(0).getPath();
|
||||
File tsFile = new File(recordingsDir, path);
|
||||
new Thread(() -> {
|
||||
DesktopIntegration.open(tsFile.getParent());
|
||||
|
@ -403,16 +418,22 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
MenuItem downloadRecording = new MenuItem("Download");
|
||||
downloadRecording.setOnAction((e) -> {
|
||||
try {
|
||||
download(recording);
|
||||
download(recordings.get(0));
|
||||
} catch (IOException | ParseException | PlaylistException e1) {
|
||||
showErrorDialog("Error while downloading recording", "The recording could not be downloaded", e1);
|
||||
LOG.error("Error while downloading recording", e1);
|
||||
}
|
||||
});
|
||||
if (!Config.getInstance().getSettings().localRecording && recording.getStatus() == STATUS.FINISHED) {
|
||||
if (!Config.getInstance().getSettings().localRecording && recordings.get(0).getStatus() == STATUS.FINISHED) {
|
||||
contextMenu.getItems().add(downloadRecording);
|
||||
}
|
||||
|
||||
if(recordings.size() > 1) {
|
||||
openInPlayer.setDisable(true);
|
||||
openDir.setDisable(true);
|
||||
downloadRecording.setDisable(true);
|
||||
}
|
||||
|
||||
return contextMenu;
|
||||
}
|
||||
|
||||
|
@ -477,80 +498,6 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
}
|
||||
|
||||
// private void download(Recording recording) throws IOException, ParseException, PlaylistException {
|
||||
// String filename = recording.getPath().replaceAll("/", "-") + ".ts";
|
||||
// FileChooser chooser = new FileChooser();
|
||||
// chooser.setInitialFileName(filename);
|
||||
// if(config.getSettings().lastDownloadDir != null && !config.getSettings().lastDownloadDir.equals("")) {
|
||||
// File dir = new File(config.getSettings().lastDownloadDir);
|
||||
// while(!dir.exists()) {
|
||||
// dir = dir.getParentFile();
|
||||
// }
|
||||
// chooser.setInitialDirectory(dir);
|
||||
// }
|
||||
// File target = chooser.showSaveDialog(null);
|
||||
// if(target != null) {
|
||||
// config.getSettings().lastDownloadDir = target.getParent();
|
||||
// String hlsBase = "http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/hls";
|
||||
// URL url = new URL(hlsBase + "/" + recording.getPath() + "/playlist.m3u8");
|
||||
// LOG.info("Downloading {}", recording.getPath());
|
||||
//
|
||||
// PlaylistParser parser = new PlaylistParser(url.openStream(), Format.EXT_M3U, Encoding.UTF_8);
|
||||
// Playlist playlist = parser.parse();
|
||||
// MediaPlaylist mediaPlaylist = playlist.getMediaPlaylist();
|
||||
// List<TrackData> tracks = mediaPlaylist.getTracks();
|
||||
// List<String> segmentUris = new ArrayList<>();
|
||||
// for (TrackData trackData : tracks) {
|
||||
// String segmentUri = hlsBase + "/" + recording.getPath() + "/" + trackData.getUri();
|
||||
// segmentUris.add(segmentUri);
|
||||
// }
|
||||
//
|
||||
// Thread t = new Thread() {
|
||||
// @Override
|
||||
// public void run() {
|
||||
// try(FileOutputStream fos = new FileOutputStream(target)) {
|
||||
// for (int i = 0; i < segmentUris.size(); i++) {
|
||||
// URL segment = new URL(segmentUris.get(i));
|
||||
// InputStream in = segment.openStream();
|
||||
// byte[] b = new byte[1024];
|
||||
// int length = -1;
|
||||
// while( (length = in.read(b)) >= 0 ) {
|
||||
// fos.write(b, 0, length);
|
||||
// }
|
||||
// in.close();
|
||||
// int progress = (int) (i * 100.0 / segmentUris.size());
|
||||
// Platform.runLater(new Runnable() {
|
||||
// @Override
|
||||
// public void run() {
|
||||
// recording.setStatus(STATUS.DOWNLOADING);
|
||||
// recording.setProgress(progress);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// } catch (FileNotFoundException e) {
|
||||
// showErrorDialog("Error while downloading recording", "The target file couldn't be created", e);
|
||||
// LOG.error("Error while downloading recording", e);
|
||||
// } catch (IOException e) {
|
||||
// showErrorDialog("Error while downloading recording", "The recording could not be downloaded", e);
|
||||
// LOG.error("Error while downloading recording", e);
|
||||
// } finally {
|
||||
// Platform.runLater(new Runnable() {
|
||||
// @Override
|
||||
// public void run() {
|
||||
// recording.setStatus(STATUS.FINISHED);
|
||||
// recording.setProgress(-1);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
// t.setDaemon(true);
|
||||
// t.setName("Download Thread " + recording.getPath());
|
||||
// t.start();
|
||||
// }
|
||||
// }
|
||||
|
||||
private void showErrorDialog(final String title, final String msg, final Exception e) {
|
||||
Platform.runLater(new Runnable() {
|
||||
@Override
|
||||
|
@ -571,7 +518,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
@Override
|
||||
public void run() {
|
||||
boolean started = Player.play(recording);
|
||||
if(started) {
|
||||
if(started && Config.getInstance().getSettings().showPlayerStarting) {
|
||||
Platform.runLater(() -> Toast.makeText(getTabPane().getScene(), "Starting Player", 2000, 500, 500));
|
||||
}
|
||||
}
|
||||
|
@ -583,7 +530,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
@Override
|
||||
public void run() {
|
||||
boolean started = Player.play(url);
|
||||
if(started) {
|
||||
if(started && Config.getInstance().getSettings().showPlayerStarting) {
|
||||
Platform.runLater(() -> Toast.makeText(getTabPane().getScene(), "Starting Player", 2000, 500, 500));
|
||||
}
|
||||
}
|
||||
|
@ -592,12 +539,16 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
|
||||
}
|
||||
|
||||
private void delete(Recording r) {
|
||||
if(r.getStatus() != STATUS.FINISHED) {
|
||||
return;
|
||||
}
|
||||
private void delete(List<JavaFxRecording> recordings) {
|
||||
table.setCursor(Cursor.WAIT);
|
||||
String msg = "Delete " + r.getModelName() + "/" + r.getStartDate() + " for good?";
|
||||
|
||||
String msg;
|
||||
if(recordings.size() > 1) {
|
||||
msg = "Delete " + recordings.size() + " recordings for good?";
|
||||
} else {
|
||||
Recording r = recordings.get(0);
|
||||
msg = "Delete " + r.getModelName() + "/" + r.getStartDate() + " for good?";
|
||||
}
|
||||
AutosizeAlert confirm = new AutosizeAlert(AlertType.CONFIRMATION, msg, YES, NO);
|
||||
confirm.setTitle("Delete recording?");
|
||||
confirm.setHeaderText(msg);
|
||||
|
@ -607,14 +558,26 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
Thread deleteThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
recordingsLock.lock();
|
||||
try {
|
||||
recorder.delete(r);
|
||||
Platform.runLater(() -> observableRecordings.remove(r));
|
||||
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
|
||||
LOG.error("Error while deleting recording", e1);
|
||||
showErrorDialog("Error while deleting recording", "Recording not deleted", e1);
|
||||
List<Recording> deleted = new ArrayList<>();
|
||||
for (Iterator<JavaFxRecording> iterator = recordings.iterator(); iterator.hasNext();) {
|
||||
JavaFxRecording r = iterator.next();
|
||||
if(r.getStatus() != STATUS.FINISHED) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
recorder.delete(r);
|
||||
deleted.add(r);
|
||||
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
|
||||
LOG.error("Error while deleting recording", e1);
|
||||
showErrorDialog("Error while deleting recording", "Recording not deleted", e1);
|
||||
}
|
||||
}
|
||||
observableRecordings.removeAll(deleted);
|
||||
} finally {
|
||||
table.setCursor(Cursor.DEFAULT);
|
||||
recordingsLock.unlock();
|
||||
Platform.runLater(() -> table.setCursor(Cursor.DEFAULT));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -72,6 +72,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
private CheckBox chooseStreamQuality = new CheckBox();
|
||||
private CheckBox multiplePlayers = new CheckBox();
|
||||
private CheckBox updateThumbnails = new CheckBox();
|
||||
private CheckBox showPlayerStarting = new CheckBox();
|
||||
private RadioButton recordLocal;
|
||||
private RadioButton recordRemote;
|
||||
private ToggleGroup recordLocation;
|
||||
|
@ -409,6 +410,18 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
GridPane.setMargin(multiplePlayers, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
||||
layout.add(multiplePlayers, 1, row++);
|
||||
|
||||
l = new Label("Show \"Player Starting\" Message");
|
||||
layout.add(l, 0, row);
|
||||
showPlayerStarting.setSelected(Config.getInstance().getSettings().showPlayerStarting);
|
||||
showPlayerStarting.setOnAction((e) -> {
|
||||
Config.getInstance().getSettings().showPlayerStarting = showPlayerStarting.isSelected();
|
||||
saveConfig();
|
||||
});
|
||||
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
|
||||
GridPane.setMargin(showPlayerStarting, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
||||
layout.add(showPlayerStarting, 1, row++);
|
||||
|
||||
|
||||
l = new Label("Display stream resolution in overview");
|
||||
layout.add(l, 0, row);
|
||||
loadResolution = new CheckBox();
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package ctbrec.ui;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
@ -15,7 +16,9 @@ public class StreamSourceSelectionDialog {
|
|||
Task<List<StreamSource>> selectStreamSource = new Task<List<StreamSource>>() {
|
||||
@Override
|
||||
protected List<StreamSource> call() throws Exception {
|
||||
return model.getStreamSources();
|
||||
List<StreamSource> sources = model.getStreamSources();
|
||||
Collections.sort(sources);
|
||||
return sources;
|
||||
}
|
||||
};
|
||||
selectStreamSource.setOnSucceeded((e) -> {
|
||||
|
|
|
@ -5,6 +5,8 @@ import java.io.IOException;
|
|||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
@ -14,6 +16,7 @@ import com.iheartradio.m3u8.ParseException;
|
|||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.io.HttpException;
|
||||
import ctbrec.recorder.Recorder;
|
||||
import ctbrec.ui.controls.Toast;
|
||||
import javafx.animation.FadeTransition;
|
||||
|
@ -43,6 +46,8 @@ import javafx.scene.text.Font;
|
|||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextAlignment;
|
||||
import javafx.util.Duration;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class ThumbCell extends StackPane {
|
||||
|
||||
|
@ -74,6 +79,7 @@ public class ThumbCell extends StackPane {
|
|||
private ObservableList<Node> thumbCellList;
|
||||
private boolean mouseHovering = false;
|
||||
private boolean recording = false;
|
||||
private static ExecutorService imageLoadingThreadPool = Executors.newFixedThreadPool(30);
|
||||
|
||||
public ThumbCell(ThumbOverviewTab parent, Model model, Recorder recorder) {
|
||||
this.thumbCellList = parent.grid.getChildren();
|
||||
|
@ -110,7 +116,7 @@ public class ThumbCell extends StackPane {
|
|||
StackPane.setMargin(resolutionBackground, new Insets(2));
|
||||
getChildren().add(resolutionBackground);
|
||||
|
||||
name = new Text(model.getName());
|
||||
name = new Text(model.getDisplayName());
|
||||
name.setFill(Color.WHITE);
|
||||
name.setFont(new Font("Sansserif", 16));
|
||||
name.setTextAlignment(TextAlignment.CENTER);
|
||||
|
@ -267,18 +273,35 @@ public class ThumbCell extends StackPane {
|
|||
if(!Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
|
||||
boolean updateThumbs = Config.getInstance().getSettings().updateThumbnails;
|
||||
if(updateThumbs || iv.getImage() == null) {
|
||||
Image img = new Image(url, true);
|
||||
|
||||
// wait for the image to load, otherwise the ImageView replaces the current image with an "empty" image,
|
||||
// which causes to show the grey background until the image is loaded
|
||||
img.progressProperty().addListener(new ChangeListener<Number>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
|
||||
if(newValue.doubleValue() == 1.0) {
|
||||
//imgAspectRatio = img.getHeight() / img.getWidth();
|
||||
iv.setImage(img);
|
||||
setThumbWidth(Config.getInstance().getSettings().thumbWidth);
|
||||
imageLoadingThreadPool.submit(() -> {
|
||||
Request req = new Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
|
||||
.build();
|
||||
try(Response resp = CamrecApplication.httpClient.execute(req)) {
|
||||
if(resp.isSuccessful()) {
|
||||
Image img = new Image(resp.body().byteStream());
|
||||
if(img.progressProperty().get() == 1.0) {
|
||||
Platform.runLater(() -> {
|
||||
iv.setImage(img);
|
||||
setThumbWidth(Config.getInstance().getSettings().thumbWidth);
|
||||
});
|
||||
} else {
|
||||
img.progressProperty().addListener(new ChangeListener<Number>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
|
||||
if(newValue.doubleValue() == 1.0) {
|
||||
iv.setImage(img);
|
||||
setThumbWidth(Config.getInstance().getSettings().thumbWidth);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw new HttpException(resp.code(), resp.message());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error loading image", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -308,7 +331,7 @@ public class ThumbCell extends StackPane {
|
|||
boolean started = Player.play(model);
|
||||
Platform.runLater(() -> {
|
||||
setCursor(Cursor.DEFAULT);
|
||||
if (started) {
|
||||
if (started && Config.getInstance().getSettings().showPlayerStarting) {
|
||||
Toast.makeText(getScene(), "Starting Player", 2000, 500, 500);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -753,6 +753,8 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
String[] tokens = filter.split(" ");
|
||||
StringBuilder searchTextBuilder = new StringBuilder(m.getName());
|
||||
searchTextBuilder.append(' ');
|
||||
searchTextBuilder.append(m.getDisplayName());
|
||||
searchTextBuilder.append(' ');
|
||||
for (String tag : m.getTags()) {
|
||||
searchTextBuilder.append(tag).append(' ');
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ import java.util.Optional;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.recorder.Recorder;
|
||||
import ctbrec.ui.Player;
|
||||
|
@ -86,7 +87,7 @@ public class SearchPopoverTreeList extends PopoverTreeList<Model> implements Pop
|
|||
new Thread(() -> {
|
||||
Platform.runLater(() -> {
|
||||
boolean started = Player.play(model);
|
||||
if(started) {
|
||||
if(started && Config.getInstance().getSettings().showPlayerStarting) {
|
||||
Toast.makeText(getScene(), "Starting Player", 2000, 500, 500);
|
||||
}
|
||||
setCursor(Cursor.DEFAULT);
|
||||
|
@ -230,7 +231,7 @@ public class SearchPopoverTreeList extends PopoverTreeList<Model> implements Pop
|
|||
} else {
|
||||
follow.setVisible(model.getSite().supportsFollow());
|
||||
title.setVisible(true);
|
||||
title.setText(model.getName());
|
||||
title.setText(model.getDisplayName());
|
||||
this.model = model;
|
||||
URL anonymousPng = getClass().getResource("/anonymous.png");
|
||||
String previewUrl = Optional.ofNullable(model.getPreview()).orElse(anonymousPng.toString());
|
||||
|
|
|
@ -70,6 +70,9 @@ public class BongaCamsUpdateService extends PaginatedScheduledService {
|
|||
model.setOnlineState("offline");
|
||||
}
|
||||
model.setPreview("https:" + m.getString("thumb_image"));
|
||||
if(m.has("display_name")) {
|
||||
model.setDisplayName(m.getString("display_name"));
|
||||
}
|
||||
models.add(model);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,6 +62,7 @@ public class CamsodaUpdateService extends PaginatedScheduledService {
|
|||
if(result.has("tpl")) {
|
||||
JSONArray tpl = result.getJSONArray("tpl");
|
||||
String name = tpl.getString(0);
|
||||
String displayName = tpl.getString(1);
|
||||
// int connections = tpl.getInt(2);
|
||||
String streamName = tpl.getString(5);
|
||||
String tsize = tpl.getString(6);
|
||||
|
@ -77,11 +78,11 @@ public class CamsodaUpdateService extends PaginatedScheduledService {
|
|||
JSONArray edgeServers = result.getJSONArray("edge_servers");
|
||||
model.setStreamUrl("https://" + edgeServers.getString(0) + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8");
|
||||
}
|
||||
model.setDisplayName(displayName);
|
||||
models.add(model);
|
||||
} else {
|
||||
String name = result.getString("username");
|
||||
CamsodaModel model = (CamsodaModel) camsoda.createModel(name);
|
||||
|
||||
if(result.has("server_prefix")) {
|
||||
String serverPrefix = result.getString("server_prefix");
|
||||
String streamName = result.getString("stream_name");
|
||||
|
@ -91,6 +92,10 @@ public class CamsodaUpdateService extends PaginatedScheduledService {
|
|||
model.setOnlineState(result.getString("status"));
|
||||
}
|
||||
|
||||
if(result.has("display_name")) {
|
||||
model.setDisplayName(result.getString("display_name"));
|
||||
}
|
||||
|
||||
if(result.has("edge_servers")) {
|
||||
JSONArray edgeServers = result.getJSONArray("edge_servers");
|
||||
model.setStreamUrl("https://" + edgeServers.getString(0) + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8");
|
||||
|
|
|
@ -42,14 +42,29 @@ public class ChaturbateConfigUi extends AbstractConfigUI {
|
|||
GridPane.setColumnSpan(password, 2);
|
||||
layout.add(password, 1, 1);
|
||||
|
||||
layout.add(new Label("Chaturbate Base URL"), 0, 2);
|
||||
TextField baseUrl = new TextField();
|
||||
baseUrl.setText(Config.getInstance().getSettings().chaturbateBaseUrl);
|
||||
baseUrl.textProperty().addListener((ob, o, n) -> {
|
||||
Config.getInstance().getSettings().chaturbateBaseUrl = baseUrl.getText();
|
||||
save();
|
||||
});
|
||||
GridPane.setFillWidth(baseUrl, true);
|
||||
GridPane.setHgrow(baseUrl, Priority.ALWAYS);
|
||||
GridPane.setColumnSpan(baseUrl, 2);
|
||||
layout.add(baseUrl, 1, 2);
|
||||
|
||||
Button createAccount = new Button("Create new Account");
|
||||
createAccount.setOnAction((e) -> DesktopIntegration.open(Chaturbate.REGISTRATION_LINK));
|
||||
layout.add(createAccount, 1, 2);
|
||||
layout.add(createAccount, 1, 3);
|
||||
GridPane.setColumnSpan(createAccount, 2);
|
||||
GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
||||
GridPane.setMargin(password, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
||||
GridPane.setMargin(baseUrl, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
||||
GridPane.setMargin(createAccount, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
||||
|
||||
username.setPrefWidth(300);
|
||||
|
||||
return layout;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package ctbrec.ui.sites.chaturbate;
|
||||
|
||||
import static ctbrec.sites.chaturbate.Chaturbate.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -21,17 +19,17 @@ public class ChaturbateTabProvider extends TabProvider {
|
|||
public ChaturbateTabProvider(Chaturbate chaturbate) {
|
||||
this.chaturbate = chaturbate;
|
||||
this.recorder = chaturbate.getRecorder();
|
||||
this.followedTab = new ChaturbateFollowedTab("Followed", BASE_URI + "/followed-cams/", chaturbate);
|
||||
this.followedTab = new ChaturbateFollowedTab("Followed", chaturbate.getBaseUrl() + "/followed-cams/", chaturbate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Tab> getTabs(Scene scene) {
|
||||
List<Tab> tabs = new ArrayList<>();
|
||||
tabs.add(createTab("Featured", BASE_URI + "/"));
|
||||
tabs.add(createTab("Female", BASE_URI + "/female-cams/"));
|
||||
tabs.add(createTab("Male", BASE_URI + "/male-cams/"));
|
||||
tabs.add(createTab("Couples", BASE_URI + "/couple-cams/"));
|
||||
tabs.add(createTab("Trans", BASE_URI + "/trans-cams/"));
|
||||
tabs.add(createTab("Featured", chaturbate.getBaseUrl() + "/"));
|
||||
tabs.add(createTab("Female", chaturbate.getBaseUrl() + "/female-cams/"));
|
||||
tabs.add(createTab("Male", chaturbate.getBaseUrl() + "/male-cams/"));
|
||||
tabs.add(createTab("Couples", chaturbate.getBaseUrl() + "/couple-cams/"));
|
||||
tabs.add(createTab("Trans", chaturbate.getBaseUrl() + "/trans-cams/"));
|
||||
followedTab.setScene(scene);
|
||||
followedTab.setRecorder(recorder);
|
||||
tabs.add(followedTab);
|
||||
|
|
|
@ -14,6 +14,7 @@ public abstract class AbstractModel implements Model {
|
|||
|
||||
private String url;
|
||||
private String name;
|
||||
private String displayName;
|
||||
private String preview;
|
||||
private String description;
|
||||
private List<String> tags = new ArrayList<>();
|
||||
|
@ -46,6 +47,20 @@ public abstract class AbstractModel implements Model {
|
|||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
if(displayName != null) {
|
||||
return displayName;
|
||||
} else {
|
||||
return getName();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDisplayName(String name) {
|
||||
this.displayName = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPreview() {
|
||||
return preview;
|
||||
|
|
|
@ -7,6 +7,7 @@ import java.io.FileInputStream;
|
|||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
@ -46,7 +47,6 @@ public class Config {
|
|||
} else {
|
||||
filename = "settings.json";
|
||||
}
|
||||
load();
|
||||
}
|
||||
|
||||
private void load() throws FileNotFoundException, IOException {
|
||||
|
@ -61,6 +61,13 @@ public class Config {
|
|||
BufferedSource source = buffer.readFrom(fin);
|
||||
settings = adapter.fromJson(source);
|
||||
settings.httpTimeout = Math.max(settings.httpTimeout, 10_000);
|
||||
} catch(Throwable e) {
|
||||
settings = OS.getDefaultSettings();
|
||||
for (Site site : sites) {
|
||||
site.setEnabled(!settings.disabledSites.contains(site.getName()));
|
||||
}
|
||||
makeBackup(configFile);
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
LOG.error("Config file does not exist. Falling back to default values.");
|
||||
|
@ -71,9 +78,22 @@ public class Config {
|
|||
}
|
||||
}
|
||||
|
||||
private void makeBackup(File source) {
|
||||
try {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
|
||||
String timestamp = sdf.format(new Date());
|
||||
String backup = source.getName() + '.' + timestamp;
|
||||
File target = new File(source.getParentFile(), backup);
|
||||
Files.copy(source.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch(Throwable e) {
|
||||
LOG.error("Couldn't create backup of settings file", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void init(List<Site> sites) throws FileNotFoundException, IOException {
|
||||
if(instance == null) {
|
||||
instance = new Config(sites);
|
||||
instance.load();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,8 @@ import ctbrec.sites.Site;
|
|||
public interface Model {
|
||||
public String getUrl();
|
||||
public void setUrl(String url);
|
||||
public String getDisplayName();
|
||||
public void setDisplayName(String name);
|
||||
public String getName();
|
||||
public void setName(String name);
|
||||
public String getPreview();
|
||||
|
|
|
@ -30,6 +30,7 @@ public class Settings {
|
|||
}
|
||||
|
||||
public boolean singlePlayer = true;
|
||||
public boolean showPlayerStarting = false;
|
||||
public boolean localRecording = true;
|
||||
public int httpPort = 8080;
|
||||
public int httpTimeout = 10000;
|
||||
|
@ -42,6 +43,7 @@ public class Settings {
|
|||
public String postProcessing = "";
|
||||
public String username = ""; // chaturbate username TODO maybe rename this onetime
|
||||
public String password = ""; // chaturbate password TODO maybe rename this onetime
|
||||
public String chaturbateBaseUrl = "https://chaturbate.com";
|
||||
public String bongaUsername = "";
|
||||
public String bongaPassword = "";
|
||||
public String mfcUsername = "";
|
||||
|
|
|
@ -431,7 +431,8 @@ public class LocalRecorder implements Recorder {
|
|||
running = true;
|
||||
while (running) {
|
||||
Instant begin = Instant.now();
|
||||
for (Model model : getModelsRecording()) {
|
||||
List<Model> models = getModelsRecording();
|
||||
for (Model model : models) {
|
||||
try {
|
||||
boolean isOnline = model.isOnline(IGNORE_CACHE);
|
||||
LOG.trace("Checking online state for {}: {}", model, (isOnline ? "online" : "offline"));
|
||||
|
@ -450,6 +451,7 @@ public class LocalRecorder implements Recorder {
|
|||
}
|
||||
Instant end = Instant.now();
|
||||
Duration timeCheckTook = Duration.between(begin, end);
|
||||
LOG.trace("Online check for {} models took {} seconds", models.size(), timeCheckTook.getSeconds());
|
||||
|
||||
long sleepTime = Config.getInstance().getSettings().onlineCheckIntervalInSecs;
|
||||
if(timeCheckTook.getSeconds() < sleepTime) {
|
||||
|
@ -710,13 +712,20 @@ public class LocalRecorder implements Recorder {
|
|||
|
||||
@Override
|
||||
public void switchStreamSource(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
||||
LOG.debug("Switching stream source to index {} for model {}", model.getStreamUrlIndex(), model.getName());
|
||||
Download download = recordingProcesses.get(model);
|
||||
if(download != null) {
|
||||
stopRecordingProcess(model);
|
||||
if (models.contains(model)) {
|
||||
int index = models.indexOf(model);
|
||||
models.get(index).setStreamUrlIndex(model.getStreamUrlIndex());
|
||||
config.save();
|
||||
LOG.debug("Switching stream source to index {} for model {}", model.getStreamUrlIndex(), model.getName());
|
||||
Download download = recordingProcesses.get(model);
|
||||
if(download != null) {
|
||||
stopRecordingProcess(model);
|
||||
}
|
||||
tryRestartRecording(model);
|
||||
} else {
|
||||
LOG.warn("Couldn't switch stream source for model {}. Not found in list", model.getName());
|
||||
return;
|
||||
}
|
||||
tryRestartRecording(model);
|
||||
config.save();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -752,13 +761,17 @@ public class LocalRecorder implements Recorder {
|
|||
int index = models.indexOf(model);
|
||||
Model m = models.get(index);
|
||||
m.setSuspended(false);
|
||||
startRecordingProcess(m);
|
||||
if(m.isOnline()) {
|
||||
startRecordingProcess(m);
|
||||
}
|
||||
model.setSuspended(false);
|
||||
config.save();
|
||||
} else {
|
||||
LOG.warn("Couldn't resume model {}. Not found in list", model.getName());
|
||||
return;
|
||||
}
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
LOG.error("Couldn't check, if model {} is online", model.getName());
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
@ -787,6 +800,10 @@ public class LocalRecorder implements Recorder {
|
|||
|
||||
private boolean enoughSpaceForRecording() throws IOException {
|
||||
long minimum = config.getSettings().minimumSpaceLeftInBytes;
|
||||
return getFreeSpaceBytes() > minimum;
|
||||
if(minimum == 0) { // 0 means don't check
|
||||
return true;
|
||||
} else {
|
||||
return getFreeSpaceBytes() > minimum;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory;
|
|||
import com.iheartradio.m3u8.Encoding;
|
||||
import com.iheartradio.m3u8.Format;
|
||||
import com.iheartradio.m3u8.ParseException;
|
||||
import com.iheartradio.m3u8.ParsingMode;
|
||||
import com.iheartradio.m3u8.PlaylistException;
|
||||
import com.iheartradio.m3u8.PlaylistParser;
|
||||
import com.iheartradio.m3u8.PlaylistWriter;
|
||||
|
@ -190,7 +191,7 @@ public class PlaylistGenerator {
|
|||
public void validate(File recDir) throws IOException, ParseException, PlaylistException {
|
||||
File playlist = new File(recDir, "playlist.m3u8");
|
||||
if(playlist.exists()) {
|
||||
PlaylistParser playlistParser = new PlaylistParser(new FileInputStream(playlist), Format.EXT_M3U, Encoding.UTF_8);
|
||||
PlaylistParser playlistParser = new PlaylistParser(new FileInputStream(playlist), Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
|
||||
Playlist m3u = playlistParser.parse();
|
||||
MediaPlaylist mediaPlaylist = m3u.getMediaPlaylist();
|
||||
int playlistSize = mediaPlaylist.getTracks().size();
|
||||
|
|
|
@ -83,12 +83,18 @@ public abstract class AbstractHlsDownload implements Download {
|
|||
|
||||
|
||||
String getSegmentPlaylistUrl(Model model) throws IOException, ExecutionException, ParseException, PlaylistException {
|
||||
LOG.debug("{} stream idx: {}", model.getName(), model.getStreamUrlIndex());
|
||||
List<StreamSource> streamSources = model.getStreamSources();
|
||||
Collections.sort(streamSources);
|
||||
for (StreamSource streamSource : streamSources) {
|
||||
LOG.debug("{} src {}", model.getName(), streamSource);
|
||||
}
|
||||
String url = null;
|
||||
if(model.getStreamUrlIndex() >= 0 && model.getStreamUrlIndex() < streamSources.size()) {
|
||||
// TODO don't use the index, but the bandwidth. if the bandwidth does not match, take the closest one
|
||||
LOG.debug("{} selected {}", model.getName(), streamSources.get(model.getStreamUrlIndex()));
|
||||
url = streamSources.get(model.getStreamUrlIndex()).getMediaPlaylistUrl();
|
||||
} else {
|
||||
Collections.sort(streamSources);
|
||||
// filter out stream resolutions, which are too high
|
||||
int maxRes = Config.getInstance().getSettings().maximumResolution;
|
||||
if(maxRes > 0) {
|
||||
|
@ -103,9 +109,11 @@ public abstract class AbstractHlsDownload implements Download {
|
|||
if(streamSources.isEmpty()) {
|
||||
throw new ExecutionException(new RuntimeException("No stream left in playlist"));
|
||||
} else {
|
||||
LOG.debug("{} selected {}", model.getName(), streamSources.get(streamSources.size()-1));
|
||||
url = streamSources.get(streamSources.size()-1).getMediaPlaylistUrl();
|
||||
}
|
||||
}
|
||||
LOG.debug("Segment playlist url {}", url);
|
||||
return url;
|
||||
}
|
||||
|
||||
|
|
|
@ -46,4 +46,9 @@ public abstract class AbstractSite implements Site {
|
|||
public boolean searchRequiresLogin() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Model createModelFromUrl(String url) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,4 +29,5 @@ public interface Site {
|
|||
public boolean isEnabled();
|
||||
public List<Model> search(String q) throws IOException, InterruptedException;
|
||||
public boolean searchRequiresLogin();
|
||||
public Model createModelFromUrl(String url);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ import java.net.URLEncoder;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
@ -160,6 +162,9 @@ public class BongaCams extends AbstractSite {
|
|||
if(thumb != null) {
|
||||
model.setPreview("https:" + thumb);
|
||||
}
|
||||
if(result.has("display_name")) {
|
||||
model.setDisplayName(result.getString("display_name"));
|
||||
}
|
||||
models.add(model);
|
||||
}
|
||||
return models;
|
||||
|
@ -184,4 +189,14 @@ public class BongaCams extends AbstractSite {
|
|||
return username != null && !username.trim().isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Model createModelFromUrl(String url) {
|
||||
Matcher m = Pattern.compile("https?://.*?bongacams.com(?:/profile)?/([^/]*?)/?").matcher(url);
|
||||
if(m.matches()) {
|
||||
String modelName = m.group(1);
|
||||
return createModel(modelName);
|
||||
} else {
|
||||
return super.createModelFromUrl(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import org.slf4j.LoggerFactory;
|
|||
import com.iheartradio.m3u8.Encoding;
|
||||
import com.iheartradio.m3u8.Format;
|
||||
import com.iheartradio.m3u8.ParseException;
|
||||
import com.iheartradio.m3u8.ParsingMode;
|
||||
import com.iheartradio.m3u8.PlaylistException;
|
||||
import com.iheartradio.m3u8.PlaylistParser;
|
||||
import com.iheartradio.m3u8.data.MasterPlaylist;
|
||||
|
@ -101,7 +102,7 @@ public class BongaCamsModel extends AbstractModel {
|
|||
try(Response response = site.getHttpClient().execute(req)) {
|
||||
if(response.isSuccessful()) {
|
||||
InputStream inputStream = response.body().byteStream();
|
||||
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8);
|
||||
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
|
||||
Playlist playlist = parser.parse();
|
||||
MasterPlaylist master = playlist.getMasterPlaylist();
|
||||
streamSources.clear();
|
||||
|
|
|
@ -4,6 +4,8 @@ import java.io.IOException;
|
|||
import java.net.URLEncoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
@ -154,4 +156,15 @@ public class Cam4 extends AbstractSite {
|
|||
String username = Config.getInstance().getSettings().cam4Username;
|
||||
return username != null && !username.trim().isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Model createModelFromUrl(String url) {
|
||||
Matcher m = Pattern.compile("https?://(?:www\\.)?cam4(?:.*?).com/([^/]*?)/?").matcher(url);
|
||||
if(m.matches()) {
|
||||
String modelName = m.group(1);
|
||||
return createModel(modelName);
|
||||
} else {
|
||||
return super.createModelFromUrl(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import org.slf4j.LoggerFactory;
|
|||
import com.iheartradio.m3u8.Encoding;
|
||||
import com.iheartradio.m3u8.Format;
|
||||
import com.iheartradio.m3u8.ParseException;
|
||||
import com.iheartradio.m3u8.ParsingMode;
|
||||
import com.iheartradio.m3u8.PlaylistException;
|
||||
import com.iheartradio.m3u8.PlaylistParser;
|
||||
import com.iheartradio.m3u8.data.MasterPlaylist;
|
||||
|
@ -129,7 +130,7 @@ public class Cam4Model extends AbstractModel {
|
|||
Response response = site.getHttpClient().execute(req);
|
||||
try {
|
||||
InputStream inputStream = response.body().byteStream();
|
||||
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8);
|
||||
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
|
||||
Playlist playlist = parser.parse();
|
||||
MasterPlaylist master = playlist.getMasterPlaylist();
|
||||
return master;
|
||||
|
|
|
@ -5,6 +5,8 @@ import java.net.URLEncoder;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
@ -138,6 +140,9 @@ public class Camsoda extends AbstractSite {
|
|||
if(thumb != null) {
|
||||
model.setPreview("https:" + thumb);
|
||||
}
|
||||
if(result.has("display_name")) {
|
||||
model.setDisplayName(result.getString("display_name"));
|
||||
}
|
||||
models.add(model);
|
||||
}
|
||||
return models;
|
||||
|
@ -161,4 +166,15 @@ public class Camsoda extends AbstractSite {
|
|||
String username = Config.getInstance().getSettings().camsodaUsername;
|
||||
return username != null && !username.trim().isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Model createModelFromUrl(String url) {
|
||||
Matcher m = Pattern.compile("https?://(?:www\\.)?camsoda.com/([^/]*?)/?").matcher(url);
|
||||
if(m.matches()) {
|
||||
String modelName = m.group(1);
|
||||
return createModel(modelName);
|
||||
} else {
|
||||
return super.createModelFromUrl(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import com.google.common.cache.CacheBuilder;
|
|||
import com.iheartradio.m3u8.Encoding;
|
||||
import com.iheartradio.m3u8.Format;
|
||||
import com.iheartradio.m3u8.ParseException;
|
||||
import com.iheartradio.m3u8.ParsingMode;
|
||||
import com.iheartradio.m3u8.PlaylistException;
|
||||
import com.iheartradio.m3u8.PlaylistParser;
|
||||
import com.iheartradio.m3u8.data.MasterPlaylist;
|
||||
|
@ -112,7 +113,7 @@ public class CamsodaModel extends AbstractModel {
|
|||
Response response = site.getHttpClient().execute(req);
|
||||
try {
|
||||
InputStream inputStream = response.body().byteStream();
|
||||
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8);
|
||||
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
|
||||
Playlist playlist = parser.parse();
|
||||
MasterPlaylist master = playlist.getMasterPlaylist();
|
||||
PlaylistData playlistData = master.getPlaylists().get(0);
|
||||
|
|
|
@ -10,6 +10,8 @@ import java.util.List;
|
|||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -20,6 +22,7 @@ import com.google.common.cache.LoadingCache;
|
|||
import com.iheartradio.m3u8.Encoding;
|
||||
import com.iheartradio.m3u8.Format;
|
||||
import com.iheartradio.m3u8.ParseException;
|
||||
import com.iheartradio.m3u8.ParsingMode;
|
||||
import com.iheartradio.m3u8.PlaylistException;
|
||||
import com.iheartradio.m3u8.PlaylistParser;
|
||||
import com.iheartradio.m3u8.data.MasterPlaylist;
|
||||
|
@ -42,14 +45,14 @@ import okhttp3.Response;
|
|||
public class Chaturbate extends AbstractSite {
|
||||
|
||||
private static final transient Logger LOG = LoggerFactory.getLogger(Chaturbate.class);
|
||||
public static final String BASE_URI = "https://chaturbate.com";
|
||||
public static final String AFFILIATE_LINK = BASE_URI + "/in/?track=default&tour=grq0&campaign=55vTi";
|
||||
public static final String REGISTRATION_LINK = BASE_URI + "/in/?track=default&tour=g4pe&campaign=55vTi";
|
||||
static String baseUrl = "https://chaturbate.com";
|
||||
public static final String AFFILIATE_LINK = "https://chaturbate.com/in/?track=default&tour=grq0&campaign=55vTi";
|
||||
public static final String REGISTRATION_LINK = "https://chaturbate.com/in/?track=default&tour=g4pe&campaign=55vTi";
|
||||
private ChaturbateHttpClient httpClient;
|
||||
|
||||
@Override
|
||||
public void init() throws IOException {
|
||||
|
||||
baseUrl = Config.getInstance().getSettings().chaturbateBaseUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -59,7 +62,7 @@ public class Chaturbate extends AbstractSite {
|
|||
|
||||
@Override
|
||||
public String getBaseUrl() {
|
||||
return "https://chaturbate.com";
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -136,7 +139,7 @@ public class Chaturbate extends AbstractSite {
|
|||
|
||||
@Override
|
||||
public List<Model> search(String q) throws IOException, InterruptedException {
|
||||
String url = BASE_URI + "?keywords=" + URLEncoder.encode(q, "utf-8");
|
||||
String url = baseUrl + "?keywords=" + URLEncoder.encode(q, "utf-8");
|
||||
List<Model> result = new ArrayList<>();
|
||||
|
||||
// search online models
|
||||
|
@ -152,7 +155,7 @@ public class Chaturbate extends AbstractSite {
|
|||
|
||||
// since chaturbate does not return offline models, we at least try, if the profile page
|
||||
// exists for the search string
|
||||
url = BASE_URI + '/' + q;
|
||||
url = baseUrl + '/' + q;
|
||||
req = new Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
|
||||
|
@ -324,7 +327,7 @@ public class Chaturbate extends AbstractSite {
|
|||
try (Response response = getHttpClient().execute(req)) {
|
||||
if(response.isSuccessful()) {
|
||||
InputStream inputStream = response.body().byteStream();
|
||||
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8);
|
||||
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
|
||||
Playlist playlist = parser.parse();
|
||||
MasterPlaylist master = playlist.getMasterPlaylist();
|
||||
return master;
|
||||
|
@ -339,4 +342,15 @@ public class Chaturbate extends AbstractSite {
|
|||
String username = Config.getInstance().getSettings().username;
|
||||
return username != null && !username.trim().isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Model createModelFromUrl(String url) {
|
||||
Matcher m = Pattern.compile("https?://.*?chaturbate.com(?:/p)?/([^/]*?)/?").matcher(url);
|
||||
if(m.matches()) {
|
||||
String modelName = m.group(1);
|
||||
return createModel(modelName);
|
||||
} else {
|
||||
return super.createModelFromUrl(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ public class ChaturbateHttpClient extends HttpClient {
|
|||
|
||||
try {
|
||||
Request login = new Request.Builder()
|
||||
.url(Chaturbate.BASE_URI + "/auth/login/")
|
||||
.url(Chaturbate.baseUrl + "/auth/login/")
|
||||
.build();
|
||||
Response response = client.newCall(login).execute();
|
||||
String content = response.body().string();
|
||||
|
@ -68,8 +68,8 @@ public class ChaturbateHttpClient extends HttpClient {
|
|||
.add("csrfmiddlewaretoken", token)
|
||||
.build();
|
||||
login = new Request.Builder()
|
||||
.url(Chaturbate.BASE_URI + "/auth/login/")
|
||||
.header("Referer", Chaturbate.BASE_URI + "/auth/login/")
|
||||
.url(Chaturbate.baseUrl + "/auth/login/")
|
||||
.header("Referer", Chaturbate.baseUrl + "/auth/login/")
|
||||
.post(body)
|
||||
.build();
|
||||
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package ctbrec.sites.chaturbate;
|
||||
|
||||
import static ctbrec.sites.chaturbate.Chaturbate.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
@ -113,6 +111,9 @@ public class ChaturbateModel extends AbstractModel {
|
|||
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
|
||||
String segmentUri = baseUrl + playlist.getUri();
|
||||
src.mediaPlaylistUrl = segmentUri;
|
||||
if(src.mediaPlaylistUrl.contains("?")) {
|
||||
src.mediaPlaylistUrl = src.mediaPlaylistUrl.substring(0, src.mediaPlaylistUrl.lastIndexOf('?'));
|
||||
}
|
||||
LOG.trace("Media playlist {}", src.mediaPlaylistUrl);
|
||||
sources.add(src);
|
||||
}
|
||||
|
@ -137,9 +138,9 @@ public class ChaturbateModel extends AbstractModel {
|
|||
|
||||
String url = null;
|
||||
if(follow) {
|
||||
url = BASE_URI + "/follow/follow/" + getName() + "/";
|
||||
url = getSite().getBaseUrl() + "/follow/follow/" + getName() + "/";
|
||||
} else {
|
||||
url = BASE_URI + "/follow/unfollow/" + getName() + "/";
|
||||
url = getSite().getBaseUrl() + "/follow/unfollow/" + getName() + "/";
|
||||
}
|
||||
|
||||
RequestBody body = RequestBody.create(null, new byte[0]);
|
||||
|
|
|
@ -2,6 +2,8 @@ package ctbrec.sites.mfc;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.jsoup.select.Elements;
|
||||
|
||||
|
@ -122,4 +124,20 @@ public class MyFreeCams extends AbstractSite {
|
|||
String username = Config.getInstance().getSettings().mfcUsername;
|
||||
return username != null && !username.trim().isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Model createModelFromUrl(String url) {
|
||||
String[] patterns = new String[] {
|
||||
"https?://profiles.myfreecams.com/([^/]*?)",
|
||||
"https?://(?:www.)?myfreecams.com/#(.*)"
|
||||
};
|
||||
for (String pattern : patterns) {
|
||||
Matcher m = Pattern.compile(pattern).matcher(url);
|
||||
if(m.matches()) {
|
||||
String modelName = m.group(1);
|
||||
return createModel(modelName);
|
||||
}
|
||||
}
|
||||
return super.createModelFromUrl(url);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import org.slf4j.LoggerFactory;
|
|||
import com.iheartradio.m3u8.Encoding;
|
||||
import com.iheartradio.m3u8.Format;
|
||||
import com.iheartradio.m3u8.ParseException;
|
||||
import com.iheartradio.m3u8.ParsingMode;
|
||||
import com.iheartradio.m3u8.PlaylistException;
|
||||
import com.iheartradio.m3u8.PlaylistParser;
|
||||
import com.iheartradio.m3u8.data.MasterPlaylist;
|
||||
|
@ -98,7 +99,7 @@ public class MyFreeCamsModel extends AbstractModel {
|
|||
}
|
||||
|
||||
private MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException {
|
||||
if(hlsUrl == null) {
|
||||
if(getHlsUrl() == null) {
|
||||
throw new IllegalStateException("Stream url unknown");
|
||||
}
|
||||
LOG.trace("Loading master playlist {}", hlsUrl);
|
||||
|
@ -106,7 +107,7 @@ public class MyFreeCamsModel extends AbstractModel {
|
|||
try(Response response = site.getHttpClient().execute(req)) {
|
||||
if(response.isSuccessful()) {
|
||||
InputStream inputStream = response.body().byteStream();
|
||||
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8);
|
||||
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
|
||||
Playlist playlist = parser.parse();
|
||||
MasterPlaylist master = playlist.getMasterPlaylist();
|
||||
return master;
|
||||
|
@ -116,6 +117,14 @@ public class MyFreeCamsModel extends AbstractModel {
|
|||
}
|
||||
}
|
||||
|
||||
private String getHlsUrl() {
|
||||
if(hlsUrl == null) {
|
||||
MyFreeCams mfc = (MyFreeCams) getSite();
|
||||
mfc.getClient().update(this);
|
||||
}
|
||||
return hlsUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateCacheEntries() {
|
||||
resolution = null;
|
||||
|
|
Loading…
Reference in New Issue