diff --git a/CHANGELOG.md b/CHANGELOG.md
index b791eb9b..7e3294b2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,17 @@
+1.13.0
+========================
+* Added possibility to open small live previews of online models
+ int the Recording tab
+* Added setting to toggle "Player Starting" message
+* Added possibility to add models by their URL
+* Added pause / resume all buttons
+* Setting to define the base URL for MFC and CTB
+* The paused checkbox are now clickable
+* Implemented multi-selection for Recording and Recordings tab
+* Fix: Don't throw exceptions for unknown attributes in PlaylistParser
+* Fix: Don't do space check, if minimum is set to 0
+* Fix: Player not starting when path contains spaces
+
1.12.1
========================
* Fixed downloads in client / server mode
diff --git a/client/pom.xml b/client/pom.xml
index 343c11d1..7b866a81 100644
--- a/client/pom.xml
+++ b/client/pom.xml
@@ -8,7 +8,7 @@
ctbrec
master
- 1.12.1
+ 1.13.0
../master
diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java
index 72c0259d..b55622ff 100644
--- a/client/src/main/java/ctbrec/ui/CamrecApplication.java
+++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java
@@ -250,9 +250,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();
}
diff --git a/client/src/main/java/ctbrec/ui/JavaFxModel.java b/client/src/main/java/ctbrec/ui/JavaFxModel.java
index fccdd9e3..99c4fcb6 100644
--- a/client/src/main/java/ctbrec/ui/JavaFxModel.java
+++ b/client/src/main/java/ctbrec/ui/JavaFxModel.java
@@ -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);
+ }
}
diff --git a/client/src/main/java/ctbrec/ui/Player.java b/client/src/main/java/ctbrec/ui/Player.java
index f9c0d9fd..bace7e78 100644
--- a/client/src/main/java/ctbrec/ui/Player.java
+++ b/client/src/main/java/ctbrec/ui/Player.java
@@ -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();
diff --git a/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java b/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java
new file mode 100644
index 00000000..2305a73a
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java
@@ -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 {
+ 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 row = (TableRow) event.getSource();
+ TableView table = row.getTableView();
+ double offset = 0;
+ double width = 0;
+ for (TableColumn 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 row = (TableRow) event.getSource();
+ TableView 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 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();
+ }
+}
diff --git a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java
index 2fc735fd..f636b424 100644
--- a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java
+++ b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java
@@ -3,6 +3,7 @@ package ctbrec.ui;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
+import java.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 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 row = new TableRow<>();
+ row.addEventHandler(MouseEvent.ANY, previewPopupHandler);
+ return row;
+ });
+ TableColumn preview = new TableColumn<>("🎥");
+ preview.setPrefWidth(35);
+ preview.setCellValueFactory(cdf -> new SimpleStringProperty(" â–¶ "));
+ preview.setEditable(false);
+ preview.setId("preview");
TableColumn name = new TableColumn<>("Model");
name.setPrefWidth(200);
- name.setCellValueFactory(new PropertyValueFactory("name"));
+ name.setCellValueFactory(new PropertyValueFactory("displayName"));
+ name.setEditable(false);
TableColumn url = new TableColumn<>("URL");
url.setCellValueFactory(new PropertyValueFactory("url"));
url.setPrefWidth(400);
+ url.setEditable(false);
TableColumn 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 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 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 selectedModels = table.getSelectionModel().getSelectedItems();
+ if (event.getCode() == KeyCode.DELETE) {
+ stopAction(selectedModels);
+ } else if (event.getCode() == KeyCode.P) {
+ List pausedModels = selectedModels.stream().filter(m -> m.isSuspended()).collect(Collectors.toList());
+ List 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 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 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 models = recorder.getModelsRecording();
+ Consumer 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 models = recorder.getModelsRecording();
+ Consumer 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 models, Consumer 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 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 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 selectedModels) {
+ Consumer 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 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 selectedModels) {
+ Consumer 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 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 selectedModels) {
+ Consumer 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 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 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 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]);
}
diff --git a/client/src/main/java/ctbrec/ui/RecordingsTab.java b/client/src/main/java/ctbrec/ui/RecordingsTab.java
index f8fef9dd..da238bc3 100644
--- a/client/src/main/java/ctbrec/ui/RecordingsTab.java
+++ b/client/src/main/java/ctbrec/ui/RecordingsTab.java
@@ -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 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 name = new TableColumn<>("Model");
name.setPrefWidth(200);
name.setCellValueFactory(new PropertyValueFactory("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 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 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 iterator = observableRecordings.iterator(); iterator.hasNext();) {
- JavaFxRecording old = iterator.next();
- if (!recordings.contains(old)) {
- // remove deleted recordings
- iterator.remove();
+ recordingsLock.lock();
+ try {
+ for (Iterator 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 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 tracks = mediaPlaylist.getTracks();
- // List 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 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 deleted = new ArrayList<>();
+ for (Iterator 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));
}
}
};
diff --git a/client/src/main/java/ctbrec/ui/SettingsTab.java b/client/src/main/java/ctbrec/ui/SettingsTab.java
index d1f816cc..a5182481 100644
--- a/client/src/main/java/ctbrec/ui/SettingsTab.java
+++ b/client/src/main/java/ctbrec/ui/SettingsTab.java
@@ -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();
diff --git a/client/src/main/java/ctbrec/ui/StreamSourceSelectionDialog.java b/client/src/main/java/ctbrec/ui/StreamSourceSelectionDialog.java
index 76a90059..2347493b 100644
--- a/client/src/main/java/ctbrec/ui/StreamSourceSelectionDialog.java
+++ b/client/src/main/java/ctbrec/ui/StreamSourceSelectionDialog.java
@@ -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> selectStreamSource = new Task>() {
@Override
protected List call() throws Exception {
- return model.getStreamSources();
+ List sources = model.getStreamSources();
+ Collections.sort(sources);
+ return sources;
}
};
selectStreamSource.setOnSucceeded((e) -> {
diff --git a/client/src/main/java/ctbrec/ui/ThumbCell.java b/client/src/main/java/ctbrec/ui/ThumbCell.java
index eb316656..ece9efce 100644
--- a/client/src/main/java/ctbrec/ui/ThumbCell.java
+++ b/client/src/main/java/ctbrec/ui/ThumbCell.java
@@ -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 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() {
- @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() {
+ @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);
}
});
diff --git a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java
index 618b0bcd..fce0d327 100644
--- a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java
+++ b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java
@@ -752,6 +752,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(' ');
}
diff --git a/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java b/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java
index 8b949ece..5b58e3a4 100644
--- a/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java
+++ b/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java
@@ -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 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 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());
diff --git a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsUpdateService.java b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsUpdateService.java
index 80f979fe..cd52462a 100644
--- a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsUpdateService.java
+++ b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsUpdateService.java
@@ -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);
}
}
diff --git a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaUpdateService.java b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaUpdateService.java
index f7b6e321..4b035962 100644
--- a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaUpdateService.java
+++ b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaUpdateService.java
@@ -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");
diff --git a/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateConfigUi.java b/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateConfigUi.java
index e48a7892..706ff33c 100644
--- a/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateConfigUi.java
+++ b/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateConfigUi.java
@@ -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;
}
}
diff --git a/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateTabProvider.java b/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateTabProvider.java
index b9864907..f425a23b 100644
--- a/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateTabProvider.java
+++ b/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateTabProvider.java
@@ -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 getTabs(Scene scene) {
List 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);
diff --git a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java
index 79d3bdc9..bf64358b 100644
--- a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java
+++ b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java
@@ -26,6 +26,7 @@ public class MyFreeCamsConfigUI extends AbstractConfigUI {
GridPane layout = SettingsTab.createGridLayout();
layout.add(new Label("MyFreeCams User"), 0, 0);
TextField username = new TextField(Config.getInstance().getSettings().mfcUsername);
+ username.setPrefWidth(300);
username.textProperty().addListener((ob, o, n) -> {
Config.getInstance().getSettings().mfcUsername = username.getText();
save();
@@ -47,13 +48,27 @@ public class MyFreeCamsConfigUI extends AbstractConfigUI {
GridPane.setColumnSpan(password, 2);
layout.add(password, 1, 1);
+ layout.add(new Label("MyFreeCams Base URL"), 0, 2);
+ TextField baseUrl = new TextField();
+ baseUrl.setText(Config.getInstance().getSettings().mfcBaseUrl);
+ baseUrl.textProperty().addListener((ob, o, n) -> {
+ Config.getInstance().getSettings().mfcBaseUrl = 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(myFreeCams.getAffiliateLink()));
- 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));
+
return layout;
}
}
diff --git a/common/pom.xml b/common/pom.xml
index 3206ef56..3fef25a0 100644
--- a/common/pom.xml
+++ b/common/pom.xml
@@ -8,7 +8,7 @@
ctbrec
master
- 1.12.1
+ 1.13.0
../master
diff --git a/common/src/main/java/ctbrec/AbstractModel.java b/common/src/main/java/ctbrec/AbstractModel.java
index ce8b7ef1..61238759 100644
--- a/common/src/main/java/ctbrec/AbstractModel.java
+++ b/common/src/main/java/ctbrec/AbstractModel.java
@@ -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 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;
diff --git a/common/src/main/java/ctbrec/Config.java b/common/src/main/java/ctbrec/Config.java
index 871c36ff..9170a5f1 100644
--- a/common/src/main/java/ctbrec/Config.java
+++ b/common/src/main/java/ctbrec/Config.java
@@ -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 sites) throws FileNotFoundException, IOException {
if(instance == null) {
instance = new Config(sites);
+ instance.load();
}
}
diff --git a/common/src/main/java/ctbrec/Model.java b/common/src/main/java/ctbrec/Model.java
index 3144f777..e13f2fcd 100644
--- a/common/src/main/java/ctbrec/Model.java
+++ b/common/src/main/java/ctbrec/Model.java
@@ -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();
diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java
index 3b613845..64fc8e42 100644
--- a/common/src/main/java/ctbrec/Settings.java
+++ b/common/src/main/java/ctbrec/Settings.java
@@ -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,10 +43,12 @@ 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 = "";
public String mfcPassword = "";
+ public String mfcBaseUrl = "https://www.myfreecams.com";
public String camsodaUsername = "";
public String camsodaPassword = "";
public String cam4Username;
diff --git a/common/src/main/java/ctbrec/recorder/LocalRecorder.java b/common/src/main/java/ctbrec/recorder/LocalRecorder.java
index e6df866b..4d71e58e 100644
--- a/common/src/main/java/ctbrec/recorder/LocalRecorder.java
+++ b/common/src/main/java/ctbrec/recorder/LocalRecorder.java
@@ -431,7 +431,8 @@ public class LocalRecorder implements Recorder {
running = true;
while (running) {
Instant begin = Instant.now();
- for (Model model : getModelsRecording()) {
+ List 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;
+ }
}
}
diff --git a/common/src/main/java/ctbrec/recorder/PlaylistGenerator.java b/common/src/main/java/ctbrec/recorder/PlaylistGenerator.java
index e615f63f..2fec113c 100644
--- a/common/src/main/java/ctbrec/recorder/PlaylistGenerator.java
+++ b/common/src/main/java/ctbrec/recorder/PlaylistGenerator.java
@@ -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();
diff --git a/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java b/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java
index 04b11402..1fb6333d 100644
--- a/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java
+++ b/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java
@@ -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 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;
}
diff --git a/common/src/main/java/ctbrec/sites/AbstractSite.java b/common/src/main/java/ctbrec/sites/AbstractSite.java
index 96d67005..4ecd6465 100644
--- a/common/src/main/java/ctbrec/sites/AbstractSite.java
+++ b/common/src/main/java/ctbrec/sites/AbstractSite.java
@@ -46,4 +46,9 @@ public abstract class AbstractSite implements Site {
public boolean searchRequiresLogin() {
return false;
}
+
+ @Override
+ public Model createModelFromUrl(String url) {
+ return null;
+ }
}
diff --git a/common/src/main/java/ctbrec/sites/Site.java b/common/src/main/java/ctbrec/sites/Site.java
index cf6f3119..9225b52c 100644
--- a/common/src/main/java/ctbrec/sites/Site.java
+++ b/common/src/main/java/ctbrec/sites/Site.java
@@ -29,4 +29,5 @@ public interface Site {
public boolean isEnabled();
public List search(String q) throws IOException, InterruptedException;
public boolean searchRequiresLogin();
+ public Model createModelFromUrl(String url);
}
diff --git a/common/src/main/java/ctbrec/sites/bonga/BongaCams.java b/common/src/main/java/ctbrec/sites/bonga/BongaCams.java
index 6b2670d8..fc847912 100644
--- a/common/src/main/java/ctbrec/sites/bonga/BongaCams.java
+++ b/common/src/main/java/ctbrec/sites/bonga/BongaCams.java
@@ -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);
+ }
+ }
}
diff --git a/common/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java b/common/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java
index a6fd377f..eaad5853 100644
--- a/common/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java
+++ b/common/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java
@@ -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();
diff --git a/common/src/main/java/ctbrec/sites/cam4/Cam4.java b/common/src/main/java/ctbrec/sites/cam4/Cam4.java
index 04b032f4..8c3907a0 100644
--- a/common/src/main/java/ctbrec/sites/cam4/Cam4.java
+++ b/common/src/main/java/ctbrec/sites/cam4/Cam4.java
@@ -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);
+ }
+ }
}
diff --git a/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java b/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java
index 68b24354..95686b19 100644
--- a/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java
+++ b/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java
@@ -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;
diff --git a/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java b/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java
index e79688fa..3008f14b 100644
--- a/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java
+++ b/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java
@@ -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);
+ }
+ }
}
diff --git a/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java b/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java
index 53af0dad..b3d03d94 100644
--- a/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java
+++ b/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java
@@ -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);
diff --git a/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java b/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java
index 534a39bb..bd96ad96 100644
--- a/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java
+++ b/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java
@@ -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 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 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)
@@ -237,7 +240,7 @@ public class Chaturbate extends AbstractSite {
.add("bandwidth", "high")
.build();
Request req = new Request.Builder()
- .url("https://chaturbate.com/get_edge_hls_url_ajax/")
+ .url(getBaseUrl() + "/get_edge_hls_url_ajax/")
.post(body)
.addHeader("X-Requested-With", "XMLHttpRequest")
.build();
@@ -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);
+ }
+ }
}
diff --git a/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateHttpClient.java b/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateHttpClient.java
index 9950bccd..fbfffa70 100644
--- a/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateHttpClient.java
+++ b/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateHttpClient.java
@@ -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();
diff --git a/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java b/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java
index bd17cd23..5ca806c1 100644
--- a/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java
+++ b/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java
@@ -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]);
diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java
index 146c834a..315d040c 100644
--- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java
+++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java
@@ -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;
@@ -15,13 +17,14 @@ import okhttp3.Response;
public class MyFreeCams extends AbstractSite {
- public static final String BASE_URI = "https://www.myfreecams.com";
+ static String baseUrl = "https://www.myfreecams.com";
private MyFreeCamsClient client;
private MyFreeCamsHttpClient httpClient;
@Override
public void init() throws IOException {
+ baseUrl = Config.getInstance().getSettings().mfcBaseUrl;
client = MyFreeCamsClient.getInstance();
client.setSite(this);
client.start();
@@ -39,12 +42,12 @@ public class MyFreeCams extends AbstractSite {
@Override
public String getBaseUrl() {
- return BASE_URI;
+ return baseUrl;
}
@Override
public String getAffiliateLink() {
- return BASE_URI + "/?baf=8127165";
+ return baseUrl + "/?baf=8127165";
}
@Override
@@ -57,7 +60,7 @@ public class MyFreeCams extends AbstractSite {
@Override
public Integer getTokenBalance() throws IOException {
- Request req = new Request.Builder().url(BASE_URI + "/php/account.php?request=status").build();
+ Request req = new Request.Builder().url(baseUrl + "/php/account.php?request=status").build();
try(Response response = getHttpClient().execute(req)) {
if(response.isSuccessful()) {
String content = response.body().string();
@@ -72,7 +75,7 @@ public class MyFreeCams extends AbstractSite {
@Override
public String getBuyTokensLink() {
- return BASE_URI + "/php/purchase.php?request=tokens";
+ return baseUrl + "/php/purchase.php?request=tokens";
}
@Override
@@ -122,4 +125,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);
+ }
}
diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java
index 013dea20..6f167b77 100644
--- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java
+++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java
@@ -85,7 +85,7 @@ public class MyFreeCamsClient {
public void start() throws IOException {
running = true;
- serverConfig = new ServerConfig(mfc.getHttpClient());
+ serverConfig = new ServerConfig(mfc);
List websocketServers = new ArrayList(serverConfig.wsServers.keySet());
String server = websocketServers.get((int) (Math.random()*websocketServers.size()));
String wsUrl = "ws://" + server + ".myfreecams.com:8080/fcsl";
diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsHttpClient.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsHttpClient.java
index 1225ee45..8100b7b8 100644
--- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsHttpClient.java
+++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsHttpClient.java
@@ -53,8 +53,8 @@ public class MyFreeCamsHttpClient extends HttpClient {
.add("submit_login", "97")
.build();
Request req = new Request.Builder()
- .url(MyFreeCams.BASE_URI + "/php/login.php")
- .header("Referer", MyFreeCams.BASE_URI)
+ .url(MyFreeCams.baseUrl + "/php/login.php")
+ .header("Referer", MyFreeCams.baseUrl)
.header("Content-Type", "application/x-www-form-urlencoded")
.post(body)
.build();
@@ -75,7 +75,7 @@ public class MyFreeCamsHttpClient extends HttpClient {
}
private boolean checkLogin() throws IOException {
- Request req = new Request.Builder().url(MyFreeCams.BASE_URI + "/php/account.php?request=status").build();
+ Request req = new Request.Builder().url(MyFreeCams.baseUrl + "/php/account.php?request=status").build();
try(Response response = execute(req)) {
if(response.isSuccessful()) {
String content = response.body().string();
@@ -99,7 +99,7 @@ public class MyFreeCamsHttpClient extends HttpClient {
public Cookie getCookie(String name) {
CookieJar jar = client.cookieJar();
- HttpUrl url = HttpUrl.parse(MyFreeCams.BASE_URI);
+ HttpUrl url = HttpUrl.parse(MyFreeCams.baseUrl);
List cookies = jar.loadForRequest(url);
for (Cookie cookie : cookies) {
if(Objects.equals(cookie.name(), name)) {
diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java
index 45f13c08..d79afa01 100644
--- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java
+++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java
@@ -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;
@@ -123,7 +132,7 @@ public class MyFreeCamsModel extends AbstractModel {
@Override
public void receiveTip(int tokens) throws IOException {
- String tipUrl = MyFreeCams.BASE_URI + "/php/tip.php";
+ String tipUrl = MyFreeCams.baseUrl + "/php/tip.php";
String initUrl = tipUrl + "?request=tip&username="+getName()+"&broadcaster_id="+getUid();
Request req = new Request.Builder().url(initUrl).build();
try(Response resp = site.getHttpClient().execute(req)) {
diff --git a/common/src/main/java/ctbrec/sites/mfc/ServerConfig.java b/common/src/main/java/ctbrec/sites/mfc/ServerConfig.java
index 6713212c..a880e633 100644
--- a/common/src/main/java/ctbrec/sites/mfc/ServerConfig.java
+++ b/common/src/main/java/ctbrec/sites/mfc/ServerConfig.java
@@ -9,13 +9,16 @@ import java.util.Objects;
import org.json.JSONArray;
import org.json.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
-import ctbrec.io.HttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class ServerConfig {
+ private static final transient Logger LOG = LoggerFactory.getLogger(ServerConfig.class);
+
List ajaxServers;
List videoServers;
List chatServers;
@@ -24,9 +27,11 @@ public class ServerConfig {
Map wzobsServers;
Map ngVideoServers;
- public ServerConfig(HttpClient client) throws IOException {
- Request req = new Request.Builder().url("http://www.myfreecams.com/_js/serverconfig.js").build();
- Response resp = client.execute(req);
+ public ServerConfig(MyFreeCams mfc) throws IOException {
+ String url = mfc.getBaseUrl() + "/_js/serverconfig.js";
+ LOG.debug("Loading server config from {}", url);
+ Request req = new Request.Builder().url(url).build();
+ Response resp = mfc.getHttpClient().execute(req);
String json = resp.body().string();
JSONObject serverConfig = new JSONObject(json);
diff --git a/master/pom.xml b/master/pom.xml
index 869bcf9e..8a107451 100644
--- a/master/pom.xml
+++ b/master/pom.xml
@@ -6,7 +6,7 @@
ctbrec
master
pom
- 1.12.1
+ 1.13.0
../common
@@ -68,7 +68,7 @@
com.iheartradio.m3u8
open-m3u8
- 0.2.4
+ 0.2.7-CTBREC
org.jcodec
diff --git a/server/pom.xml b/server/pom.xml
index dcd1739e..32ae4365 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -8,7 +8,7 @@
ctbrec
master
- 1.12.1
+ 1.13.0
../master